├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── build ├── index.js ├── package.json └── util.js ├── bump.json ├── examples ├── preact.esbuild │ ├── package.json │ └── src │ │ ├── index.html │ │ ├── index.js │ │ ├── routes │ │ ├── _layout.jsx │ │ ├── _layout.less │ │ ├── _layout.scss │ │ ├── _layout.styl │ │ ├── blog │ │ │ ├── [id].jsx │ │ │ └── index.jsx │ │ ├── index.jsx │ │ ├── index.less │ │ ├── index.sass │ │ └── index.styl │ │ └── static │ │ └── robots.txt ├── preact │ ├── package.json │ └── src │ │ ├── index.html │ │ ├── index.js │ │ ├── routes │ │ ├── _layout.jsx │ │ ├── blog │ │ │ ├── [id].jsx │ │ │ └── index.jsx │ │ └── index.jsx │ │ └── static │ │ └── robots.txt ├── svelte.typescript │ ├── freshie.config.js │ ├── package.json │ ├── src │ │ ├── errors │ │ │ ├── 5xx.svelte │ │ │ └── _layout.svelte │ │ ├── index.dom.ts │ │ ├── index.html │ │ ├── public │ │ │ ├── manifest.json │ │ │ └── robots.txt │ │ └── routes │ │ │ ├── _layout.svelte │ │ │ ├── blog │ │ │ ├── [id].svelte │ │ │ └── index.svelte │ │ │ └── index.svelte │ └── tsconfig.json ├── svelte │ ├── package.json │ └── src │ │ ├── errors │ │ ├── 5xx.svelte │ │ └── _layout.svelte │ │ ├── index.dom.js │ │ ├── index.html │ │ ├── public │ │ ├── manifest.json │ │ └── robots.txt │ │ └── routes │ │ ├── _layout.svelte │ │ ├── blog │ │ ├── [id].svelte │ │ └── index.svelte │ │ └── index.svelte └── vue │ ├── freshie.config.js │ ├── package.json │ └── src │ ├── index.js │ └── routes │ ├── blog │ ├── [id].vue │ └── index.vue │ └── index.vue ├── license ├── package.json ├── packages ├── @freshie │ ├── plugin.babel │ │ ├── config.js │ │ ├── package.json │ │ └── readme.md │ ├── plugin.esbuild │ │ ├── config.js │ │ ├── package.json │ │ └── readme.md │ ├── plugin.postcss │ │ ├── assets.js │ │ ├── config.js │ │ ├── index.js │ │ ├── package.json │ │ ├── readme.md │ │ └── runtime.js │ ├── plugin.typescript │ │ ├── config.js │ │ ├── package.json │ │ └── readme.md │ ├── ssr.node │ │ ├── config.js │ │ ├── entry.js │ │ ├── index.d.ts │ │ ├── index.js │ │ └── package.json │ ├── ssr.worker │ │ ├── config.js │ │ ├── entry.js │ │ ├── index.js │ │ └── package.json │ ├── ui.preact │ │ ├── _error.jsx │ │ ├── config.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ ├── ui.svelte │ │ ├── _error.svelte │ │ ├── config.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ └── ui.vue │ │ ├── config.js │ │ ├── index.js │ │ └── package.json └── freshie │ ├── @types │ ├── index.d.ts │ └── rollup.d.ts │ ├── bin.js │ ├── env │ ├── index.d.ts │ ├── index.js │ └── index.mjs │ ├── http │ ├── index.d.ts │ ├── index.js │ └── index.mjs │ ├── index.d.ts │ ├── package.json │ ├── router │ ├── index.d.ts │ ├── index.js │ └── index.mjs │ ├── runtime │ ├── index.d.ts │ └── index.dom.js │ └── src │ ├── commands │ ├── build.ts │ └── watch │ │ ├── index.ts │ │ └── watcher.ts │ ├── config │ ├── index.ts │ ├── options.ts │ └── plugins │ │ ├── copy.ts │ │ ├── html.ts │ │ ├── index.ts │ │ ├── router.ts │ │ ├── runtime.ts │ │ ├── summary.ts │ │ ├── template.ts │ │ └── terser.ts │ ├── index.ts │ └── utils │ ├── __tests__ │ ├── argv.ts │ ├── errors.ts │ ├── fs.ts │ └── routes.ts │ ├── argv.ts │ ├── assert.ts │ ├── entries.ts │ ├── errors.ts │ ├── fs.ts │ ├── index.ts │ ├── log.ts │ ├── pretty.ts │ ├── routes.ts │ └── scoped.ts ├── pnpm-workspace.yaml ├── readme.md └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,md}] 13 | indent_style = space 14 | 15 | [fixtures/**.babel.js] 16 | insert_final_newline = false 17 | indent_style = space 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: lukeed 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | name: Node.js v${{ matrix.nodejs }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | nodejs: [10, 14, 18] 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: ${{ matrix.nodejs }} 17 | - uses: pnpm/action-setup@v2.2.4 18 | with: 19 | version: 5 20 | run_install: false 21 | 22 | - name: (env) cache 23 | uses: actions/cache@master 24 | with: 25 | path: | 26 | node_modules 27 | */*/node_modules 28 | key: ${{ runner.os }}-${{ hashFiles('**/package.json') }} 29 | 30 | - name: Install 31 | run: pnpm install 32 | 33 | - name: Test 34 | if: matrix.nodejs < 18 35 | run: pnpm test 36 | 37 | - name: Test w/ Coverage 38 | if: matrix.nodejs >= 18 39 | run: | 40 | pnpm add -g c8 41 | c8 --include=packages pnpm test 42 | 43 | # - name: Report 44 | # if: matrix.nodejs >= 18 45 | # run: | 46 | # c8 report --reporter=text-lcov > coverage.lcov 47 | # bash <(curl -s https://codecov.io/bash) 48 | # env: 49 | # CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *-lock.* 4 | *.lock 5 | *.log 6 | 7 | examples/**/build/** 8 | packages/**/build/** 9 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | const { transpileModule } = require('typescript'); 2 | const tsconfig = require('../tsconfig.json'); 3 | const { build } = require('./util'); 4 | 5 | const terser = require('rollup-plugin-terser').terser(); 6 | const resolve = require('@rollup/plugin-node-resolve').default({ 7 | extensions: ['.ts', '.mjs', '.js', '.json'] 8 | }); 9 | 10 | const typescript = { 11 | name: 'typescript', 12 | transform(code, file) { 13 | if (!/\.ts$/.test(file)) return code; 14 | 15 | // @ts-ignore 16 | let output = transpileModule(code, { 17 | ...tsconfig, 18 | fileName: file 19 | }); 20 | 21 | return { 22 | code: output.outputText, 23 | map: output.sourceMapText || null 24 | }; 25 | } 26 | }; 27 | 28 | // --- 29 | 30 | build('freshie', { 31 | input: 'packages/freshie/src/index.ts', 32 | interop: false, 33 | format: { 34 | cjs: 'build/index.js' 35 | }, 36 | plugins: [ 37 | resolve, 38 | typescript, 39 | terser 40 | ], 41 | }); 42 | -------------------------------------------------------------------------------- /build/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@rollup/plugin-node-resolve": "9.0.0", 5 | "escalade": "3.1.0", 6 | "kleur": "4.1.1", 7 | "rollup": "2.27.1", 8 | "rollup-plugin-terser": "7.0.2", 9 | "rollup-plugin-typescript2": "0.27.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /build/util.js: -------------------------------------------------------------------------------- 1 | // NOTE: lukeed/bundt fork 2 | const kleur = require('kleur'); 3 | const { join } = require('path'); 4 | const { gzipSync } = require('zlib'); 5 | const { builtinModules } = require('module'); 6 | const { rollup } = require('rollup'); 7 | const scale = require('escalade'); 8 | 9 | const GUTTER = ' '; // 4 10 | const UNITS = ['B ', 'kB', 'MB', 'GB']; 11 | const lpad = (str, max, fmt) => fmt(' '.repeat(max - str.length) + str); 12 | const rpad = (str, max, fmt) => fmt(str + ' '.repeat(max - str.length)); 13 | const COL1 = kleur.dim().bold().italic().underline; 14 | const COL2 = kleur.dim().bold().italic; 15 | const PKG = 'package.json'; 16 | 17 | function size(val=0) { 18 | if (val < 1e3) return `${val} ${UNITS[0]}`; 19 | let exp = Math.min(Math.floor(Math.log10(val) / 3), UNITS.length - 1) || 1; 20 | let out = (val / Math.pow(1e3, exp)).toPrecision(3); 21 | let idx = out.indexOf('.'); 22 | if (idx === -1) out += '.00'; 23 | else if (out.length - idx - 1 !== 2) { 24 | out = (out + '00').substring(0, idx + 3); // 2 + 1 for 0-based 25 | } 26 | return out + ' ' + UNITS[exp]; 27 | } 28 | 29 | function table(mode, arr) { 30 | let label = `Package: ${mode}`; 31 | let f=label.length, s=8, g=6, out=''; 32 | 33 | if (arr.length === 1) { 34 | f = Math.max(f, arr[0].file.length); 35 | g = Math.max(g, arr[0].gzip.length); 36 | s = Math.max(s, arr[0].size.length); 37 | } else { 38 | arr.sort((a, b) => { 39 | f = Math.max(f, a.file.length, b.file.length); 40 | g = Math.max(g, a.gzip.length, b.gzip.length); 41 | s = Math.max(s, a.size.length, b.size.length); 42 | return a.file.length - b.file.length; 43 | }); 44 | } 45 | 46 | f += 4; // spacing 47 | 48 | out += rpad(label, f, COL1) + GUTTER + lpad('Filesize', s, COL1) + ' ' + lpad('(gzip)', g, COL2) + '\n'; 49 | 50 | arr.forEach(obj => { 51 | out += rpad(obj.file, f, kleur.white) + GUTTER + lpad(obj.size, s, kleur.cyan) + ' ' + lpad(obj.gzip, g, kleur.dim().italic) + '\n'; 52 | }); 53 | 54 | console.log('\n' + out); 55 | } 56 | 57 | exports.build = async function (name, opts = {}) { 58 | try { 59 | const FILES = []; // outputs 60 | const { input, format, externals=[], plugins=[], ...rest } = opts; 61 | const external = [...builtinModules, ...externals]; 62 | const pkgDir = join('packages', name); 63 | 64 | for (let key in format) { 65 | format[key] = join(pkgDir, format[key]); 66 | } 67 | 68 | // @ts-ignore - bad commonjs definition 69 | const file = await scale(input, (dir, files) => { 70 | return files.includes(PKG) && join(dir, PKG); 71 | }); 72 | 73 | const pkg = file && require(file); 74 | 75 | if (pkg) external.push( 76 | ...Object.keys(pkg.dependencies || {}), 77 | ...Object.keys(pkg.peerDependencies || {}), 78 | ...Object.keys(pkg.optionalDependencies || {}), 79 | ); 80 | 81 | const bundle = await rollup({ input, plugins, external }); 82 | 83 | await Promise.all( 84 | Object.keys(format).map(key => { 85 | return bundle.write({ 86 | // @ts-ignore 87 | format: key, 88 | strict: false, 89 | esModule: false, 90 | file: format[key], 91 | ...rest, 92 | }).then(result => { 93 | let { code } = result.output[0]; 94 | FILES.push({ 95 | file: format[key].replace(/^(\.[\\\/])?/, ''), 96 | gzip: size(gzipSync(code).length), 97 | size: size(code.length), 98 | }); 99 | }); 100 | }) 101 | ); 102 | 103 | table(name, FILES); 104 | } catch (err) { 105 | let msg = (err.message || err || 'Unknown error').replace(/(\r?\n)/g, '$1 '); 106 | console.error(kleur.red().bold('[BUILD] ') + msg); 107 | process.exit(1); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /bump.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.8", 3 | "packages": [ 4 | "packages" 5 | ], 6 | "message": "v{VERSION}" 7 | } -------------------------------------------------------------------------------- /examples/preact.esbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "freshie build" 5 | }, 6 | "dependencies": { 7 | "preact": "10.4.8" 8 | }, 9 | "devDependencies": { 10 | "@freshie/plugin.esbuild": "workspace:*", 11 | "@freshie/plugin.postcss": "workspace:*", 12 | "@freshie/ui.preact": "workspace:*", 13 | "esbuild": "0.7.7", 14 | "freshie": "workspace:*", 15 | "postcss": "7.0.32", 16 | "cssnano": "4.1.10", 17 | "stylus": "0.54.8" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | preact | freshie 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/index.js: -------------------------------------------------------------------------------- 1 | import * as freshie from 'freshie/runtime'; 2 | import * as preact from '@freshie/ui.preact'; 3 | 4 | freshie.start({ 5 | base: '/', 6 | // target: document.body, 7 | hydrate: preact.hydrate, 8 | render: preact.render, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/routes/_layout.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import * as styles from './_layout.styl'; 3 | 4 | export default function Layout(props) { 5 | return ( 6 |
7 | {props.children} 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/routes/_layout.less: -------------------------------------------------------------------------------- 1 | .layout { 2 | border: 1px solid red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/routes/_layout.scss: -------------------------------------------------------------------------------- 1 | .layout { 2 | border: 1px solid red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/routes/_layout.styl: -------------------------------------------------------------------------------- 1 | .layout 2 | border 1px solid red 3 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/routes/blog/[id].jsx: -------------------------------------------------------------------------------- 1 | import { get } from 'freshie/http'; 2 | import { h, Fragment } from 'preact'; 3 | 4 | export async function preload(req) { 5 | let res = await get(`https://jsonplaceholder.typicode.com/posts/${req.params.id}`); 6 | return { article: res.data }; 7 | } 8 | 9 | export default function Article(props) { 10 | // {/* 11 | // {article.title} 12 | // */} 13 | return ( 14 | <> 15 |

{ props.article.title }

16 |
17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/routes/blog/index.jsx: -------------------------------------------------------------------------------- 1 | import { get } from 'freshie/http'; 2 | import { h, Fragment } from 'preact'; 3 | 4 | export async function preload() { 5 | let res = await get('https://jsonplaceholder.typicode.com/posts'); 6 | return { articles: res.data }; 7 | } 8 | 9 | export default function Blog(props) { 10 | // 11 | // Articles 12 | // 13 | 14 | return ( 15 | <> 16 |

All Articles

17 | 18 |
19 | 28 |
29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import * as styles from './index.styl'; 3 | 4 | export default function Home(props) { 5 | const name = props.name || 'world'; 6 | 7 | return ( 8 | <> 9 |

Hello {name}

10 | Blog 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/routes/index.less: -------------------------------------------------------------------------------- 1 | .title { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/routes/index.sass: -------------------------------------------------------------------------------- 1 | .title 2 | color: red 3 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/routes/index.styl: -------------------------------------------------------------------------------- 1 | .title 2 | color red 3 | -------------------------------------------------------------------------------- /examples/preact.esbuild/src/static/robots.txt: -------------------------------------------------------------------------------- 1 | robots.txt 2 | -------------------------------------------------------------------------------- /examples/preact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "freshie build" 5 | }, 6 | "dependencies": { 7 | "preact": "10.4.8" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "7.11.6", 11 | "@babel/plugin-transform-react-jsx": "7.10.4", 12 | "@freshie/plugin.babel": "workspace:*", 13 | "@freshie/ui.preact": "workspace:*", 14 | "freshie": "workspace:*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/preact/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | preact | freshie 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/preact/src/index.js: -------------------------------------------------------------------------------- 1 | import * as freshie from 'freshie/runtime'; 2 | import * as preact from '@freshie/ui.preact'; 3 | 4 | freshie.start({ 5 | base: '/', 6 | // target: document.body, 7 | hydrate: preact.hydrate, 8 | render: preact.render, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/preact/src/routes/_layout.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function Layout(props) { 4 | return ( 5 |
6 | { props.children } 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /examples/preact/src/routes/blog/[id].jsx: -------------------------------------------------------------------------------- 1 | import { get } from 'freshie/http'; 2 | import { h, Fragment } from 'preact'; 3 | 4 | // export async function preload(req) { 5 | // let res = await fetch(`https://jsonplaceholder.typicode.com/posts/${req.params.id}`); 6 | // let article = await res.json(); 7 | // return { article }; 8 | // } 9 | 10 | export async function preload(req) { 11 | let res = await get(`https://jsonplaceholder.typicode.com/posts/${req.params.id}`); 12 | return { article: res.data }; 13 | } 14 | 15 | export default function Article(props) { 16 | // {/* 17 | // {article.title} 18 | // */} 19 | return ( 20 | <> 21 |

{ props.article.title }

22 |
23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /examples/preact/src/routes/blog/index.jsx: -------------------------------------------------------------------------------- 1 | import { get } from 'freshie/http'; 2 | import { h, Fragment } from 'preact'; 3 | 4 | // export async function preload() { 5 | // let res = await fetch(`https://jsonplaceholder.typicode.com/posts`); 6 | // let articles = await res.json(); 7 | // return { articles }; 8 | // } 9 | 10 | export async function preload() { 11 | let res = await get('https://jsonplaceholder.typicode.com/posts'); 12 | return { articles: res.data }; 13 | } 14 | 15 | export default function Blog(props) { 16 | // 17 | // Articles 18 | // 19 | 20 | return ( 21 | <> 22 |

All Articles

23 | 24 |
25 |
    26 | { 27 | props.articles.map(obj => ( 28 |
  • 29 | {obj.title} 30 |
  • 31 | )) 32 | } 33 |
34 |
35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /examples/preact/src/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | 3 | export default function Home(props) { 4 | const name = props.name || 'world'; 5 | 6 | return ( 7 | <> 8 |

Hello {name}

9 | Blog 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/preact/src/static/robots.txt: -------------------------------------------------------------------------------- 1 | robots.txt 2 | -------------------------------------------------------------------------------- /examples/svelte.typescript/freshie.config.js: -------------------------------------------------------------------------------- 1 | // TODO? include inside ui.svelte 2 | exports.svelte = function (config) { 3 | // @ts-ignore - export default type 4 | config.preprocess = require('svelte-preprocess')() 5 | } 6 | -------------------------------------------------------------------------------- /examples/svelte.typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "freshie build", 5 | "watch": "freshie watch" 6 | }, 7 | "devDependencies": { 8 | "@freshie/plugin.postcss": "workspace:*", 9 | "@freshie/plugin.typescript": "workspace:*", 10 | "@freshie/ssr.node": "workspace:*", 11 | "@freshie/ui.svelte": "workspace:*", 12 | "freshie": "workspace:*", 13 | "postcss": "7.0.32", 14 | "svelte": "3.29.0", 15 | "svelte-preprocess": "4.3.1", 16 | "typescript": "4.0.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/svelte.typescript/src/errors/5xx.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 |

Error: {status}

14 |

this is a custom 5xx error page

15 | -------------------------------------------------------------------------------- /examples/svelte.typescript/src/errors/_layout.svelte: -------------------------------------------------------------------------------- 1 |
2 |

i am a layout

3 | 4 |
5 | 6 | 11 | -------------------------------------------------------------------------------- /examples/svelte.typescript/src/index.dom.ts: -------------------------------------------------------------------------------- 1 | import * as freshie from 'freshie/runtime'; 2 | import * as svelte from '@freshie/ui.svelte'; 3 | 4 | freshie.start({ 5 | hydrate: svelte.hydrate, 6 | render: svelte.render, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/svelte.typescript/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | svelte | freshie 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/svelte.typescript/src/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "freshie", 3 | "name": "freshie + svelte", 4 | "background_color": "#3E82F7", 5 | "orientation": "portrait", 6 | "theme_color": "#1e88e5", 7 | "display": "standalone", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "/icon.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | }, 15 | { 16 | "src": "/icon@2x.png", 17 | "type": "image/png", 18 | "sizes": "1024x1024" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/svelte.typescript/src/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /examples/svelte.typescript/src/routes/_layout.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 10 | -------------------------------------------------------------------------------- /examples/svelte.typescript/src/routes/blog/[id].svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 16 | {article.title} 17 | 18 | 19 |

{article.title}

20 | 21 |
22 | {@html article.body} 23 |
24 | -------------------------------------------------------------------------------- /examples/svelte.typescript/src/routes/blog/index.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | 17 | 18 | Articles 19 | 20 | 21 |

All Articles

22 | 23 |
24 |
    25 | {#each articles as article (article.id)} 26 |
  • {article.title}
  • 27 | {:else} 28 |
  • NO POSTS
  • 29 | {/each} 30 |
31 |
32 | -------------------------------------------------------------------------------- /examples/svelte.typescript/src/routes/index.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

Hello {name}

6 | 7 | Blog 8 | 9 | 14 | -------------------------------------------------------------------------------- /examples/svelte.typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "noImplicitAny": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "allowSyntheticDefaultImports": true, 8 | "moduleResolution": "node", 9 | "removeComments": true, 10 | "allowJs": true, 11 | "noEmit": true 12 | }, 13 | "include": [ 14 | "src" 15 | ], 16 | "exclude": [ 17 | "node_modules", 18 | "build" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /examples/svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "freshie build", 5 | "watch": "freshie watch" 6 | }, 7 | "devDependencies": { 8 | "@freshie/ssr.node": "workspace:*", 9 | "@freshie/ui.svelte": "workspace:*", 10 | "freshie": "workspace:*", 11 | "svelte": "3.29.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/svelte/src/errors/5xx.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 |

Error: {status}

12 |

this is a custom 5xx error page

13 | -------------------------------------------------------------------------------- /examples/svelte/src/errors/_layout.svelte: -------------------------------------------------------------------------------- 1 |
2 |

i am a layout

3 | 4 |
5 | 6 | 11 | -------------------------------------------------------------------------------- /examples/svelte/src/index.dom.js: -------------------------------------------------------------------------------- 1 | import * as freshie from 'freshie/runtime'; 2 | import * as svelte from '@freshie/ui.svelte'; 3 | 4 | freshie.start({ 5 | base: '/', 6 | // target: document.body, 7 | hydrate: svelte.hydrate, 8 | render: svelte.render, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/svelte/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | svelte | freshie 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/svelte/src/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "freshie", 3 | "name": "freshie + svelte", 4 | "background_color": "#3E82F7", 5 | "orientation": "portrait", 6 | "theme_color": "#1e88e5", 7 | "display": "standalone", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "/icon.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | }, 15 | { 16 | "src": "/icon@2x.png", 17 | "type": "image/png", 18 | "sizes": "1024x1024" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/svelte/src/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /examples/svelte/src/routes/_layout.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 10 | -------------------------------------------------------------------------------- /examples/svelte/src/routes/blog/[id].svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | 19 | 20 | {article.title} 21 | 22 | 23 |

{article.title}

24 | 25 |
26 | {@html article.body} 27 |
28 | -------------------------------------------------------------------------------- /examples/svelte/src/routes/blog/index.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | 19 | 20 | Articles 21 | 22 | 23 |

All Articles

24 | 25 |
26 |
    27 | {#each articles as article (article.id)} 28 |
  • {article.title}
  • 29 | {:else} 30 |
  • NO POSTS
  • 31 | {/each} 32 |
33 |
34 | -------------------------------------------------------------------------------- /examples/svelte/src/routes/index.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

Hello {name}

6 | 7 | Blog 8 | 9 | 14 | -------------------------------------------------------------------------------- /examples/vue/freshie.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: WIP -- no SSR attempts yet 3 | */ 4 | 5 | exports.rollup = function (config) { 6 | config.plugins.push( 7 | // https://rollup-plugin-vue.vuejs.org/options.html 8 | require('rollup-plugin-vue')({ 9 | css: false, 10 | }) 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "freshie build" 5 | }, 6 | "dependencies": { 7 | "vue": "2.6.12" 8 | }, 9 | "devDependencies": { 10 | "@freshie/ui.vue": "workspace:*", 11 | "rollup-plugin-vue": "5.1.9", 12 | "freshie": "workspace:*", 13 | "vue-template-compiler": "2.6.12" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/vue/src/index.js: -------------------------------------------------------------------------------- 1 | import * as freshie from 'freshie/runtime'; 2 | import * as vue from '@freshie/ui.vue'; 3 | 4 | freshie.start({ 5 | base: '/', 6 | target: document.body, 7 | hydrate: vue.hydrate, 8 | render: vue.render, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vue/src/routes/blog/[id].vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | -------------------------------------------------------------------------------- /examples/vue/src/routes/blog/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | -------------------------------------------------------------------------------- /examples/vue/src/routes/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Luke Edwards (https://lukeed.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "0.0.2", 4 | "repository": "lukeed/freshie", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "node build", 8 | "pretest": "tsc --noEmit --skipLibCheck", 9 | "test": "uvu -r ts-node/register packages __tests__ -i fixtures" 10 | }, 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "devDependencies": { 15 | "ts-node": "10.2.1", 16 | "typescript": "4.4.3", 17 | "uvu": "0.5.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/@freshie/plugin.babel/config.js: -------------------------------------------------------------------------------- 1 | const toArray = (x, y=[]) => x == null ? y : y.concat(x); 2 | 3 | exports.babel = function (config) { 4 | config.babelrc = false; 5 | config.babelHelpers = 'bundled'; 6 | config.presets = toArray(config.presets); 7 | config.exclude = toArray(config.exclude, ['node_modules/**']); 8 | config.plugins = toArray(config.plugins); 9 | } 10 | 11 | exports.rollup = function (config, context, options) { 12 | config.plugins.push( 13 | require('@rollup/plugin-babel').default(options.babel) 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/@freshie/plugin.babel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.3", 3 | "name": "@freshie/plugin.babel", 4 | "dependencies": { 5 | "@rollup/plugin-babel": "^5.2.0" 6 | }, 7 | "peerDependencies": { 8 | "@babel/core": "^7.0.0", 9 | "freshie": "*" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | } 14 | } -------------------------------------------------------------------------------- /packages/@freshie/plugin.babel/readme.md: -------------------------------------------------------------------------------- 1 | # @freshie/plugin.babel 2 | 3 | ## Install 4 | 5 | ```sh 6 | $ npm install --save-dev @babel/core @freshie/plugin.babel 7 | ``` 8 | -------------------------------------------------------------------------------- /packages/@freshie/plugin.esbuild/config.js: -------------------------------------------------------------------------------- 1 | exports.esbuild = function (config, context) { 2 | config.define = config.define || {}; 3 | config.include = /\.[jt]sx?$/; // default - TODO: options.ui.extensions ? 4 | config.target = context.ssr ? 'node12.18.0' : 'es2020'; 5 | } 6 | 7 | exports.rollup = function (config, context, options) { 8 | Object.assign(options.esbuild.define, options.replace); 9 | 10 | config.plugins.push( 11 | require('rollup-plugin-esbuild')({ 12 | ...options.esbuild, 13 | minify: context.minify, 14 | }) 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/@freshie/plugin.esbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.5", 3 | "name": "@freshie/plugin.esbuild", 4 | "dependencies": { 5 | "rollup-plugin-esbuild": "^2.5.2" 6 | }, 7 | "peerDependencies": { 8 | "esbuild": "^0.7.0", 9 | "freshie": "*" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | } 14 | } -------------------------------------------------------------------------------- /packages/@freshie/plugin.esbuild/readme.md: -------------------------------------------------------------------------------- 1 | # @freshie/plugin.esbuild 2 | 3 | ## Install 4 | 5 | ```sh 6 | $ npm install --save-dev esbuild @freshie/plugin.esbuild 7 | ``` 8 | -------------------------------------------------------------------------------- /packages/@freshie/plugin.postcss/assets.js: -------------------------------------------------------------------------------- 1 | const { copyFileSync, existsSync, readFileSync, mkdirSync } = require('fs'); 2 | const { dirname, isAbsolute, parse, resolve } = require('path'); 3 | const { createHash } = require('crypto'); 4 | const postcss = require('postcss'); 5 | 6 | const RGX = /(url\(\s*['"]?)([^"')]+)(["']?\s*\))/g; 7 | 8 | function toHash(filename, source) { 9 | const hmac = createHash('shake256', { outputLength: 16 }); 10 | return hmac.update(source).digest('hex'); 11 | } 12 | 13 | function noop() { 14 | // 15 | } 16 | 17 | // TODO: Rework for postcss 8.x 18 | module.exports = postcss.plugin('freshie/postcss.assets', (opts={}) => { 19 | const { hash, filters, write, target } = opts; 20 | 21 | let toWrite; 22 | let mkdir = false; 23 | const Cache = new Map; 24 | 25 | if (write) { 26 | toWrite = write; 27 | mkdir = true; 28 | } else { 29 | // TODO: throw error no target? 30 | const dir = resolve('.', target); 31 | 32 | toWrite = (input, output) => { 33 | if (!mkdir) { 34 | mkdirSync(dir, { recursive: true }); 35 | mkdir = true; 36 | } 37 | copyFileSync(input, resolve(dir, output)); 38 | } 39 | } 40 | 41 | const toHasher = hash || toHash; 42 | const toFilter = typeof filters === 'function' ? filters : noop; 43 | 44 | return function (styles) { 45 | const file = styles.source.input.file; 46 | 47 | styles.walkDecls(decl => { 48 | if (!RGX.test(decl.value)) return; 49 | 50 | decl.value = decl.value.replace(RGX, (full, open, inner, close) => { 51 | let tmp = Cache.get(inner); 52 | if (tmp) return tmp; 53 | 54 | if (inner.startsWith('data:')) { 55 | return full; // url(data:...) 56 | } 57 | 58 | tmp = toFilter(inner, file, decl.value); 59 | 60 | if (tmp && RGX.test(tmp)) { 61 | Cache.set(inner, tmp); 62 | return tmp; // url(...) 63 | } 64 | 65 | tmp = tmp || inner; 66 | 67 | if (tmp.charAt(0) === '/') { 68 | tmp = open + tmp + close; 69 | Cache.set(inner, tmp); 70 | return tmp; // url(/...) 71 | } 72 | 73 | if (/^(https?:)?\/\//.test(tmp)) { 74 | tmp = open + tmp + close; 75 | Cache.set(inner, tmp); 76 | return tmp; // url(http://) 77 | } 78 | 79 | if (!isAbsolute(tmp)) { 80 | tmp = resolve(dirname(file), tmp); 81 | } 82 | 83 | let xyz = Cache.get(tmp); 84 | if (xyz) return xyz; 85 | 86 | if (!existsSync(tmp)) { 87 | throw new Error(`Asset file does not exist: "${tmp}"`); 88 | } 89 | 90 | let info = parse(tmp); 91 | let outfile = toHasher(tmp, readFileSync(tmp)) + info.ext; 92 | 93 | toWrite(tmp, outfile); 94 | 95 | tmp = open + '/' + outfile + close; 96 | Cache.set('/' + outfile, tmp); 97 | Cache.set(inner, tmp); 98 | return tmp; // url(/{hash}.{ext}) 99 | }); 100 | 101 | return decl; 102 | }); 103 | } 104 | }); 105 | -------------------------------------------------------------------------------- /packages/@freshie/plugin.postcss/config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | function installed(name) { 4 | try { return require.resolve(name) } 5 | catch (err) { return false } 6 | } 7 | 8 | exports.stylus = { 9 | // 10 | } 11 | 12 | exports.sass = { 13 | // 14 | } 15 | 16 | exports.cssnano = { 17 | // 18 | } 19 | 20 | exports.postcss = function (config, context) { 21 | const { sourcemap, isProd } = context; 22 | 23 | config.plugins = [].concat(config.plugins || []); 24 | config.sourcemap = sourcemap ? { inline: true } : false; 25 | 26 | config.modules = config.modules || {}; 27 | config.modules.scopeBehaviour = 'local'; 28 | config.modules.generateScopedName = '[name]__[local]___[hash:base64:5]'; 29 | 30 | if (isProd) { 31 | config.modules.generateScopedName = '[hash:base64:5]'; 32 | if (installed('autoprefixer')) { 33 | config.plugins.push(require('autoprefixer')()); 34 | } 35 | } 36 | } 37 | 38 | // TODO: no-op writes/output when `ssr` context 39 | // Will be using DOM output links anyway; ssr useless 40 | exports.rollup = function (config, context, options) { 41 | const { isProd, minify } = context; 42 | const entries = options.alias.entries; 43 | 44 | if (isProd && minify && installed('cssnano')) { 45 | options.postcss.plugins.push( 46 | require('cssnano')(options.cssnano) 47 | ); 48 | } 49 | 50 | options.postcss.server = context.ssr; 51 | 52 | // route-based CSS chunking/code-splitting 53 | options.postcss.extract = function (filename) { 54 | const relative = filename.replace(entries['~routes'], ''); 55 | const match = /[\\\/+]?([^\\\/+]*)/i.exec(relative); 56 | if (!match) return 'index.css'; // commons 57 | 58 | let name = match[1].toLowerCase(); 59 | if (!name.endsWith('.css')) name += '.css'; 60 | return (name.startsWith('_') ? '' : 'r.') + name; 61 | }; 62 | 63 | // Apply `~assets` alias to url() contents 64 | //=> support "~assets", "~@assets", or "@assets" 65 | options.postcss.assets = function (value) { 66 | if (/(\~?@|\~)assets[\\\/]+?/.test(value)) { 67 | let tmp = value.replace(/(\~?@|\~)assets[\\\/]+?/, ''); 68 | return join(entries['~assets'], tmp); 69 | } 70 | }; 71 | 72 | options.postcss.sass = { ...options.sass, ...options.postcss.sass }; 73 | options.postcss.stylus = { ...options.stylus, ...options.postcss.stylus }; 74 | options.postcss.less = { ...options.less, ...options.postcss.less }; 75 | 76 | config.plugins.push( 77 | require('.')(options.postcss), 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /packages/@freshie/plugin.postcss/index.js: -------------------------------------------------------------------------------- 1 | const { basename } = require('path'); 2 | const { existsSync, readFile, readFileSync } = require('fs'); 3 | const { promisify } = require('util'); 4 | const postcss = require('postcss'); 5 | 6 | const read = promisify(readFile); 7 | let render_stylus, render_less, render_sass; 8 | 9 | function load(name) { 10 | try { return require(name) } 11 | catch (e) { throw new Error(`\nPlease install the "${name}" package:\n $ npm install --save-dev ${name}`) } 12 | } 13 | 14 | function stylus(filename, filedata, sourcemap, options={}) { 15 | if (!render_stylus) render_stylus = load('stylus'); 16 | 17 | options.filename = filename; 18 | if (sourcemap) options.sourcemap={ comment: false }; 19 | 20 | let ctx = render_stylus(filedata, options); 21 | 22 | return new Promise((res, rej) => { 23 | ctx.render((err, css) => { 24 | let map = sourcemap && ctx.sourcemap; 25 | return err ? rej(err) : res({ css, map }); 26 | }); 27 | }); 28 | } 29 | 30 | function sass(filename, filedata, sourcemap, options={}) { 31 | if (!render_sass) render_sass = load('node-sass').render; 32 | 33 | const indentedSyntax = /\.sass$/.test(filename); 34 | options.data = filedata; 35 | options.file = filename; 36 | 37 | if (sourcemap) { 38 | options.outFile = filename.replace(/\.s[ac]ss$/, '.css'); 39 | options.sourceMap = true; 40 | } 41 | 42 | return new Promise((res, rej) => { 43 | render_sass({ ...options, indentedSyntax }, (err, result) => { 44 | return err ? rej(err) : res({ 45 | css: result.css.toString(), 46 | map: result.map && result.map.toString() 47 | }); 48 | }); 49 | }); 50 | } 51 | 52 | function less(filename, filedata, sourcemap, options={}) { 53 | if (!render_less) { 54 | render_less = load('less').render; 55 | } 56 | 57 | options.filename = filename; 58 | if (sourcemap) options.sourceMap={}; 59 | return render_less(filedata, options); 60 | } 61 | 62 | module.exports = function (opts={}) { 63 | const { plugins=[], assets, extract, sourcemap, server, ...rest } = opts; 64 | 65 | let toExtract = false; 66 | const FILES = new Map, REFS = new Map; 67 | if (typeof extract === 'string') toExtract = () => extract; 68 | else if (typeof extract === 'function') toExtract = extract; 69 | else if (extract === true) toExtract = () => 'bundle.css'; 70 | 71 | const toMap = sourcemap != null && !!sourcemap; 72 | const RUNTIME = require.resolve('./runtime.js'); 73 | const IDENT = '!!~freshie.postcss.runtime~!!'; 74 | 75 | return { 76 | name: 'freshie/postcss', 77 | 78 | resolveId(id) { 79 | return id === IDENT ? RUNTIME : null; 80 | }, 81 | 82 | load(id) { 83 | if (!/\.(css|s[ac]ss|less|styl(us)?)$/.test(id)) return null; 84 | return existsSync(id) && read(id, 'utf8') || null; 85 | }, 86 | 87 | async transform(source, id) { 88 | let css, file, tmp, map; 89 | if (/\.css$/.test(id)) { 90 | css = source; 91 | file = id; 92 | } else if (/\.styl(us)?$/.test(id)) { 93 | tmp = await stylus(id, source, toMap, rest.stylus); 94 | file = id.replace(/\.styl(us)?$/, '.css'); 95 | css=tmp.css; map=tmp.map; 96 | } else if (/\.less$/.test(id)) { 97 | tmp = await less(id, source, toMap, rest.less); 98 | file = id.replace(/\.less$/, '.css'); 99 | css=tmp.css; map=tmp.map; 100 | } else if (/\.s[ac]ss$/.test(id)) { 101 | tmp = await sass(id, source, toMap, rest.sass); 102 | file = id.replace(/\.s[ac]ss$/, '.css'); 103 | css=tmp.css; map=tmp.map; 104 | } else { 105 | return null; 106 | } 107 | 108 | const toWrite = (file, data) => { 109 | this.emitFile({ 110 | type: 'asset', 111 | fileName: file, 112 | source: data, 113 | }); 114 | }; 115 | 116 | const copy = [ 117 | ...plugins, 118 | require('./assets')({ 119 | filters: assets, 120 | write(input, output) { 121 | let content = readFileSync(input); 122 | return toWrite(output, content); 123 | } 124 | }), 125 | ].filter(Boolean); 126 | 127 | let Manifest = {}; 128 | if (rest.modules) { 129 | copy.push( 130 | require('postcss-modules')({ 131 | ...Object(rest.modules), 132 | getJSON(file, mapping) { 133 | // TODO: config.modules.getJSON proxy 134 | Manifest = mapping; 135 | } 136 | }) 137 | ); 138 | } 139 | 140 | if (sourcemap && map) { 141 | map = { ...sourcemap, prev: map }; 142 | } else if (!sourcemap) { 143 | map = false; 144 | } 145 | 146 | const output = await postcss(copy).process(css, { 147 | ...rest, map, from: file, 148 | }); 149 | 150 | if (toExtract) { 151 | file = toExtract(file); 152 | } 153 | 154 | // TODO: handle external sourcemap: `if (sourcemap && output.map)` 155 | // NOTE: `sourcemap.inline` already handled 156 | // console.log('FINAL OUTPUT', output.map); 157 | 158 | const content = (FILES.get(file) || '') + output.css; 159 | FILES.set(file, content); // full asset source 160 | 161 | let loader = ''; 162 | 163 | if (!server) { 164 | const ref = REFS.get(file) || this.emitFile({ 165 | type: 'asset', 166 | // sets `source` later 167 | name: basename(file), 168 | }); 169 | 170 | REFS.set(file, ref); 171 | 172 | loader += ` 173 | import { link } from "${IDENT}"; 174 | link(import.meta.ROLLUP_FILE_URL_${ref}); 175 | `; 176 | } 177 | 178 | for (let key in Manifest) { 179 | loader += `\nexport const ${key} = ${JSON.stringify(Manifest[key])};`; 180 | } 181 | 182 | return { 183 | code: loader.replace(/^\s+/gm, ''), 184 | // moduleSideEffects: true, 185 | }; 186 | }, 187 | 188 | renderStart() { 189 | if (server) return; 190 | for (let [file, ref] of REFS) { 191 | let content = FILES.get(file); 192 | this.setAssetSource(ref, content); 193 | FILES.delete(file); REFS.delete(file); 194 | } 195 | } 196 | }; 197 | } 198 | -------------------------------------------------------------------------------- /packages/@freshie/plugin.postcss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.6", 3 | "name": "@freshie/plugin.postcss", 4 | "dependencies": { 5 | "postcss-modules": "^3.2.0" 6 | }, 7 | "peerDependencies": { 8 | "autoprefixer": "^9.8.0", 9 | "cssnano": "^4.1.10", 10 | "freshie": "*", 11 | "less": "^3.12.0", 12 | "node-sass": "^4.14.0", 13 | "postcss": "^7.0.32", 14 | "stylus": "^0.54.0" 15 | }, 16 | "peerDependenciesMeta": { 17 | "autoprefixer": { 18 | "optional": true 19 | }, 20 | "cssnano": { 21 | "optional": true 22 | }, 23 | "less": { 24 | "optional": true 25 | }, 26 | "node-sass": { 27 | "optional": true 28 | }, 29 | "stylus": { 30 | "optional": true 31 | } 32 | }, 33 | "publishConfig": { 34 | "access": "public" 35 | } 36 | } -------------------------------------------------------------------------------- /packages/@freshie/plugin.postcss/readme.md: -------------------------------------------------------------------------------- 1 | # @freshie/plugin.postcss 2 | 3 | ## Install 4 | 5 | ```sh 6 | $ npm install --save-dev postcss @freshie/plugin.postcss 7 | ``` 8 | 9 | ***Optional*** 10 | 11 | > Additional modules that, if installed, `@freshie/plugin.postcss` will attach. 12 | 13 | ```sh 14 | $ npm install --save-dev autoprefixer cssnano 15 | ``` 16 | 17 | ## Preprocessors 18 | 19 | > Note: Only `stylus` is supported right now. 20 | 21 | Bring your desired preprocessor flavor, if any~! 22 | 23 | Simply install the preprocessor itself; for example: 24 | 25 | ```sh 26 | $ npm install --save-dev stylus 27 | ``` 28 | 29 | In most cases, that's all you need to do – `@freshie/plugin.postcss` will invoke the preprocessor when its extension(s) is/are detected. However, should you need to send the preprocessor some configuration, modify its configuration key within your `freshie.config.js` file; for example: 30 | 31 | ```js 32 | // freshie.config.js 33 | exports.stylus = { 34 | // custom options 35 | } 36 | ``` 37 | 38 | ***Direct Usage*** 39 | 40 | If you are using this plugin directly (AKA, you **are not** using freshie), you may define preprocessor configuration via plugin options: 41 | 42 | ```js 43 | // rollup.config.js 44 | import Postcss from '@freshie/plugin.postcss'; 45 | 46 | export default { 47 | // ... 48 | plugins: [ 49 | Postcss({ 50 | stylus: { 51 | // custom options 52 | } 53 | }) 54 | ] 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /packages/@freshie/plugin.postcss/runtime.js: -------------------------------------------------------------------------------- 1 | var cache = []; 2 | 3 | export function link(href, tmp) { 4 | if (!cache.length) { 5 | for (var i=0, arr=document.styleSheets; tmp = arr[i]; i++) { 6 | if (cache.push(tmp.href) && tmp.href === href) return; 7 | } 8 | } 9 | (tmp = document.createElement('link')).rel='stylesheet'; 10 | document.head.appendChild(tmp); 11 | cache.push(tmp.href = href); 12 | } 13 | -------------------------------------------------------------------------------- /packages/@freshie/plugin.typescript/config.js: -------------------------------------------------------------------------------- 1 | exports.typescript = function (config, context) { 2 | const override = config.tsconfigOverride || {}; 3 | override.compilerOptions = override.compilerOptions || {}; 4 | override.compilerOptions.watch = !context.isProd; 5 | config.tsconfigOverride = override; 6 | } 7 | 8 | exports.rollup = function (config, context, options) { 9 | config.plugins.push( 10 | require('rollup-plugin-typescript2')(options.typescript) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/@freshie/plugin.typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.3", 3 | "name": "@freshie/plugin.typescript", 4 | "dependencies": { 5 | "rollup-plugin-typescript2": "^0.28.0" 6 | }, 7 | "peerDependencies": { 8 | "typescript": ">=3.9.0", 9 | "freshie": "*" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | } 14 | } -------------------------------------------------------------------------------- /packages/@freshie/plugin.typescript/readme.md: -------------------------------------------------------------------------------- 1 | # @freshie/plugin.typescript 2 | 3 | ## Install 4 | 5 | ```sh 6 | $ npm install --save-dev typescript @freshie/plugin.typescript 7 | ``` 8 | -------------------------------------------------------------------------------- /packages/@freshie/ssr.node/config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | exports.ssr = function (config) { 4 | config.type = 'node'; 5 | // set fallback entry point 6 | config.entry = join(__dirname, 'entry.js'); 7 | } 8 | 9 | exports.rollup = function (config, context) { 10 | if (!context.ssr) return; 11 | config.output.format = 'cjs'; 12 | config.output.esModule = false; 13 | config.external = [ 14 | ...(config.external || []), 15 | ...require('module').builtinModules, 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /packages/@freshie/ssr.node/entry.js: -------------------------------------------------------------------------------- 1 | import { ssr } from '!!~ui~!!'; // alias 2 | import { start } from './index'; 3 | 4 | const { PORT=3000 } = process.env; 5 | 6 | start({ render: ssr, port: PORT }); 7 | -------------------------------------------------------------------------------- /packages/@freshie/ssr.node/index.d.ts: -------------------------------------------------------------------------------- 1 | export function setup(options: TODO): TODO; 2 | export function middleware(options: TODO): TODO; 3 | export function start(options: TODO): TODO; 4 | -------------------------------------------------------------------------------- /packages/@freshie/ssr.node/index.js: -------------------------------------------------------------------------------- 1 | import sirv from 'sirv'; 2 | import { join } from 'path'; 3 | import { resolve } from 'url'; 4 | import parse from '@polka/url'; 5 | import regexparam from 'regexparam'; 6 | import { createServer } from 'http'; 7 | import { HTML } from '!!~html~!!'; 8 | 9 | const Tree = new Map; 10 | const ERRORS = { /* */ }; 11 | 12 | function prepare(Tags, extra={}) { 13 | let i=0, tmp, loaders=[], views=[]; 14 | for (; i < Tags.length; i++) { 15 | tmp = Tags[i]; 16 | views.push(tmp.default); 17 | if (tmp.preload) loaders.push(tmp.preload); 18 | } 19 | return { ...extra, loaders, views }; 20 | } 21 | 22 | // NOTE: ideally `layout` here 23 | function define(route, ...Tags) { 24 | let { keys, pattern } = regexparam(route); 25 | let entry = prepare(Tags, { keys }); 26 | Tree.set(pattern, entry); 27 | } 28 | 29 | function toError(status, message='') { 30 | const error = new Error(message); 31 | error.status = status; 32 | throw error; 33 | } 34 | 35 | function find(pathname) { 36 | let rgx, data, match; 37 | let j=0, arr, params={}; 38 | for ([rgx, data] of Tree) { 39 | arr = data.keys; 40 | if (arr.length > 0) { 41 | match = pathname.match(rgx); 42 | if (match === null) continue; 43 | for (j=0; j < arr.length;) params[arr[j]]=match[++j]; 44 | return { params, loaders:data.loaders, views:data.views }; 45 | } else if (rgx.test(pathname)) { 46 | return { params, loaders:data.loaders, views:data.views }; 47 | } 48 | } 49 | } 50 | 51 | export function setup() { 52 | /* */ 53 | return Tree; 54 | } 55 | 56 | // TODO: file server (sirv) 57 | // TODO: tie `sirv` existence to `options.ssr.*` thing 58 | export function start(options={}) { 59 | const { decode, port, render } = options; 60 | setup(); //=> attach app routes 61 | 62 | const assets = true && sirv( 63 | join(__dirname, '..', 'client'), 64 | { dev: __DEV__ } // all from options.ssr? 65 | ); 66 | 67 | // TODO: req.url vs req.href disparity 68 | async function draw(req, route, context) { 69 | let props = { url: req.href }; 70 | 71 | if (route.loaders.length > 0) { 72 | await Promise.all( 73 | route.loaders.map(p => p(req, context)) 74 | ).then(list => { 75 | // TODO? deep merge props 76 | Object.assign(props, ...list); 77 | }); 78 | } 79 | 80 | return render(route.views, props); 81 | } 82 | 83 | return createServer(async (req, res) => { 84 | let page={}, request=parse(req, decode); 85 | let route, isAsset, context={ status: 0 }; 86 | context.headers = { 'Content-Type': 'text/html;charset=utf-8' }; 87 | 88 | request.query = request.query || {}; 89 | request.headers = req.headers; 90 | request.params = {}; 91 | 92 | try { 93 | if (req.method !== 'GET' && req.method !== 'HEAD') { 94 | return toError(405); 95 | } 96 | 97 | route = find(request.pathname); 98 | if (!route && !assets) return toError(404); 99 | if (isAsset = !route) return assets(req, res, () => { 100 | return (isAsset=false,toError(404)); 101 | }); 102 | 103 | request.params = route.params; 104 | page = await draw(request, route, context); 105 | } catch (err) { 106 | context.error = err; 107 | context.status = context.status || err.statusCode || err.status || 500; 108 | // look up error by specificity 109 | const key = String(context.status); 110 | const route = ERRORS[key] || ERRORS[key[0] + 'xx'] || ERRORS['xxx'] 111 | page = await draw(request, route, context); 112 | } finally { 113 | if (isAsset) return; // handled 114 | if (context.redirect) { 115 | context.headers.location = resolve(request.href, context.redirect); 116 | context.status = (context.status > 300 && context.status < 400) ? context.status : 302; 117 | } 118 | // props.head=head; props.body=body; 119 | // TODO: static HTML vs HTML component file 120 | res.writeHead(context.status || 200, context.headers); 121 | let output = HTML.replace(/<\/body>/, page.body + ''); 122 | res.end(page.head ? output.replace(/<\/head>/, page.head + '') : output); 123 | } 124 | }).listen(port); 125 | } 126 | 127 | // TODO: allow file server option ?? 128 | export function middleware(options={}) { 129 | const { decode } = options; 130 | setup(); //=> attach app routes 131 | return function (req, res, next) { 132 | let info, route, method=req.method; 133 | if (method !== 'GET' && method !== 'HEAD') { 134 | return next(); // user handles it 135 | } 136 | 137 | info = parse(req, decode); 138 | route = find(info.pathname); 139 | if (!route) return next(); // user 140 | 141 | // ... 142 | }; 143 | } 144 | -------------------------------------------------------------------------------- /packages/@freshie/ssr.node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.5", 3 | "name": "@freshie/ssr.node", 4 | "dependencies": { 5 | "@polka/url": "next", 6 | "regexparam": "^1.3.0", 7 | "sirv": "^1.0.6" 8 | }, 9 | "peerDependencies": { 10 | "freshie": "*" 11 | }, 12 | "publishConfig": { 13 | "access": "public" 14 | } 15 | } -------------------------------------------------------------------------------- /packages/@freshie/ssr.worker/config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | exports.ssr = function (config) { 4 | config.type = 'worker'; 5 | // set fallback entry point 6 | config.entry = join(__dirname, 'entry.js'); 7 | } 8 | 9 | exports.resolve = function (config, context) { 10 | if (!context.ssr) return; 11 | // redefine defaults if somehow missing 12 | config.mainFields = config.mainFields || ['module', 'jsnext', 'jsnext:main', 'main']; 13 | config.mainFields.unshift('worker'); // prefer "worker" entry if found 14 | } 15 | 16 | exports.rollup = function (config, context) { 17 | if (!context.ssr) return; 18 | config.output.format = 'esm'; 19 | config.output.esModule = false; 20 | config.output.sourcemap = false; 21 | context.sourcemap = false; 22 | } 23 | -------------------------------------------------------------------------------- /packages/@freshie/ssr.worker/entry.js: -------------------------------------------------------------------------------- 1 | import { ssr } from '!!~ui~!!'; // alias 2 | import * as App from './index'; 3 | 4 | const handler = App.setup({ render: ssr }); 5 | addEventListener('fetch', handler); 6 | -------------------------------------------------------------------------------- /packages/@freshie/ssr.worker/index.js: -------------------------------------------------------------------------------- 1 | import regexparam from 'regexparam'; 2 | import { HTML } from '!!~html~!!'; 3 | 4 | export const TREE = {}; 5 | export const Cache = caches.default; 6 | const ERRORS = { /* */ }; 7 | 8 | var render, decode=true; 9 | export function setup(options={}) { 10 | decode = !!options.decode; 11 | render = options.render; 12 | /* */ 13 | return function (event) { 14 | event.respondWith( 15 | run(event) 16 | ); 17 | } 18 | } 19 | 20 | function prepare(Tags) { 21 | let i=0, tmp, loaders=[], views=[]; 22 | for (; i < Tags.length; i++) { 23 | tmp = Tags[i]; 24 | views.push(tmp.default); 25 | if (tmp.preload) loaders.push(tmp.preload); 26 | } 27 | return { loaders, views }; 28 | } 29 | 30 | // NOTE: ideally `layout` here 31 | export function define(route, ...Tags) { 32 | let { views, loaders } = prepare(Tags); 33 | add('GET', route, views, loaders); 34 | } 35 | 36 | export function toCache(event, res) { 37 | event.waitUntil(Cache.put(event.request, res.clone())); 38 | return res; 39 | } 40 | 41 | export function isCachable(res) { 42 | if (res.status === 206) return false; 43 | 44 | const vary = res.headers.get('Vary') || ''; 45 | if (vary.includes('*')) return false; 46 | 47 | const ccontrol = res.headers.get('Cache-Control') || ''; 48 | if (/(private|no-cache|no-store)/i.test(ccontrol)) return false; 49 | 50 | if (res.headers.has('Set-Cookie')) { 51 | res.headers.append('Cache-Control', 'private=Set-Cookie'); 52 | } 53 | 54 | return true; 55 | } 56 | 57 | // todo: add anything 58 | export function add(method, route, views, loaders=[]) { 59 | if (TREE[method] === void 0) { 60 | TREE[method] = { __roots__: new Map }; 61 | } 62 | 63 | if (/[:|*]/.test(route)) { 64 | const { keys, pattern } = regexparam(route); 65 | TREE[method].__roots__.set(pattern, { keys, views, loaders }); 66 | } else { 67 | TREE[method][route] = { keys:[], views, loaders }; 68 | } 69 | } 70 | 71 | export function toBody(request, ctype) { 72 | if (ctype.includes('application/json')) return request.json(); 73 | if (ctype.includes('application/text')) return request.text(); 74 | if (ctype.includes('form')) return request.formData().then(toObj); 75 | return /text\/*/i.test(ctype) ? request.text() : request.blob(); 76 | } 77 | 78 | export function find(method, pathname) { 79 | let dict = TREE[method]; 80 | let tmp = dict[pathname]; 81 | let match, params={}; 82 | 83 | if (tmp !== void 0) { 84 | return { params, views: tmp.views, loaders: tmp.loaders }; 85 | } 86 | 87 | for (const [rgx, val] of dict.__roots__) { 88 | match = rgx.exec(pathname); 89 | if (match === null) continue; 90 | 91 | if (val.keys.length > 0) { 92 | for (tmp=0; tmp < val.keys.length;) { 93 | params[val.keys[tmp++]] = match[tmp]; 94 | } 95 | } 96 | 97 | return { params, views: val.views, loaders: val.loaders }; 98 | } 99 | } 100 | 101 | function toError(status, message='') { 102 | const error = new Error(message); 103 | error.status = status; 104 | throw error; 105 | } 106 | 107 | async function draw(req, route, context) { 108 | let props = { url: req.url }; 109 | 110 | if (route.loaders.length > 0) { 111 | await Promise.all( 112 | route.loaders.map(p => p(req, context)) 113 | ).then(list => { 114 | // TODO? deep merge props 115 | Object.assign(props, ...list); 116 | }); 117 | } 118 | 119 | return render(route.views, props); 120 | } 121 | 122 | export async function run(event) { 123 | const { request } = event; 124 | const { url, method, headers } = request; 125 | 126 | const isGET = /^(GET|HEAD)$/.test(method); 127 | const { pathname, search, searchParams } = new URL(url); 128 | const query = search ? Object.fromEntries(searchParams) : {}; 129 | const path = decode ? decodeURIComponent(pathname) : pathname; 130 | const req = { url, method, headers, path, query, search, params:{}, body:null }; 131 | 132 | let page={}, context={ status: 0 }; 133 | context.headers = { 'Content-Type': 'text/html;charset=utf-8' }; 134 | 135 | try { 136 | // TODO: detach if has custom 137 | if (!isGET) return toError(405); 138 | 139 | const route = find(method, path); 140 | if (!route) return toError(404); 141 | 142 | // TODO: only if has custom 143 | if (false && request.body) { 144 | try { 145 | const ctype = headers.get('content-type'); 146 | if (ctype) req.body = await toBody(request, ctype); 147 | } catch (err) { 148 | err.status = 400; 149 | throw err; 150 | } 151 | } 152 | 153 | req.params = route.params; 154 | page = await draw(req, route, context); 155 | } catch (err) { 156 | context.error = err; 157 | context.status = context.status || err.statusCode || err.status || 500; 158 | // look up error by specificity 159 | const key = String(context.status); 160 | const route = ERRORS[key] || ERRORS[key[0] + 'xx'] || ERRORS['xxx'] 161 | page = await draw(req, route, context); 162 | } finally { 163 | if (context.redirect) { 164 | let { status, redirect } = context; 165 | const { href } = new URL(redirect, url); 166 | if (status <= 300 || status >= 400) status = 302; 167 | return Response.redirect(href, status); 168 | } 169 | 170 | // props.head=head; props.body=body; 171 | // TODO: static HTML vs HTML component file 172 | let output = HTML.replace(/<\/body>/, page.body + ''); 173 | if (page.head) output = output.replace(/<\/head>/, page.head + ''); 174 | 175 | const res = new Response(output, { 176 | status: context.status || 200, 177 | headers: context.headers, 178 | }); 179 | 180 | return isGET && isCachable(res) ? toCache(event, res) : res; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /packages/@freshie/ssr.worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.5", 3 | "name": "@freshie/ssr.worker", 4 | "dependencies": { 5 | "regexparam": "^1.3.0" 6 | }, 7 | "peerDependencies": { 8 | "freshie": "*" 9 | }, 10 | "publishConfig": { 11 | "access": "public" 12 | } 13 | } -------------------------------------------------------------------------------- /packages/@freshie/ui.preact/_error.jsx: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | 3 | export function preload(req, context) { 4 | console.log('_error.jsx got:', req, context); 5 | return { status: context.status } 6 | } 7 | 8 | const messages = { 9 | '400': 'Bad Request', 10 | '404': 'Page Not Found', 11 | '429': 'Too Many Requests', 12 | '500': 'Unknown Error', 13 | } 14 | 15 | export default function Error(props) { 16 | const { status=500 } = props; 17 | 18 | return ( 19 | <> 20 |

Error ({status})

21 |

{ messages[status] }

22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/@freshie/ui.preact/config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | exports.alias = function (config) { 4 | config.entries['!!~error~!!'] = join(__dirname, '_error.jsx'); 5 | } 6 | 7 | exports.templates = function (config) { 8 | config.test = /\.[tj]sx?$/; 9 | } 10 | 11 | exports.babel = function (config) { 12 | config.plugins = config.plugins || []; 13 | 14 | config.plugins.push( 15 | ['@babel/plugin-transform-react-jsx', { 16 | pragmaFrag: 'Fragment', 17 | pragma: 'h', 18 | }] 19 | ); 20 | } 21 | 22 | // TODO: options.ui.extension (for loaders/include) 23 | exports.esbuild = function (config) { 24 | config.jsxFragment = 'Fragment'; 25 | config.jsxFactory = 'h'; 26 | } 27 | -------------------------------------------------------------------------------- /packages/@freshie/ui.preact/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@freshie/ui.preact' { 2 | import type { Props } from 'freshie'; 3 | import type { ComponentChild, ComponentChildren } from 'preact'; 4 | 5 | export { Props }; 6 | 7 | export function render(Tags: ComponentChildren, props: Props, target: HTMLElement): void; 8 | export function hydrate(Tags: ComponentChildren, props: Props, target: HTMLElement): void; 9 | export function layout(Tags: ComponentChildren, props?: Props): ComponentChild; 10 | 11 | export function ssr(Tags: ComponentChildren, props?: Props): Record<'head'|'body', string>; 12 | } 13 | -------------------------------------------------------------------------------- /packages/@freshie/ui.preact/index.js: -------------------------------------------------------------------------------- 1 | import * as preact from 'preact'; 2 | 3 | export function render(Tags, props, target) { 4 | preact.render(layout(Tags, props), target); 5 | } 6 | 7 | export function hydrate(Tags, props, target) { 8 | preact.hydrate(layout(Tags, props), target); 9 | } 10 | 11 | export function layout(Tags, props={}) { 12 | let len=Tags.length, vnode; 13 | while (len-- > 0) vnode = preact.h(Tags[len], props, vnode); 14 | return vnode; 15 | } 16 | 17 | // --- 18 | 19 | import toHTML from 'preact-render-to-string'; 20 | 21 | export function ssr(Tags, props={}) { 22 | let vnode = layout(Tags, props); 23 | let body = toHTML(vnode) || ''; 24 | return { head: '', body }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/@freshie/ui.preact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.6", 3 | "name": "@freshie/ui.preact", 4 | "types": "index.d.ts", 5 | "dependencies": { 6 | "preact-render-to-string": "^5.1.10" 7 | }, 8 | "peerDependencies": { 9 | "@babel/plugin-transform-react-jsx": "^7.10.0", 10 | "@freshie/plugin.babel": "*", 11 | "freshie": "*", 12 | "preact": "^10.0.0" 13 | }, 14 | "peerDependenciesMeta": { 15 | "@babel/plugin-transform-react-jsx": { 16 | "optional": true 17 | }, 18 | "@freshie/plugin.babel": { 19 | "optional": true 20 | } 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | } 25 | } -------------------------------------------------------------------------------- /packages/@freshie/ui.preact/readme.md: -------------------------------------------------------------------------------- 1 | # @freshie/ui.preact 2 | 3 | ## Install 4 | 5 | ```sh 6 | $ npm install --save-dev preact @freshie/ui.preact 7 | ``` 8 | 9 | ***With Babel*** 10 | 11 | > When compiling with Babel, this plugin requires these packages: 12 | 13 | ```sh 14 | $ npm install --save-dev @babel/plugin-transform-react-jsx @freshie/plugin.babel 15 | ``` 16 | -------------------------------------------------------------------------------- /packages/@freshie/ui.svelte/_error.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 |

Error ({status})

20 |

{ messages[status] }

21 | -------------------------------------------------------------------------------- /packages/@freshie/ui.svelte/config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | exports.alias = function (config) { 4 | config.entries['!!~error~!!'] = join(__dirname, '_error.svelte'); 5 | } 6 | 7 | exports.templates = function (config) { 8 | config.test = /\.svelte$/; 9 | } 10 | 11 | exports.postcss = function (config) { 12 | config.modules = false; 13 | } 14 | 15 | exports.svelte = { 16 | emitCss: true, 17 | hydratable: true, 18 | // TODO: 7.0 19 | // compilerOptions: { 20 | // hydratable: true, 21 | // } 22 | } 23 | 24 | // TODO: load CWD/svelte.config.js values 25 | exports.rollup = function (config, context, options) { 26 | /* 27 | TODO: 7.0 28 | const { compilerOptions } = options.svelte; 29 | 30 | compilerOptions.hydratable = true; 31 | compilerOptions.generate = context.ssr ? 'ssr' : 'dom'; 32 | 33 | options.svelte.compilerOptions = compilerOptions; 34 | options.svelte.emitCss = true; 35 | */ 36 | 37 | options.svelte.generate = context.ssr ? 'ssr' : 'dom'; 38 | 39 | config.plugins.push( 40 | require('rollup-plugin-svelte')(options.svelte) 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /packages/@freshie/ui.svelte/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@freshie/ui.svelte' { 2 | import type { Props } from 'freshie'; 3 | import type { SvelteComponent } from 'svelte'; 4 | 5 | export { Props }; 6 | 7 | export function render(Tags: SvelteComponent[], props: Props, target: HTMLElement): void; 8 | export function hydrate(Tags: SvelteComponent[], props: Props, target: HTMLElement): void; 9 | export function layout(Tags: SvelteComponent[], props?: Props): SvelteComponent; 10 | 11 | export function ssr(Tags: SvelteComponent[], props?: Props): Record<'head'|'body', string>; 12 | } 13 | -------------------------------------------------------------------------------- /packages/@freshie/ui.svelte/index.js: -------------------------------------------------------------------------------- 1 | var App; 2 | 3 | export function render(Tags, props, target) { 4 | if (App) App.$destroy(); 5 | var Tag = layout(Tags, props); 6 | App = Tag({ props, target }); 7 | } 8 | 9 | export function hydrate(Tags, props, target) { 10 | var Tag = layout(Tags, props); 11 | App = Tag({ props, target, hydrate: true }); 12 | } 13 | 14 | // --- 15 | 16 | // --- 17 | 18 | import { detach, insert, noop, SvelteComponent } from 'svelte/internal'; 19 | 20 | function slotty(elem) { 21 | return function () { 22 | var t, frag={}; 23 | if (elem instanceof SvelteComponent) t = elem; 24 | else if (typeof elem === 'function') t = new elem({ $$inline: true }); 25 | frag = t && t.$$ && t.$$.fragment || frag; 26 | 27 | frag.c = frag.c || noop; 28 | frag.l = frag.l || noop; 29 | 30 | frag.m = frag.m || function (target, anchor) { 31 | insert(target, elem, anchor); 32 | } 33 | 34 | frag.d = frag.d || function (detaching) { 35 | if (detaching) detach(t || elem); 36 | }; 37 | 38 | return frag; 39 | }; 40 | } 41 | 42 | function bury(props, prev) { 43 | return { 44 | ...props, 45 | $$scope: {}, 46 | $$slots: { 47 | default: [ slotty(prev) ] 48 | } 49 | }; 50 | } 51 | 52 | export function layout(Tags, props={}) { 53 | var len=Tags.length, last, Tag=Tags[0]; 54 | while (len-- > 1) { 55 | last = new Tags[len]({ 56 | props: last ? bury(props, last) : props, 57 | $$inline: true, 58 | }); 59 | } 60 | return function (opts={}) { 61 | if (last && opts.props) { 62 | opts.props = bury(props, last); 63 | } 64 | return new Tag(opts); 65 | } 66 | } 67 | 68 | // --- 69 | 70 | import { create_ssr_component } from 'svelte/internal'; 71 | 72 | export function ssr(Tags, props={}) { 73 | let head='', body='', len=Tags.length; 74 | 75 | if (len > 0) { 76 | let ssr = create_ssr_component(($$out, $$props, $$bindings) => { 77 | let slots={}; 78 | while (len-- > 1) { 79 | let tmp = Tags[len]; 80 | // NOTE: you can ONLY use in layouts 81 | slots = { default: () => tmp.$$render($$out, $$props, $$bindings, slots) }; 82 | } 83 | return Tags[0].$$render($$out, $$props, $$bindings, slots); 84 | }).render(props); 85 | 86 | body += ssr.html; 87 | head += ssr.head || ''; 88 | if (ssr.css && ssr.css.code) { 89 | head += ``; 90 | } 91 | } 92 | 93 | return { head, body }; 94 | } 95 | -------------------------------------------------------------------------------- /packages/@freshie/ui.svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.6", 3 | "name": "@freshie/ui.svelte", 4 | "types": "index.d.ts", 5 | "dependencies": { 6 | "rollup-plugin-svelte": "^6.1.1" 7 | }, 8 | "peerDependencies": { 9 | "freshie": "*", 10 | "svelte": "^3.25.0" 11 | }, 12 | "publishConfig": { 13 | "access": "public" 14 | } 15 | } -------------------------------------------------------------------------------- /packages/@freshie/ui.svelte/readme.md: -------------------------------------------------------------------------------- 1 | # @freshie/ui.svelte 2 | 3 | ## Install 4 | 5 | ```sh 6 | $ npm install --save-dev svelte @freshie/ui.svelte 7 | ``` 8 | -------------------------------------------------------------------------------- /packages/@freshie/ui.vue/config.js: -------------------------------------------------------------------------------- 1 | exports.templates = function (config) { 2 | config.test = /\.vue$/; 3 | } 4 | -------------------------------------------------------------------------------- /packages/@freshie/ui.vue/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | var root, Route; 4 | export function render(Tag, props, target) { 5 | if (root) { 6 | root.$destroy(); 7 | target.removeChild(root.$el); 8 | } 9 | 10 | Route = Vue.extend(Tag); 11 | root = new Route({ propsData: props }).$mount(); 12 | target.appendChild(root.$el); 13 | } 14 | 15 | // TODO: $mount(target, true) 16 | export function hydrate(Tag, props, target) { 17 | Route = Vue.extend(Tag); 18 | root = new Route({ propsData: props }).$mount(); 19 | target.appendChild(root.$el); 20 | } 21 | -------------------------------------------------------------------------------- /packages/@freshie/ui.vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "0.0.1", 4 | "name": "@freshie/ui.vue", 5 | "peerDependencies": { 6 | "freshie": "*", 7 | "vue": "^2.6.0" 8 | }, 9 | "publishConfig": { 10 | "access": "public" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/freshie/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | type Nullable = T | null; 2 | type Arrayable = T[] | T; 3 | type Promisable = Promise | T; 4 | 5 | type Dict = Record; 6 | type Subset = T & Dict; 7 | 8 | type TODO = any; 9 | 10 | // --- 11 | 12 | declare namespace Argv { 13 | interface Options { 14 | cwd: string; 15 | minify: boolean; 16 | sourcemap: boolean; 17 | ssr: boolean; 18 | // 19 | src: string; 20 | dest: string; 21 | srcDir: string; 22 | destDir: string; 23 | // 24 | isProd: boolean; 25 | } 26 | } 27 | 28 | declare namespace Config { 29 | type Rollup = Rollup.Config; 30 | 31 | interface Options extends Dict { 32 | publicPath: string; 33 | 34 | alias: Subset<{ 35 | entries: Subset<{ 36 | '~routes': string; 37 | '~components': string; 38 | '~assets': string; 39 | '~utils': string; 40 | '~tags': string; 41 | }, string>; 42 | }>; 43 | 44 | ssr: { 45 | type: 'node' | 'worker' | 'lambda'; 46 | entry: Nullable; // default entry 47 | // bucket?: string; 48 | }; 49 | 50 | templates: { 51 | test: RegExp; 52 | routes: string; 53 | errors: string; 54 | layout: RegExp; 55 | }; 56 | 57 | copy: string[]; 58 | 59 | assets: { 60 | dir: string; 61 | test: RegExp; 62 | }; 63 | 64 | replace: Subset<{ 65 | '__DEV__': string; 66 | '__BROWSER__': string; 67 | 'process.browser': string; 68 | 'process.env.NODE_ENV': string; 69 | }, string>; 70 | 71 | resolve: Subset<{ 72 | extensions: string[]; 73 | mainFields: string[]; 74 | }>; 75 | 76 | commonjs: Subset<{ 77 | extensions: string[]; 78 | }>; 79 | 80 | json: Subset<{ 81 | preferConst: boolean; 82 | namedExports: boolean; 83 | indent: string; 84 | }>; 85 | 86 | terser: import('terser').MinifyOptions; 87 | } 88 | 89 | interface Context { 90 | cwd: string; 91 | ssr: boolean; 92 | isProd: boolean; 93 | sourcemap: boolean; 94 | minify: boolean; 95 | src: string; 96 | } 97 | 98 | interface Group { 99 | client: Rollup; 100 | options: Options; 101 | server: Rollup | void; 102 | } 103 | 104 | namespace Customize { 105 | type Rollup = (config: Config.Rollup, context: Config.Context, options: Config.Options) => void; 106 | type Options = { 107 | [K in keyof Config.Options]: (config: Config.Options[K], context: Config.Context) => void; 108 | }; 109 | } 110 | } 111 | 112 | declare namespace Build { 113 | interface Route { 114 | file: string; 115 | pattern: string; 116 | type: number; 117 | layout: Nullable; 118 | wild: Nullable; 119 | } 120 | 121 | interface Error { 122 | key: string; 123 | file: string; 124 | layout: Nullable; 125 | } 126 | 127 | interface Entries { 128 | dom: string; 129 | html: string; 130 | ssr: string; 131 | } 132 | } 133 | 134 | declare namespace Runtime { 135 | type Params = Dict; 136 | 137 | type Headers = Dict; 138 | 139 | interface Request { 140 | params: Params; 141 | pathname: string; 142 | search: string; 143 | query: Dict; 144 | } 145 | 146 | interface Context { 147 | status: number; 148 | headers: Headers; 149 | redirect?: string; 150 | error?: Error; 151 | } 152 | 153 | interface Options { 154 | basePath: string; 155 | render>(Component: C, props: P): void; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /packages/freshie/@types/rollup.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Rollup { 2 | type Plugin = import('rollup').Plugin; 3 | type Asset = import('rollup').OutputAsset; 4 | type Chunk = import('rollup').OutputChunk; 5 | type Bundle = import('rollup').RollupBuild; 6 | type Output = import('rollup').RollupOutput; 7 | type Watcher = import('rollup').RollupWatcher; 8 | type Config = Partial & { output: import('rollup').OutputOptions }; 9 | type Resolve = Partial & { mainFields: string[] }; 10 | type Options = Record & { resolve: Resolve }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/freshie/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('sade'); 3 | const commands = require('./build'); 4 | const { version } = require('./package'); 5 | 6 | sade('freshie') 7 | .version(version) 8 | .option('-C, --cwd', 'The relative working directory', '.') 9 | 10 | .command('build [src]') 11 | .describe('Compile the Worker(s) within a directory.') 12 | .option('-x, --sourcemap', 'Generate sourcemap(s)') 13 | .option('-m, --minify', 'Minify built assets', true) 14 | .example('build --sourcemap --no-minify') 15 | .action(commands.build) 16 | 17 | .command('watch [src]') 18 | .describe('Compile the Worker(s) within a directory.') 19 | .alias('dev').action(commands.watch) 20 | 21 | .parse(process.argv); 22 | -------------------------------------------------------------------------------- /packages/freshie/env/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'freshie/env' { 2 | export const DEV: boolean; 3 | export const BROWSER: boolean; 4 | export const SSR: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /packages/freshie/env/index.js: -------------------------------------------------------------------------------- 1 | exports.DEV = __DEV__; 2 | exports.BROWSER = __BROWSER__; 3 | exports.SSR = __SSR__; 4 | -------------------------------------------------------------------------------- /packages/freshie/env/index.mjs: -------------------------------------------------------------------------------- 1 | export const DEV = __DEV__; 2 | export const BROWSER = __BROWSER__; 3 | export const SSR = __SSR__; 4 | -------------------------------------------------------------------------------- /packages/freshie/http/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'freshie/http' { 2 | export * from 'httpie'; 3 | } 4 | -------------------------------------------------------------------------------- /packages/freshie/http/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('httpie'); 2 | -------------------------------------------------------------------------------- /packages/freshie/http/index.mjs: -------------------------------------------------------------------------------- 1 | export * from 'httpie'; 2 | -------------------------------------------------------------------------------- /packages/freshie/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'freshie' { 2 | type Nullable = T | null; 3 | type Arrayable = T[] | T; 4 | type Promisable = Promise | T; 5 | 6 | type Dict = Record; 7 | type Subset = T & Dict; 8 | 9 | // --- 10 | 11 | export namespace Config { 12 | type Rollup = Partial & { 13 | output: import('rollup').OutputOptions 14 | }; 15 | 16 | interface Options extends Dict { 17 | publicPath: string; 18 | 19 | alias: Subset<{ 20 | entries: Subset<{ 21 | '~routes': string; 22 | '~components': string; 23 | '~assets': string; 24 | '~utils': string; 25 | '~tags': string; 26 | }, string>; 27 | }>; 28 | 29 | ssr: { 30 | type: 'node' | 'worker' | 'lambda'; 31 | entry: Nullable; // default entry 32 | // bucket?: string; 33 | }; 34 | 35 | templates: { 36 | test: RegExp; 37 | routes: string; 38 | errors: string; 39 | layout: RegExp; 40 | }; 41 | 42 | copy: string[]; 43 | 44 | assets: { 45 | dir: string; 46 | test: RegExp; 47 | }; 48 | 49 | replace: Subset<{ 50 | '__DEV__': string; 51 | '__BROWSER__': string; 52 | 'process.browser': string; 53 | 'process.env.NODE_ENV': string; 54 | }, string>; 55 | 56 | resolve: Subset<{ 57 | extensions: string[]; 58 | mainFields: string[]; 59 | }>; 60 | 61 | commonjs: Subset<{ 62 | extensions: string[]; 63 | }>; 64 | 65 | json: Subset<{ 66 | preferConst: boolean; 67 | namedExports: boolean; 68 | indent: string; 69 | }>; 70 | 71 | terser: Subset<{ 72 | mangle: boolean; 73 | compress: boolean; 74 | output: Dict; 75 | }>; 76 | } 77 | 78 | interface Context { 79 | ssr: boolean; 80 | isProd: boolean; 81 | minify: boolean; 82 | } 83 | 84 | interface Group { 85 | client: Rollup; 86 | options: Options; 87 | server: Rollup | void; 88 | } 89 | 90 | namespace Customize { 91 | type Rollup = (config: Config.Rollup, context: Config.Context, options: Config.Options) => void; 92 | type Options = { 93 | [K in keyof Config.Options]: (config: Config.Options[K], context: Config.Context) => void; 94 | }; 95 | } 96 | } 97 | 98 | export type Props = Dict; 99 | export type Params = Dict; 100 | export type Headers = Dict; 101 | 102 | export interface Request { 103 | params: Params; 104 | pathname: string; 105 | search: string; 106 | query: Dict; 107 | } 108 | 109 | export interface Context { 110 | status: number; 111 | headers: Headers; 112 | redirect?: string; 113 | error?: Error; 114 | } 115 | 116 | export type Preload = (req: Request, context: Context) => Promisable; 117 | 118 | export interface Options { 119 | basePath: string; 120 | render>(Component: C, props: P): void; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /packages/freshie/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freshie", 3 | "version": "0.0.8", 4 | "repository": "lukeed/freshie", 5 | "license": "MIT", 6 | "types": "index.d.ts", 7 | "bin": { 8 | "freshie": "bin.js" 9 | }, 10 | "exports": { 11 | "./package.json": "./package.json", 12 | "./runtime": "./runtime", 13 | "./router": { 14 | "import": "./router/index.mjs", 15 | "require": "./router/index.js" 16 | }, 17 | "./http": { 18 | "import": "./http/index.mjs", 19 | "require": "./http/index.js" 20 | }, 21 | "./env": { 22 | "import": "./env/index.mjs", 23 | "require": "./env/index.js" 24 | } 25 | }, 26 | "files": [ 27 | "*.d.ts", 28 | "*.js", 29 | "build", 30 | "runtime", 31 | "router", 32 | "http", 33 | "env" 34 | ], 35 | "dependencies": { 36 | "@freshie/ssr.node": "workspace:*", 37 | "@rollup/plugin-alias": "^3.1.0", 38 | "@rollup/plugin-commonjs": "^15.0.0", 39 | "@rollup/plugin-json": "^4.1.0", 40 | "@rollup/plugin-node-resolve": "^9.0.0", 41 | "@rollup/plugin-replace": "^2.3.0", 42 | "escalade": "^3.1.0", 43 | "httpie": "^2.0.0-next.11", 44 | "kleur": "^4.1.0", 45 | "klona": "^2.0.0", 46 | "navaid": "^1.2.0", 47 | "node-html-parser": "^1.3.1", 48 | "premove": "^3.0.0", 49 | "rollup": "2.27.1", 50 | "rollup-route-manifest": "^1.0.0", 51 | "route-sort": "^1.0.0", 52 | "sade": "^1.7.0", 53 | "terser": "^5.6.0", 54 | "totalist": "^2.0.0" 55 | }, 56 | "engines": { 57 | "node": ">=10" 58 | }, 59 | "peerDependencies": { 60 | "rollup": "^2.27.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/freshie/router/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'freshie/router' { 2 | export * from 'navaid'; 3 | } 4 | -------------------------------------------------------------------------------- /packages/freshie/router/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('navaid'); 2 | -------------------------------------------------------------------------------- /packages/freshie/router/index.mjs: -------------------------------------------------------------------------------- 1 | export { default } from 'navaid'; 2 | -------------------------------------------------------------------------------- /packages/freshie/runtime/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'freshie/router'; 2 | import type { Props } from 'freshie'; 3 | 4 | export const router: Router; 5 | 6 | declare namespace DOM { 7 | export type Render = (Tags: T[], props: Props, target: HTMLElement) => void; 8 | } 9 | 10 | export interface Options { 11 | /** 12 | * The function that renders into the DOM container. 13 | */ 14 | render: DOM.Render; 15 | /** 16 | * The hydration-specific code, if different than `options.render`. 17 | * @default options.render 18 | */ 19 | hydrate?: DOM.Render; 20 | /** 21 | * The HTML element to render into. 22 | * @default document.body 23 | */ 24 | target?: HTMLElement; 25 | /** 26 | * The application's base URL path. 27 | * @default "/" 28 | */ 29 | base?: string; 30 | } 31 | 32 | export function start(options?: Options): void; 33 | -------------------------------------------------------------------------------- /packages/freshie/runtime/index.dom.js: -------------------------------------------------------------------------------- 1 | import Router from 'freshie/router'; 2 | 3 | export var router; 4 | var target, render, hydrate; 5 | var ERRORS = { /* */ }; 6 | 7 | // var hasSW = ('serviceWorker' in navigator); 8 | // var root = document.body; 9 | 10 | function request(params) { 11 | var { pathname, search } = location; 12 | var USP = new URLSearchParams(search); 13 | var query = Object.fromEntries(USP); 14 | return { pathname, search, query, params }; 15 | } 16 | 17 | function run(Tags, params, ctx, req) { 18 | ctx = ctx || {}; 19 | params = params || {}; 20 | var draw = hydrate || render; 21 | var i=0, loaders=[], views=[]; 22 | var props = { params }; 23 | 24 | for (; i < Tags.length; i++) { 25 | views.push(Tags[i].default); 26 | if (Tags[i].preload) loaders.push(Tags[i].preload); 27 | } 28 | 29 | if (loaders.length) { 30 | req = req || request(params); 31 | Promise.all( 32 | loaders.map(f => f(req, ctx)) 33 | ).then(list => { 34 | if (ctx.redirect) { 35 | // TODO? Could `new URL` and origin match 36 | // but wouldn't guarantee same `base` path 37 | if (/^(https?:)?\/\//.test(ctx.redirect)) { 38 | location.href = ctx.redirect; // meh 39 | } else { 40 | router.route(ctx.redirect, true); 41 | } 42 | } else { 43 | Object.assign(props, ...list); 44 | draw(views, props, target); 45 | hydrate = false; 46 | } 47 | }).catch(err => { 48 | ctx.error = err; 49 | ErrorPage(params, ctx); 50 | }); 51 | } else { 52 | draw(views, props, target); 53 | hydrate = false; 54 | } 55 | } 56 | 57 | function toError(code) { 58 | var key = String(code); 59 | return ERRORS[key] || ERRORS[key[0] + 'xx'] || ERRORS['xxx']; 60 | } 61 | 62 | function ErrorPage(params, ctx) { 63 | var err = ctx.error || {}; 64 | ctx.status = ctx.status || err.statusCode || err.status || 500; 65 | toError(ctx.status)().then(arr => run(arr, params, ctx)); 66 | } 67 | 68 | // TODO: accept multiple layouts 69 | // TODO: attach manifest/files loader 70 | function define(pattern, importer) { 71 | // let files = []; 72 | let toFiles = Promise.resolve(); 73 | 74 | // if (!hasSW && window.__rmanifest) { 75 | // if (files = window.__rmanifest[pattern]) { 76 | // // console.log('~> files', pattern, files); 77 | // toFiles = Promise.all(files.map(preload)); 78 | // } 79 | // } 80 | 81 | router.on(pattern, (params) => { 82 | var ctx = {}; 83 | 84 | Promise.all([ 85 | importer(), //=> Components 86 | toFiles, //=> Assets 87 | ]).then(arr => { 88 | run(arr[0], params, ctx); 89 | }).catch(err => { 90 | ctx.error = err; 91 | ErrorPage(params, ctx); 92 | }) 93 | }); 94 | } 95 | 96 | function is404(url) { 97 | ErrorPage({ url }, { status: 404 }); 98 | } 99 | 100 | export function start(options) { 101 | options = options || {}; 102 | 103 | render = options.render; 104 | hydrate = options.hydrate || render; 105 | // TODO: options.target 106 | target = document.body; 107 | 108 | router = Router(options.base || '/', is404); 109 | /* */ 110 | 111 | // INIT 112 | if (document.readyState !== 'loading') router.listen(); 113 | else addEventListener('DOMContentLoaded', router.listen); 114 | } 115 | -------------------------------------------------------------------------------- /packages/freshie/src/commands/build.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from '../utils/argv'; 2 | import * as log from '../utils/log'; 3 | import * as fs from '../utils/fs'; 4 | import { load } from '../config'; 5 | 6 | async function compile(rollup: typeof import('rollup').rollup, config: Config.Rollup): Promise { 7 | return rollup(config).then(b => b.write(config.output)); 8 | } 9 | 10 | export default async function (src: Nullable, argv: Partial) { 11 | try { 12 | normalize(src, argv, { isProd: true }); 13 | 14 | const config = await load(argv as Argv.Options).catch(log.bail); 15 | 16 | if (fs.exists(argv.dest)) { 17 | log.warn(`Removing "${ log.$dir(argv.destDir) }" directory`); 18 | await fs.remove(argv.dest); 19 | } 20 | 21 | const { rollup } = require('rollup'); 22 | 23 | // TODO: Add Manifest | HTML to Client 24 | await compile(rollup, config.client); 25 | if (config.server) await compile(rollup, config.server); 26 | log.success('Build complete! 🎉'); 27 | } catch (err) { 28 | log.bail(err); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/freshie/src/commands/watch/index.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from '../../utils/argv'; 2 | import * as log from '../../utils/log'; 3 | import * as fs from '../../utils/fs'; 4 | import { load } from '../../config'; 5 | import Watcher from './watcher'; 6 | 7 | export default async function (src: Nullable, argv: Partial) { 8 | normalize(src, argv, { isProd: false }); 9 | 10 | const config = await load(argv as Argv.Options).catch(log.bail); 11 | 12 | if (fs.exists(argv.dest)) { 13 | log.warn(`Removing "${ log.$dir(argv.destDir) }" directory`); 14 | await fs.remove(argv.dest); 15 | } 16 | 17 | Watcher(config.client, argv as Argv.Options); 18 | } 19 | -------------------------------------------------------------------------------- /packages/freshie/src/commands/watch/watcher.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | // import HTML from './html'; 3 | 4 | /** 5 | * Create a Watcher from base config 6 | * @param config {import('rollup').RollupWatchOptions} 7 | * @returns {import('rollup').RollupWatcher} 8 | */ 9 | export default function (config: Rollup.Config, argv: Argv.Options): Rollup.Watcher { 10 | const { src, dest } = argv; 11 | // const { onUpdate, onError } = TODO; 12 | // const hasMap = !!config.output.sourcemap; 13 | 14 | // dev-only plugins 15 | // config.plugins.push( 16 | // HTML({ src }) 17 | // ); 18 | 19 | // Initialize Watcher 20 | // Attach logging/event listeners 21 | const watcher: Rollup.Watcher = require('rollup').watch(config); 22 | 23 | let UPDATES: string[] = []; 24 | let CHANGED: Set = new Set; 25 | 26 | watcher.on('change', file => { 27 | if (file.startsWith(src)) { 28 | CHANGED.add('/' + relative(src, file)); 29 | } else console.error('[CHANGE] NOT WITHIN SOURCE: "%s"', file) 30 | }); 31 | 32 | watcher.on('event', evt => { 33 | console.log(evt); 34 | 35 | switch (evt.code) { 36 | case 'START': { 37 | UPDATES = [...CHANGED]; 38 | CHANGED.clear(); 39 | break; 40 | } 41 | 42 | case 'BUNDLE_END': { 43 | // TODO: prettify 44 | console.info(`Bundled in ${evt.duration}ms`); 45 | // if (onUpdate && UPDATES.length) onUpdate(UPDATES); 46 | break; 47 | } 48 | 49 | case 'ERROR': { 50 | console.error('ERROR', evt.error); 51 | break; 52 | } 53 | } 54 | }); 55 | 56 | return watcher; 57 | } 58 | -------------------------------------------------------------------------------- /packages/freshie/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import { klona } from 'klona'; 2 | import { join, resolve } from 'path'; 3 | import * as scoped from '../utils/scoped'; 4 | import * as utils from '../utils/index'; 5 | import * as log from '../utils/log'; 6 | import { defaults } from './options'; 7 | import * as Plugin from './plugins'; 8 | 9 | import type { Asset } from 'rollup-route-manifest'; 10 | 11 | type ConfigData = Partial & { 12 | rollup?: Config.Customize.Rollup 13 | }; 14 | 15 | interface ConfigPair { 16 | options: Config.Options; 17 | context: Config.Context; 18 | } 19 | 20 | // modified pwa/core util 21 | export function merge(old: Config.Options, nxt: ConfigData, context: Config.Context) { 22 | for (let k in nxt) { 23 | if (k === 'rollup') continue; 24 | if (typeof nxt[k] === 'function') { 25 | old[k] = old[k] || {}; 26 | let val = nxt[k](old[k], context); 27 | if (typeof old[k] === 'string' && typeof val === 'string') { 28 | old[k] = val; // for publicPath (only) 29 | } 30 | } else { 31 | old[k] = nxt[k] || old[k]; 32 | } 33 | } 34 | } 35 | 36 | function assemble(configs: ConfigData[], argv: Argv.Options, ssr = false): ConfigPair { 37 | const options = klona(defaults); 38 | const { src, minify, isProd, cwd, sourcemap } = argv; 39 | const context: Config.Context = { ssr, minify, isProd, sourcemap, src, cwd }; 40 | configs.forEach(tmp => merge(options, tmp, context)); 41 | 42 | const aliases = options.alias.entries; 43 | 44 | // update special aliases 45 | aliases['~assets'] = options.assets.dir; 46 | aliases['~routes'] = options.templates.routes; 47 | 48 | // resolve aliases 49 | for (let key in aliases) { 50 | let tmp = aliases[key]; 51 | aliases[key] = resolve(src, tmp); 52 | } 53 | 54 | // resolve copy list (from src dir) 55 | options.copy = options.copy.map(dir => resolve(src, dir)); 56 | 57 | // update *shared* replacements 58 | options.replace.__DEV__ = String(!isProd); 59 | options.replace['process.env.NODE_ENV'] = JSON.stringify(isProd ? 'production' : 'development'); 60 | 61 | return { options, context }; 62 | } 63 | 64 | // TODO: save `merge` functions and apply twice (ssr vs dom) 65 | export async function load(argv: Argv.Options): Promise { 66 | const { cwd, src, isProd } = argv; 67 | 68 | const file = utils.load('freshie.config.js', cwd); 69 | 70 | const configs: ConfigData[] = []; 71 | const customize: Config.Customize.Rollup[] = []; 72 | let DOM: ConfigPair, SSR: ConfigPair, uikit: string; 73 | 74 | function autoload(name: string) { 75 | log.info(`Applying ${ log.$pkg(name) } preset`); 76 | let abs = utils.from(cwd, join(name, 'config.js')); 77 | let { rollup, ...rest } = require(abs) as ConfigData; 78 | if (/[/]ui\./.test(name)) uikit = uikit || name; 79 | if (rollup) customize.push(rollup); 80 | configs.push(rest); 81 | } 82 | 83 | // auto-load @freshie packages 84 | scoped.list(cwd).forEach(autoload); 85 | 86 | if (file) { 87 | log.info(`Applying "${ log.$dir('freshie.config.js') }" config`); 88 | let { rollup, ...rest } = file; 89 | if (rollup) customize.push(rollup); 90 | configs.push(rest); 91 | } 92 | 93 | // build base/client options 94 | DOM = assemble(configs, argv); 95 | const { options } = DOM; //=> "base" 96 | 97 | // find/parse "routes" directory 98 | const routes = await utils.routes(src, options.templates); 99 | if (!routes.length) throw new Error('No routes found!'); 100 | 101 | // find/parse "errors" directory 102 | // TODO: global default, regardless of uikit? 103 | const errors = await utils.errors(src, options.templates); 104 | if (uikit && !errors.find(x => x.key === 'xxx')) errors.push({ 105 | file: options.alias.entries['!!~error~!!'], 106 | layout: null, 107 | key: 'xxx', 108 | }); 109 | 110 | // auto-detect entry points, w/ SSR fallback 111 | const entries = await utils.entries(src, options); 112 | 113 | // build DOM configuration 114 | const client = Client(argv, routes, entries, errors, DOM.options, DOM.context); 115 | 116 | let server: Rollup.Config; 117 | 118 | // force node for dev 119 | if (argv.ssr && !isProd) { 120 | options.ssr.type = 'node'; 121 | } else if (argv.ssr && !options.ssr.type) { 122 | autoload('@freshie/ssr.node'); 123 | argv.ssr = true; // forced 124 | } else if (!argv.ssr) { 125 | options.ssr.type = null; // --no-ssr 126 | } 127 | 128 | if (argv.ssr) { 129 | // build server options w/ context 130 | SSR = assemble(configs, argv, true); 131 | 132 | if (!SSR.options.ssr.type) { 133 | SSR.options.ssr = options.ssr; 134 | } 135 | 136 | if (uikit) { 137 | SSR.options.alias.entries['!!~ui~!!'] = utils.from(cwd, uikit); 138 | } // else error? 139 | 140 | // Create SSR bundle config 141 | server = Server(argv, routes, entries, errors, SSR.options, SSR.context); 142 | } 143 | 144 | customize.forEach(mutate => { 145 | mutate(client, DOM.context, DOM.options); 146 | if (server) mutate(server, SSR.context, SSR.options); 147 | }); 148 | 149 | // Summaries must be last 150 | client.plugins.push(Plugin.Summary({ isDOM: true })); 151 | if (server) server.plugins.push(Plugin.Summary({ isDOM: false })); 152 | 153 | return { options, client, server }; 154 | } 155 | 156 | export function Client(argv: Argv.Options, routes: Build.Route[], entries: Build.Entries, errors: Build.Error[], options: Config.Options, context: Config.Context): Rollup.Config { 157 | const { src, isProd, minify, sourcemap } = context; 158 | 159 | return { 160 | input: entries.dom, 161 | output: { 162 | sourcemap: !!sourcemap, 163 | dir: join(argv.dest, 'client'), 164 | minifyInternalExports: isProd, 165 | entryFileNames: isProd ? '[name].[hash].js' : '[name].js', 166 | assetFileNames: isProd ? '[name].[hash].[ext]' : '[name].[ext]', 167 | chunkFileNames: isProd ? '[name].[hash].js' : '[name].js', 168 | }, 169 | preserveEntrySignatures: isProd ? false : 'strict', 170 | treeshake: isProd && { 171 | moduleSideEffects: 'no-external', 172 | tryCatchDeoptimization: false 173 | }, 174 | plugins: [ 175 | Plugin.ENV, 176 | Plugin.HTTP, 177 | Plugin.Router, 178 | Plugin.Copy(options.copy), 179 | Plugin.HTML(entries.html, { ...options, minify }), 180 | Plugin.Runtime(src, routes, errors, true), 181 | require('@rollup/plugin-alias')(options.alias), 182 | // Assets.Plugin, 183 | require('@rollup/plugin-replace')({ 184 | ...options.replace, 185 | '__BROWSER__': 'true', 186 | 'process.browser': 'true', 187 | '__SSR__': 'false', 188 | }), 189 | require('@rollup/plugin-node-resolve').default({ 190 | browser: true, 191 | ...options.resolve, 192 | rootDir: src 193 | }), 194 | require('@rollup/plugin-json')({ 195 | compact: isProd, 196 | ...options.json 197 | }), 198 | // for CLIENT runtime 199 | require('rollup-route-manifest')({ 200 | merge: true, 201 | inline: true, 202 | headers: false, 203 | filename: false, 204 | routes(file: string) { 205 | if (file === entries.dom) return '*'; 206 | for (let i=0; i < routes.length; i++) { 207 | if (routes[i].file === file) return routes[i].pattern; 208 | } 209 | }, 210 | format(files: Asset[]) { 211 | return files.map(x => x.href); 212 | } 213 | }), 214 | require('@rollup/plugin-commonjs')(options.commonjs), 215 | minify && Plugin.Terser(options.terser) 216 | ] 217 | }; 218 | } 219 | 220 | export function Server(argv: Argv.Options, routes: Build.Route[], entries: Build.Entries, errors: Build.Error[], options: Config.Options, context: Config.Context): Rollup.Config { 221 | const { src, isProd, minify, sourcemap } = context; 222 | 223 | const template = join(argv.dest, 'client', 'index.html'); 224 | 225 | return { 226 | input: entries.ssr || options.ssr.entry || join(src, 'index.ssr.js'), 227 | output: { 228 | file: join(argv.dest, 'server', 'index.js'), 229 | minifyInternalExports: isProd, 230 | sourcemap: !!sourcemap, 231 | }, 232 | treeshake: { 233 | propertyReadSideEffects: false, 234 | moduleSideEffects: 'no-external', 235 | tryCatchDeoptimization: false 236 | }, 237 | plugins: [ 238 | Plugin.ENV, 239 | Plugin.HTTP, 240 | Plugin.Template(template), 241 | Plugin.Runtime(src, routes, errors, false), 242 | require('@rollup/plugin-alias')(options.alias), 243 | // Assets.Plugin, 244 | require('@rollup/plugin-replace')({ 245 | ...options.replace, 246 | '__BROWSER__': 'false', 247 | 'process.browser': 'false', 248 | '__SSR__': 'true', 249 | }), 250 | require('@rollup/plugin-node-resolve').default({ 251 | browser: false, 252 | ...options.resolve, 253 | rootDir: src, 254 | }), 255 | require('@rollup/plugin-json')({ 256 | compact: isProd, 257 | ...options.json 258 | }), 259 | require('@rollup/plugin-commonjs')(options.commonjs), 260 | minify && Plugin.Terser(options.terser) 261 | ] 262 | }; 263 | } 264 | -------------------------------------------------------------------------------- /packages/freshie/src/config/options.ts: -------------------------------------------------------------------------------- 1 | export const defaults: Config.Options = { 2 | publicPath: '/', 3 | 4 | alias: { 5 | entries: { 6 | // src resolve 7 | '~routes': '', // injected 8 | '~components': 'components', 9 | '~assets': '', // injected 10 | '~utils': 'utils', 11 | '~tags': 'tags', 12 | } 13 | }, 14 | 15 | ssr: { 16 | type: null, 17 | entry: null, 18 | }, 19 | 20 | templates: { 21 | routes: 'routes', 22 | layout: /^_layout/, 23 | test: /\.([tj]sx?|svelte|vue)$/, 24 | errors: 'errors', 25 | }, 26 | 27 | assets: { 28 | dir: 'assets', 29 | test: /\.(svg|woff2?|ttf|eot|jpe?g|png|gif|mp4|mov|ogg|webm)$/, 30 | }, 31 | 32 | copy: ['static', 'public'], 33 | 34 | replace: { 35 | // updated 36 | '__DEV__': 'true', 37 | '__BROWSER__': 'true', 38 | 'process.browser': 'true', 39 | 'process.env.NODE_ENV': 'development', 40 | '__SSR__': 'true', 41 | }, 42 | 43 | resolve: { 44 | extensions: ['.mjs', '.js', '.jsx', '.json'], 45 | mainFields: ['module', 'jsnext', 'jsnext:main', 'main'], 46 | }, 47 | 48 | commonjs: { 49 | extensions: ['.js', '.cjs'] 50 | }, 51 | 52 | json: { 53 | preferConst: true, 54 | namedExports: true, 55 | indent: ' ', 56 | }, 57 | 58 | terser: { 59 | mangle: true, 60 | compress: true, 61 | output: { 62 | comments: false 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/freshie/src/config/plugins/copy.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { totalist } from 'totalist'; 3 | import { exists } from '../../utils/fs'; 4 | 5 | export function Copy(dirs: string[] = []): Rollup.Plugin { 6 | return { 7 | name: 'plugins/copy', 8 | async generateBundle() { 9 | await Promise.all( 10 | dirs.map(dir => { 11 | return exists(dir) && totalist(dir, (rel, abs) => { 12 | this.emitFile({ 13 | type: 'asset', 14 | source: readFileSync(abs), 15 | fileName: rel // no hash 16 | }); 17 | }) 18 | }) 19 | ); 20 | } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/freshie/src/config/plugins/html.ts: -------------------------------------------------------------------------------- 1 | import * as fs from '../../utils/fs'; 2 | 3 | import type { HTMLElement } from 'node-html-parser'; 4 | 5 | function toPreload(href: string, type: 'script' | 'style' | 'image' | 'font'): string { 6 | return ``; 7 | } 8 | 9 | function parse(value: string): HTMLElement { 10 | return require('node-html-parser').parse(value); 11 | } 12 | 13 | function append(base: HTMLElement, content: string) { 14 | let node = parse(content); 15 | base.appendChild(node); 16 | } 17 | 18 | // TODO? add `nomodule` option 19 | // TODO? expose `preload` option 20 | interface Options { 21 | preload?: boolean; 22 | publicPath?: string; 23 | minify?: boolean; 24 | } 25 | 26 | // TODO: preload `routes` files? 27 | // TODO: template via UI libary (`index.html.{ext}`) 28 | export function HTML(template: string, opts: Options = {}): Rollup.Plugin { 29 | const { publicPath='/', preload=true, minify=true } = opts; 30 | 31 | return { 32 | name: 'plugins/html', 33 | // TODO(MAYBE): Check if HTML entry/input (hook: `options`) 34 | async generateBundle(config, bundle) { 35 | const { format } = config; 36 | const entryAssets: Set = new Set; 37 | 38 | for (let key in bundle) { 39 | if (!/\.js$/.test(key)) continue; 40 | 41 | let tmp = bundle[key] as Rollup.Chunk; 42 | if (!tmp.isEntry) continue; 43 | 44 | entryAssets.add(key); // JS file 45 | tmp.imports.forEach(str => entryAssets.add(str)); 46 | tmp.referencedFiles.forEach(str => entryAssets.add(str)); 47 | } 48 | 49 | let document = parse(await fs.read(template, 'utf8')); 50 | 51 | // TODO: meta tags 52 | document.querySelectorAll('link,script').forEach(elem => { 53 | let { src, href } = elem.rawAttributes; 54 | // ignore external asset path(s) – not ours 55 | if (/^(https?:)?\/\//.test(href || src)) return; 56 | 57 | let asset = href || src; 58 | if (asset.startsWith('/')) { 59 | asset = asset.substring(1); 60 | } 61 | 62 | // TODO: lookup MANIFEST for new/output filename 63 | // use that, else rely on what's there + exists() check? 64 | // eg; /styles.css => /abc123.css 65 | 66 | elem.setAttribute(href ? 'href' : 'src', publicPath + asset); 67 | }); 68 | 69 | // TODO? Throw error if empty 70 | if (entryAssets.size > 0) { 71 | const dochead = document.querySelector('head'); 72 | const docbody = document.querySelector('body'); 73 | 74 | for (let filename of entryAssets) { 75 | filename = publicPath + filename; 76 | 77 | if (/\.css$/.test(filename)) { 78 | if (preload) append(dochead, toPreload(filename, 'style')); 79 | append(dochead, ``); 80 | } else if (/\.m?js$/.test(filename)) { 81 | if (/esm?/.test(format)) { 82 | // TODO(future): "preload" => "modulepreload" *only* when better supported 83 | if (preload) append(dochead, ``); 84 | append(docbody, ``); 85 | append(docbody, ``); 86 | } else { 87 | if (preload) append(dochead, toPreload(filename, 'script')); 88 | append(docbody, ``); 89 | } 90 | } 91 | } 92 | } 93 | 94 | // TODO? use `html-minifier`, overkill? 95 | if (minify) document.removeWhitespace(); 96 | 97 | this.emitFile({ 98 | type: 'asset', 99 | fileName: 'index.html', 100 | source: document.toString(), 101 | }); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/freshie/src/config/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './copy'; 2 | export * from './html'; 3 | export * from './router'; 4 | export * from './runtime'; 5 | export * from './template'; 6 | export * from './summary'; 7 | export * from './terser'; 8 | -------------------------------------------------------------------------------- /packages/freshie/src/config/plugins/router.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | const DIR_ENV = join(__dirname, '..', 'env', 'index.mjs'); 4 | const DIR_HTTP = join(__dirname, '..', 'http', 'index.mjs'); 5 | const DIR_ROUTER = join(__dirname, '..', 'router', 'index.mjs'); 6 | 7 | export const Router: Rollup.Plugin = { 8 | name: 'plugins/router', 9 | resolveId: (id) => id === 'freshie/router' ? DIR_ROUTER : null, 10 | } 11 | 12 | export const HTTP: Rollup.Plugin = { 13 | name: 'plugins/http', 14 | resolveId: (id) => id === 'freshie/http' ? DIR_HTTP : null, 15 | } 16 | 17 | export const ENV: Rollup.Plugin = { 18 | name: 'plugins/env', 19 | resolveId: (id) => id === 'freshie/env' ? DIR_ENV : null, 20 | } 21 | -------------------------------------------------------------------------------- /packages/freshie/src/config/plugins/runtime.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import * as fs from '../../utils/fs'; 3 | 4 | const RUNTIME = join(__dirname, '..', 'runtime', 'index.dom.js'); 5 | 6 | async function xform(src: string, file: string, routes: Build.Route[], errors: Build.Error[], isDOM: boolean): Promise { 7 | const fdata = await fs.read(file, 'utf8'); 8 | const Layouts: Map = new Map; 9 | let count=0, imports='', $routes='', $errors=''; 10 | 11 | const to = (file: string) => file.replace(src, '\0src').replace(/[\\\/]+/g, '/'); 12 | 13 | function to_layout(file: string | void): string | void { 14 | let local = file && Layouts.get(file); 15 | if (file && local) return local; 16 | if (file && !local) { 17 | Layouts.set(file, local = `$Layout${count++}`); 18 | imports += `import * as ${local} from '${to(file)}';\n`; 19 | return local; 20 | } 21 | } 22 | 23 | // TODO: multiple layout nesting 24 | routes.forEach((tmp, idx) => { 25 | if ($routes) $routes += '\n\t'; 26 | 27 | if (isDOM) { 28 | let views = [`import('${to(tmp.file)}')`]; 29 | if (tmp.layout) views.unshift(`import('${to(tmp.layout)}')`); 30 | $routes += `define('${tmp.pattern}', () => Promise.all([ ${views} ]));`; 31 | } else { 32 | let views = [`$Route${idx}`]; 33 | let layout = to_layout(tmp.layout); 34 | if (layout) views.unshift(layout); 35 | imports += `import * as $Route${idx} from '${to(tmp.file)}';\n`; 36 | $routes += `define('${tmp.pattern}', ${views});`; 37 | } 38 | }); 39 | 40 | errors.forEach((tmp, idx) => { 41 | if (isDOM) { 42 | let views = [`import('${to(tmp.file)}')`]; 43 | if (tmp.layout) views.unshift(`import('${to(tmp.layout)}')`); 44 | $errors += `'${tmp.key}': () => Promise.all([ ${views} ]),`; 45 | } else { 46 | let views = [`$Error${idx}`]; 47 | let layout = to_layout(tmp.layout); 48 | if (layout) views.unshift(layout); 49 | imports += `import * as $Error${idx} from '${to(tmp.file)}';\n`; 50 | $errors += `'${tmp.key}': prepare([${views}]),`; 51 | } 52 | }); 53 | 54 | if (imports) imports += '\n'; 55 | return imports + fdata.replace('/* */', $routes).replace('/* */', $errors); 56 | } 57 | 58 | export function Runtime(src: string, routes: Build.Route[], errors: Build.Error[], isDOM: boolean): Rollup.Plugin { 59 | const ident = 'freshie/runtime'; 60 | 61 | return { 62 | name: 'plugins/runtime', 63 | resolveId: id => { 64 | if (isDOM && id === ident) return id; 65 | if (id.startsWith('\0src')) return join(src, id.replace('\0src', '')); 66 | }, 67 | load: id => { 68 | if (id === ident) return xform(src, RUNTIME, routes, errors, isDOM); 69 | if (/[\\\/]+@freshie[\\\/]+ssr/.test(id)) return xform(src, id, routes, errors, isDOM); 70 | } 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /packages/freshie/src/config/plugins/summary.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://github.com/lukeed/bundt 3 | */ 4 | 5 | import colors from 'kleur'; 6 | import { gzipSync } from 'zlib'; 7 | import * as pretty from '../../utils/pretty'; 8 | import * as log from '../../utils/log'; 9 | 10 | type Options = Record<'brotli'|'isDOM', boolean>; 11 | type Padding = (str: string, max: number) => string; 12 | type Stats = Record<'file'|'size'|'gzip', string> & { notice: number }; 13 | 14 | const gut2 = ' '.repeat(2); 15 | const gut4 = ' '.repeat(4); 16 | const th = colors.dim().bold().italic().underline; 17 | const rpad: Padding = (str, max) => str.padEnd(max); 18 | const lpad: Padding = (str, max) => str.padStart(max); 19 | const levels = [colors.cyan, colors.yellow, colors.red]; // sizes|notices 20 | 21 | let max = { file:0, size:0, gzip:0 }; 22 | 23 | // TODO: brotli 24 | export function Summary(opts: Partial = {}): Rollup.Plugin { 25 | const { isDOM } = opts; 26 | 27 | let start: number; 28 | let name = colors.bold().underline().green(isDOM ? 'DOM' : 'SSR'); 29 | 30 | return { 31 | name: 'plugins/summary', 32 | 33 | // options(config) { 34 | // config.perf = true; 35 | // return config; 36 | // }, 37 | 38 | buildStart() { 39 | start = Date.now(); 40 | }, 41 | 42 | generateBundle(_config, bundle) { 43 | let tmp: Stats, out = `Compiled ${name} output in ${pretty.time(Date.now() - start)}`; 44 | 45 | let assets = Object.keys(bundle).sort().map(file => { 46 | let code = (bundle[file] as Rollup.Chunk).code || (bundle[file] as Rollup.Asset).source; 47 | let len = pretty.size(code.length); 48 | let gz = gzipSync(code).length; 49 | 50 | let notice = gz >= 2e5 ? 2 : gz >= 1e5 ? 1 : 0; //~> 200kb vs 100kb 51 | tmp = { file, size: len, gzip: pretty.size(gz), notice }; 52 | 53 | max.file = Math.max(max.file, file.length); 54 | max.gzip = Math.max(max.gzip, tmp.gzip.length); 55 | max.size = Math.max(max.size, len.length); 56 | 57 | return tmp; 58 | }); 59 | 60 | if (isDOM) { 61 | // gutters 62 | max.file += 4; 63 | max.size += 4; 64 | } 65 | 66 | // table headers 67 | out += ('\n\n' + th(rpad('Filename', max.file)) + gut4 + th(lpad('Filesize', max.size)) + gut2 + colors.dim().bold().italic(lpad('(gzip)', max.gzip))); 68 | 69 | assets.forEach(obj => { 70 | let fn = levels[obj.notice]; 71 | let gz = colors.italic((obj.notice ? fn : colors.dim)(gut2 + lpad(obj.gzip, max.gzip))); 72 | out += ('\n' + colors.white(rpad(obj.file, max.file)) + gut4 + fn(lpad(obj.size, max.size)) + gz); 73 | }); 74 | 75 | log.success(out + '\n'); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/freshie/src/config/plugins/template.ts: -------------------------------------------------------------------------------- 1 | import * as fs from '../../utils/fs'; 2 | import * as assert from '../../utils/assert'; 3 | 4 | export function Template(file: string): Rollup.Plugin { 5 | const ident = '!!~html~!!'; 6 | 7 | return { 8 | name: 'plugins/template', 9 | buildStart() { 10 | assert.exists(file, 'Cannot find pre-built "index.html" template!'); 11 | }, 12 | resolveId(id) { 13 | return id === ident ? ident : null; 14 | }, 15 | async load(id) { 16 | if (id !== ident) return null; 17 | let html = await fs.read(file, 'utf8'); 18 | return `export const HTML = \`${html}\`;`; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/freshie/src/config/plugins/terser.ts: -------------------------------------------------------------------------------- 1 | import type { MinifyOptions } from 'terser'; 2 | 3 | export function Terser(options: MinifyOptions): Rollup.Plugin { 4 | let minify: typeof import('terser').minify; 5 | 6 | return { 7 | name: 'terser', 8 | buildStart() { 9 | minify = minify || require('terser').minify; 10 | }, 11 | async renderChunk(code, _chunk, config) { 12 | const { sourcemap, format } = config; 13 | 14 | const output = await minify(code, { 15 | module: /esm?/.test(format), 16 | sourceMap: !!sourcemap, 17 | toplevel: true, 18 | ...options, 19 | }); 20 | 21 | return { 22 | code: output.code, 23 | map: output.map 24 | }; 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /packages/freshie/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as build } from './commands/build'; 2 | // export { default as deploy } from './commands/deploy'; 3 | export { default as watch } from './commands/watch'; 4 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/__tests__/argv.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import { resolve } from 'path'; 3 | import * as assert from 'uvu/assert'; 4 | import * as utils from '../argv'; 5 | 6 | const toBool = suite('toBool'); 7 | 8 | toBool('should be a function', () => { 9 | assert.type(utils.toBool, 'function'); 10 | }); 11 | 12 | toBool('should parse `false` values', () => { 13 | assert.is(utils.toBool(0), false); 14 | assert.is(utils.toBool('0'), false); 15 | assert.is(utils.toBool(false), false); 16 | assert.is(utils.toBool('false'), false); 17 | }); 18 | 19 | toBool('should return `fallback` otherwise (default = true)', () => { 20 | assert.is(utils.toBool(), true); 21 | assert.is(utils.toBool(''), true); 22 | assert.is(utils.toBool(null), true); 23 | assert.is(utils.toBool(undefined), true); 24 | assert.is(utils.toBool('hello'), true); 25 | assert.is(utils.toBool(123), true); 26 | assert.is(utils.toBool(1), true); 27 | }); 28 | 29 | toBool('should return `fallback` on nullish value', () => { 30 | assert.is(utils.toBool(null, false), false); 31 | assert.is(utils.toBool(undefined, false), false); 32 | }); 33 | 34 | toBool.run(); 35 | 36 | // --- 37 | 38 | const normalize = suite('normalize'); 39 | 40 | normalize('should be a function', () => { 41 | assert.type(utils.normalize, 'function'); 42 | }); 43 | 44 | normalize('should ensure `Argv.Options` has values', () => { 45 | const input: Partial = {}; 46 | const output = utils.normalize(null, input); 47 | assert.is(output, undefined, 'returns nothing'); 48 | 49 | const cwd = resolve('.'); 50 | 51 | assert.is(input.cwd, cwd); 52 | 53 | assert.is(input.srcDir, 'src'); 54 | assert.is(input.destDir, 'build'); 55 | 56 | assert.is(input.src, cwd); // "{cwd}/src" missing 57 | assert.is(input.dest, resolve(cwd, 'build')); 58 | 59 | assert.is(input.ssr, true); 60 | assert.is(input.isProd, false); 61 | assert.is(input.minify, false); 62 | }); 63 | 64 | normalize('should accept `Argv.Options` partial values', () => { 65 | const input: Partial = { 66 | cwd: __dirname, 67 | minify: true, 68 | }; 69 | 70 | utils.normalize('hello', input); 71 | 72 | assert.is(input.cwd, __dirname); 73 | 74 | assert.is(input.srcDir, 'hello'); 75 | assert.is(input.destDir, 'build'); 76 | 77 | assert.is(input.src, __dirname); // "{cwd}/src" missing 78 | assert.is(input.dest, resolve(__dirname, 'build')); 79 | 80 | assert.is(input.ssr, true); 81 | assert.is(input.isProd, false); 82 | assert.is(input.sourcemap, true); // isProd = false 83 | assert.is(input.minify, false); // isProd = false 84 | }); 85 | 86 | normalize('should accept extra values', () => { 87 | const input: Partial = {}; 88 | utils.normalize('', input, { isProd: true }); 89 | assert.is(input.isProd, true); 90 | assert.is(input.sourcemap, false); 91 | assert.is(input.minify, true); 92 | }); 93 | 94 | normalize('should allow `minify` to be disabled in production', () => { 95 | const input: Partial = {}; 96 | utils.normalize('', input, { isProd: true, minify: false }); 97 | assert.is(input.isProd, true); 98 | assert.is(input.minify, false); 99 | }); 100 | 101 | normalize('should allow `sourcemap` to be enabled in production', () => { 102 | const input: Partial = {}; 103 | utils.normalize('', input, { isProd: true, sourcemap: true }); 104 | assert.is(input.isProd, true); 105 | assert.is(input.sourcemap, true); 106 | }); 107 | 108 | normalize.run(); 109 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/__tests__/errors.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as utils from '../errors'; 4 | 5 | const match = suite('match', { 6 | '401': '401', 7 | '4xx': '4xx', 8 | '419': '419', 9 | '5xx': '5xx', 10 | '502': '502', 11 | 'xxx': 'xxx', 12 | }); 13 | 14 | match('should be a function', () => { 15 | assert.type(utils.match, 'function'); 16 | }); 17 | 18 | match('should load exact matches', ctx => { 19 | assert.is(utils.match(401, ctx), '401'); 20 | assert.is(utils.match(419, ctx), '419'); 21 | assert.is(utils.match(502, ctx), '502'); 22 | }); 23 | 24 | match('should load "4xx" for 4xx codes w/o exact match', ctx => { 25 | assert.is(utils.match(400, ctx), '4xx'); 26 | assert.is(utils.match(402, ctx), '4xx'); 27 | assert.is(utils.match(413, ctx), '4xx'); 28 | }); 29 | 30 | match('should load "5xx" for 5xx codes w/o exact match', ctx => { 31 | assert.is(utils.match(500, ctx), '5xx'); 32 | assert.is(utils.match(501, ctx), '5xx'); 33 | assert.is(utils.match(503, ctx), '5xx'); 34 | }); 35 | 36 | match('should load "xxx" as last resort', ctx => { 37 | // test only – only sees 400-500 range 38 | assert.is(utils.match(200, ctx), 'xxx'); 39 | assert.is(utils.match(201, ctx), 'xxx'); 40 | assert.is(utils.match(399, ctx), 'xxx'); 41 | 42 | const copy = { ...ctx }; 43 | 44 | delete copy['4xx']; 45 | assert.is(utils.match(400, copy), 'xxx'); 46 | assert.is(utils.match(403, copy), 'xxx'); 47 | 48 | delete copy['5xx']; 49 | assert.is(utils.match(501, copy), 'xxx'); 50 | assert.is(utils.match(503, copy), 'xxx'); 51 | }); 52 | 53 | match.run(); 54 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/__tests__/fs.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import { join, isAbsolute } from 'path'; 3 | import * as assert from 'uvu/assert'; 4 | import * as utils from '../fs'; 5 | 6 | const read = suite('read'); 7 | 8 | read('should be a function', () => { 9 | assert.type(utils.read, 'function'); 10 | }); 11 | 12 | read('should yield a Buffer', async () => { 13 | const out = await utils.read(__filename); 14 | assert.instance(out, Buffer); 15 | }); 16 | 17 | read('should yield a "utf8" string', async () => { 18 | const out = await utils.read(__filename, 'utf8'); 19 | assert.type(out, 'string'); 20 | }); 21 | 22 | read.run(); 23 | 24 | // --- 25 | 26 | const write = suite('write'); 27 | 28 | write('should be a function', () => { 29 | assert.type(utils.write, 'function'); 30 | }); 31 | 32 | write('should write file with Buffer data', async () => { 33 | const file = join(__dirname, 'hello.txt'); 34 | 35 | try { 36 | await utils.write(file, Buffer.from('hello')); 37 | } finally { 38 | await utils.remove(file); 39 | } 40 | }); 41 | 42 | write('should write file with "utf8" data', async () => { 43 | const file = join(__dirname, 'hello.txt'); 44 | 45 | try { 46 | await utils.write(file, 'hello'); 47 | } finally { 48 | await utils.remove(file); 49 | } 50 | }); 51 | 52 | write.run(); 53 | 54 | // --- 55 | 56 | const exists = suite('exists'); 57 | 58 | exists('should be a function', () => { 59 | assert.type(utils.exists, 'function'); 60 | }); 61 | 62 | exists('should return `true` if file exists', () => { 63 | assert.is(utils.exists(__filename), true); 64 | }); 65 | 66 | exists('should return `true` if directory exists', () => { 67 | assert.is(utils.exists(__dirname), true); 68 | }); 69 | 70 | exists('should return `false` if path does not exist', () => { 71 | const foobar = join(__dirname, 'foobar'); 72 | assert.is(utils.exists(foobar), false); 73 | }); 74 | 75 | exists.run(); 76 | 77 | // --- 78 | 79 | const isDir = suite('isDir'); 80 | 81 | isDir('should be a function', () => { 82 | assert.type(utils.isDir, 'function'); 83 | }); 84 | 85 | isDir('should return `true` for existing directory', () => { 86 | assert.is(utils.isDir(__dirname), true); 87 | }); 88 | 89 | isDir('should return `false` for existing file', () => { 90 | assert.is(utils.isDir(__filename), false); 91 | }); 92 | 93 | isDir('should return `false` for non-existent path', () => { 94 | const foobar = join(__dirname, 'foobar'); 95 | assert.is(utils.isDir(foobar), false); 96 | }); 97 | 98 | isDir.run(); 99 | 100 | // --- 101 | 102 | const list = suite('list'); 103 | 104 | list('should be a function', () => { 105 | assert.type(utils.list, 'function'); 106 | }); 107 | 108 | list('should yield Array of relative strings', async () => { 109 | const out = await utils.list(__dirname); 110 | assert.instance(out, Array); 111 | assert.type(out[0], 'string'); 112 | assert.ok(!isAbsolute(out[0])); 113 | }); 114 | 115 | list('should throw error if input is file', async () => { 116 | try { 117 | await utils.list(__filename); 118 | assert.unreachable('should have thrown'); 119 | } catch (err) { 120 | assert.instance(err, Error); 121 | assert.match(err.message, 'ENOTDIR'); 122 | } 123 | }); 124 | 125 | list('should throw error if input does not exist', async () => { 126 | try { 127 | await utils.list(join(__dirname, 'foobar')); 128 | assert.unreachable('should have thrown'); 129 | } catch (err) { 130 | assert.instance(err, Error); 131 | assert.match(err.message, 'ENOENT'); 132 | } 133 | }); 134 | 135 | list.run(); 136 | 137 | // --- 138 | 139 | const match = suite('match', { 140 | list: ['hello', 'helloooo', 'world'] 141 | }); 142 | 143 | match('should be a function', () => { 144 | assert.type(utils.match, 'function'); 145 | }); 146 | 147 | match('should return a value matching the RegExp pattern', ctx => { 148 | assert.is(utils.match(ctx.list, /^wor/), 'world'); 149 | }); 150 | 151 | match('should return the first match only', ctx => { 152 | assert.is(utils.match(ctx.list, /hel/), 'hello'); 153 | assert.is(utils.match(ctx.list, /o{2,}$/), 'helloooo'); 154 | }); 155 | 156 | match('should return `void` if no matches', ctx => { 157 | assert.is(utils.match(ctx.list, /foo/), undefined); 158 | assert.is(utils.match(ctx.list, /123/), undefined); 159 | }); 160 | 161 | match.run(); 162 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/__tests__/routes.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as utils from '../routes'; 4 | 5 | const toSegment = suite('to_segment'); 6 | 7 | toSegment('should be a function', () => { 8 | assert.type(utils.to_segment, 'function'); 9 | }); 10 | 11 | toSegment('should return tuples', () => { 12 | assert.equal(utils.to_segment(''), [0, '']); 13 | }); 14 | 15 | toSegment('should handle "static" routes', () => { 16 | assert.equal(utils.to_segment('index'), [0, '']); 17 | assert.equal(utils.to_segment('blog'), [0, 'blog']); 18 | assert.equal(utils.to_segment('about'), [0, 'about']); 19 | assert.equal(utils.to_segment('foo.bar'), [0, 'foo.bar']); 20 | }); 21 | 22 | toSegment('should handle "param" routes', () => { 23 | assert.equal(utils.to_segment('[id]'), [1, ':id']); 24 | assert.equal(utils.to_segment('[slug]'), [1, ':slug']); 25 | 26 | assert.equal(utils.to_segment('[id?]'), [1, ':id?']); 27 | assert.equal(utils.to_segment('[slug?]'), [1, ':slug?']); 28 | }); 29 | 30 | toSegment('should handle "wild" routes', () => { 31 | assert.equal(utils.to_segment('[...id]'), [2, '*', 'id']); 32 | assert.equal(utils.to_segment('[...slug]'), [2, '*', 'slug']); 33 | }); 34 | 35 | toSegment.run(); 36 | 37 | // --- 38 | 39 | const toPattern = suite('to_pattern'); 40 | 41 | toPattern('should be a function', () => { 42 | assert.type(utils.to_pattern, 'function'); 43 | }); 44 | 45 | toPattern('should return { pattern, wild } object', () => { 46 | assert.equal( 47 | utils.to_pattern(''), 48 | { pattern: '/', wild: null, type: 0 } 49 | ); 50 | }); 51 | 52 | toPattern('should handle "static" routes', () => { 53 | assert.equal( 54 | utils.to_pattern('index.js'), 55 | { pattern: '/', wild: null, type: 0 } 56 | ); 57 | 58 | assert.equal( 59 | utils.to_pattern('blog.tsx'), 60 | { pattern: '/blog', wild: null, type: 0 } 61 | ); 62 | 63 | assert.equal( 64 | utils.to_pattern('blog/hello.ts'), 65 | { pattern: '/blog/hello', wild: null, type: 0 } 66 | ); 67 | 68 | assert.equal( 69 | utils.to_pattern('about/index.js'), 70 | { pattern: '/about', wild: null, type: 0 } 71 | ); 72 | 73 | assert.equal( 74 | utils.to_pattern('foo.bar'), 75 | { pattern: '/foo', wild: null, type: 0 } 76 | ); 77 | }); 78 | 79 | toPattern('should handle "param" routes', () => { 80 | assert.equal( 81 | utils.to_pattern('[id].js'), 82 | { pattern: '/:id', wild: null, type: 1 } 83 | ); 84 | 85 | assert.equal( 86 | utils.to_pattern('[slug].js'), 87 | { pattern: '/:slug', wild: null, type: 1 } 88 | ); 89 | 90 | assert.equal( 91 | utils.to_pattern('blog/[id?].ts'), 92 | { pattern: '/blog/:id?', wild: null, type: 1 } 93 | ); 94 | 95 | assert.equal( 96 | utils.to_pattern('foo/bar/[slug?].jsx'), 97 | { pattern: '/foo/bar/:slug?', wild: null, type: 1 } 98 | ); 99 | 100 | assert.equal( 101 | utils.to_pattern('blog/[year]/[month]/[slug]/index.jsx'), 102 | { pattern: '/blog/:year/:month/:slug', wild: null, type: 1 } 103 | ); 104 | }); 105 | 106 | toPattern('should handle "wild" routes', () => { 107 | assert.equal( 108 | utils.to_pattern('[...id].tsx'), 109 | { pattern: '/*', wild: 'id', type: 2 } 110 | ); 111 | 112 | assert.equal( 113 | utils.to_pattern('blog/[...slug].js'), 114 | { pattern: '/blog/*', wild: 'slug', type: 2 } 115 | ); 116 | }); 117 | 118 | toPattern.run(); 119 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/argv.ts: -------------------------------------------------------------------------------- 1 | import { join, resolve } from 'path'; 2 | import { isDir } from './fs'; 3 | 4 | // default = true 5 | export function toBool(val?: unknown, fallback = true) { 6 | return val == null ? fallback : !/(0|false)/.test(val as string); 7 | } 8 | 9 | export function normalize(src: Nullable, argv: Partial, extra: Partial = {}) { 10 | Object.assign(argv, extra); 11 | const cwd = argv.cwd = resolve(argv.cwd || '.'); 12 | 13 | argv.dest = join(cwd, argv.destDir = 'build'); 14 | argv.src = join(cwd, argv.srcDir = src || 'src'); 15 | 16 | // use root if "/src" does not exist 17 | argv.src = isDir(argv.src) ? argv.src : cwd; 18 | 19 | // default = false 20 | argv.isProd = !!argv.isProd; 21 | 22 | // default = true 23 | argv.ssr = toBool(argv.ssr, true); 24 | 25 | // default = (dev) false; (prod) true 26 | argv.minify = argv.isProd && toBool(argv.minify, true); 27 | 28 | // default = (dev) true; (prod) false 29 | argv.sourcemap = toBool(argv.sourcemap, !argv.isProd); 30 | } 31 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/assert.ts: -------------------------------------------------------------------------------- 1 | import * as log from './log'; 2 | import * as fs from './fs'; 3 | 4 | export const ok = (mix: unknown, msg: string) => !!mix || log.error(msg); 5 | export const exists = (file: string, msg: string) => fs.exists(file) || log.error(msg); 6 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/entries.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import * as fs from './fs'; 3 | 4 | export async function collect(src: string, options: Config.Options): Promise { 5 | const entries = await fs.list(src).then(files => { 6 | // dom: index.{ext} || index.dom.{ext} 7 | let dom = fs.match(files, /index\.(dom\.)?[tjm]sx?$/); 8 | if (dom) dom = join(src, dom); 9 | 10 | // ssr: index.ssr.{ext} 11 | let ssr = fs.match(files, /index\.ssr\.[tjm]sx?$/); 12 | ssr = ssr ? join(src, ssr) : options.ssr.entry; 13 | 14 | // html: index.html || index.html.{ext} 15 | let html = fs.match(files, /index\.html(\.(svelte|vue|[tjm]sx?))?$/); 16 | if (html) html = join(src, html); 17 | 18 | return { dom, ssr, html }; 19 | }); 20 | 21 | if (!entries.dom) throw new Error('Missing "DOM" entry file!'); 22 | if (!entries.html) throw new Error('Missing HTML template file!'); 23 | 24 | return entries as Build.Entries; 25 | } 26 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | import { join, parse } from 'path'; 2 | import { totalist } from 'totalist'; 3 | import escalade from 'escalade'; 4 | import { exists } from './fs'; 5 | 6 | // UNUSED - inlined directly in runtimes 7 | export function match(code: number, dict: Dict) { 8 | const key = String(code); 9 | return dict[key] || dict[key.charAt(0) + 'xx'] || dict['xxx']; 10 | } 11 | 12 | /** 13 | * Find all "errors/*" templates 14 | * @param src The "/src" directory path 15 | * @param options The "templates" config options 16 | */ 17 | export async function collect(src: string, options: Config.Options['templates']): Promise { 18 | const ERRORS: Build.Error[] = []; 19 | const { test, layout, errors } = options; 20 | 21 | const dir = join(src, errors); 22 | if (!exists(dir)) return ERRORS; 23 | 24 | let hasXXX = false, standin: Build.Error | void; 25 | const isLayout = (str: string) => layout.test(str) && test.test(str); 26 | 27 | await totalist(dir, async (base, absolute) => { 28 | if (/^[._]/.test(base) || !test.test(base)) return; 29 | const { name } = parse(base); 30 | if (name === 'xxx') { 31 | hasXXX = true; 32 | if (standin) { 33 | standin.file = absolute; 34 | return; 35 | } 36 | } 37 | 38 | const info: Build.Error = { 39 | file: absolute, 40 | layout: null, 41 | key: name, 42 | }; 43 | 44 | if (name === 'index') { 45 | if (hasXXX) return; 46 | info.key = 'xxx'; 47 | standin = info; 48 | } 49 | 50 | // scale upwards to find closest layout file 51 | await escalade(absolute, (dirname, contents) => { 52 | let tmp = contents.find(isLayout); 53 | if (tmp) return info.layout = join(dirname, tmp); 54 | }); 55 | 56 | ERRORS.push(info); 57 | }); 58 | 59 | return ERRORS; 60 | } 61 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { promisify } from 'util'; 3 | 4 | export { premove as remove } from 'premove'; 5 | 6 | export const read = promisify(fs.readFile); 7 | export const write = promisify(fs.writeFile); 8 | export const list = promisify(fs.readdir); 9 | 10 | export const exists = fs.existsSync; 11 | 12 | export function isDir(str: string): boolean { 13 | return exists(str) && fs.statSync(str).isDirectory(); 14 | } 15 | 16 | export function match(arr: string[], pattern: RegExp): string | void { 17 | return arr.find(x => pattern.test(x)); 18 | } 19 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { exists } from './fs'; 3 | 4 | export { collect as routes } from './routes'; 5 | export { collect as entries } from './entries'; 6 | export { collect as errors } from './errors'; 7 | 8 | export function load(str: string, dir?: string): T | false { 9 | str = resolve(dir || '.', str); 10 | return exists(str) && require(str); 11 | } 12 | 13 | export function from(dir: string, id: string) { 14 | return require.resolve(id, { paths: [dir, __dirname] }); 15 | } 16 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://github.com/lukeed/pwa/blob/master/packages/cli/lib/util/log.js 3 | */ 4 | 5 | import colors from 'kleur'; 6 | import type { Kleur } from 'kleur'; 7 | 8 | const PWA = colors.bold('[freshie]'); 9 | const SPACER = ' '.repeat(10); // "[freshie] " 10 | 11 | export function print(color: keyof Kleur, msg: string) { 12 | console.log(colors[color](PWA), msg.includes('\n') ? msg.replace(/(\r?\n)/g, '$1' + SPACER) : msg); 13 | } 14 | 15 | export const log = print.bind(0, 'white'); 16 | export const info = print.bind(0, 'cyan'); 17 | export const success = print.bind(0, 'green'); 18 | export const warn = print.bind(0, 'yellow'); 19 | export const error = print.bind(0, 'red'); 20 | 21 | export function bail(msg: Error | string, code = 1): never { 22 | error(msg instanceof Error ? msg.stack : msg); 23 | process.exit(code); 24 | } 25 | 26 | export const $pkg = colors.magenta; 27 | export const $dir = colors.bold().white; 28 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/pretty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://github.com/lukeed/pwa/blob/master/packages/cli/lib/util/pretty.js 3 | */ 4 | 5 | const UNITS = ['B', 'kB', 'MB', 'GB']; 6 | 7 | export function size(val: number): string { 8 | if (!val) return '0.00 kB'; 9 | let exp = Math.min(Math.floor(Math.log10(val) / 3), UNITS.length - 1) || 1; 10 | let out = (val / Math.pow(1e3, exp)).toPrecision(3); 11 | let idx = out.indexOf('.'); 12 | if (idx === -1) { 13 | out += '.00'; 14 | } else if (out.length - idx - 1 !== 2) { 15 | out = (out + '00').substring(0, idx + 3); // 2 + 1 for 0-based 16 | } 17 | return out + ' ' + UNITS[exp]; 18 | } 19 | 20 | export function time(ms=0): string { 21 | return (ms / 1e3).toFixed(2) + 's'; 22 | } 23 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/routes.ts: -------------------------------------------------------------------------------- 1 | import { join, parse, relative } from 'path'; 2 | import { totalist } from 'totalist'; 3 | import escalade from 'escalade'; 4 | import rsort from 'route-sort'; 5 | import { exists } from './fs'; 6 | 7 | // 0=static, 1=param, 2=wild 8 | export const enum Pattern { 9 | Static, 10 | Param, 11 | Wild, 12 | } 13 | 14 | type Segment = [Pattern, string, string?]; 15 | export function to_segment(name: string): Segment { 16 | if (!name || name === 'index') return [Pattern.Static, '']; 17 | if (name[0] !== '[') return [Pattern.Static, name]; 18 | 19 | name = name.slice(1, -1); // [slug] -> slug 20 | 21 | return (name.substring(0, 3) === '...') 22 | ? [Pattern.Wild, '*', name.substring(3)] 23 | : [Pattern.Param, ':' + name]; 24 | } 25 | 26 | export function to_pattern(rel: string) { 27 | let { dir, name } = parse(rel); 28 | let pattern='/', wild=null, type=0; 29 | let arr = [...dir.split(/[\\\/]+/g), name]; 30 | 31 | for (let i=0, tmp: Segment; i < arr.length; i++) { 32 | if (!arr[i]) continue; // no dir 33 | tmp = to_segment(arr[i]); 34 | type = Math.max(type, tmp[0]); 35 | 36 | if (tmp[1]) { 37 | if (pattern.length > 1) pattern += '/'; 38 | pattern += tmp[1]; 39 | 40 | if (tmp[0] === Pattern.Wild) { 41 | wild = tmp[2]; 42 | break; 43 | } 44 | } 45 | } 46 | 47 | return { pattern, wild, type }; 48 | } 49 | 50 | /** 51 | * Find all Pages/Routes 52 | * @param src The "/src" directory path 53 | * @param options The "templates" config options 54 | */ 55 | export async function collect(src: string, options: Config.Options['templates']): Promise { 56 | const routes = join(src, options.routes); 57 | if (!exists(routes)) return []; 58 | 59 | const { test, layout } = options; 60 | const PAGES = new Map(); 61 | 62 | const isLayout = (str: string) => layout.test(str) && test.test(str); 63 | 64 | await totalist(routes, async (base, absolute) => { 65 | if (/^[._]/.test(base) || !test.test(base)) return; 66 | const rel = relative(routes, absolute); 67 | 68 | const info: Build.Route = { 69 | ...to_pattern(rel), 70 | file: absolute, 71 | layout: null, 72 | }; 73 | 74 | // scale upwards to find closest layout file 75 | await escalade(absolute, (dirname, contents) => { 76 | let tmp = contents.find(isLayout); 77 | if (tmp) return info.layout = join(dirname, tmp); 78 | }); 79 | 80 | // TODO: check existing/conflict 81 | PAGES.set(info.pattern, info); 82 | }); 83 | 84 | const patterns = [...PAGES.keys()]; 85 | return rsort(patterns).map(key => PAGES.get(key)); 86 | } 87 | -------------------------------------------------------------------------------- /packages/freshie/src/utils/scoped.ts: -------------------------------------------------------------------------------- 1 | import * as utils from './index'; 2 | 3 | export const packages: Set = new Set(); 4 | 5 | export function list(cwd: string): Set { 6 | if (packages.size) return packages; 7 | 8 | const rgx = /^@freshie\//i; 9 | const pkg = utils.load('package.json', cwd); 10 | 11 | if (pkg) Object.keys( 12 | Object.assign({}, pkg.dependencies, pkg.devDependencies) 13 | ).forEach(name => rgx.test(name) && packages.add(name)); 14 | 15 | return packages; 16 | } 17 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'build' 3 | - 'examples/**' 4 | - 'packages/**' 5 | - '!**/test/**' 6 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # freshie [![CI](https://github.com/lukeed/freshie/workflows/CI/badge.svg)](https://github.com/lukeed/freshie/actions) [![licenses](https://licenses.dev/b/npm/freshie)](https://licenses.dev/npm/freshie) 2 | 3 | > A fresh take on building universal applications with support for pluggable frontends and backends. 4 | 5 | --- 6 | 7 |

WORK IN PROGRESS

8 | 9 |

Status: Functional, but very incomplete.

10 | 11 | --- 12 | 13 | ...There will be more here, I promise – please check back later! 14 | 15 | Otherwise, if you enjoy figuring things out on your own & _don't mind a few of rough edges_, please feel free to poke around! You may also reach out to me with any questions or feedback. 16 | 17 | ## License 18 | 19 | MIT © [Luke Edwards](https://lukeed.com) 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "transpileOnly": true, 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "esModuleInterop": true, 7 | "sourceMap": true 8 | } 9 | }, 10 | "compilerOptions": { 11 | "target": "esnext", 12 | "module": "esnext", 13 | "noImplicitAny": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "moduleResolution": "node", 17 | "removeComments": true, 18 | "sourceMap": false, 19 | "allowJs": true, 20 | "noEmit": true 21 | }, 22 | "include": [ 23 | "packages" 24 | ], 25 | "exclude": [ 26 | "node_modules" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------