@madojs/mado 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +291 -0
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +371 -0
- package/ROADMAP.md +52 -0
- package/dist/src/component.d.ts +48 -0
- package/dist/src/component.js +140 -0
- package/dist/src/component.js.map +1 -0
- package/dist/src/context.d.ts +40 -0
- package/dist/src/context.js +67 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/css.d.ts +54 -0
- package/dist/src/css.js +137 -0
- package/dist/src/css.js.map +1 -0
- package/dist/src/devtools.d.ts +22 -0
- package/dist/src/devtools.js +63 -0
- package/dist/src/devtools.js.map +1 -0
- package/dist/src/diagnostics.d.ts +11 -0
- package/dist/src/diagnostics.js +28 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/each.d.ts +39 -0
- package/dist/src/each.js +35 -0
- package/dist/src/each.js.map +1 -0
- package/dist/src/forms.d.ts +71 -0
- package/dist/src/forms.js +161 -0
- package/dist/src/forms.js.map +1 -0
- package/dist/src/head.d.ts +19 -0
- package/dist/src/head.js +97 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/html/bindings.d.ts +78 -0
- package/dist/src/html/bindings.js +304 -0
- package/dist/src/html/bindings.js.map +1 -0
- package/dist/src/html/parser.d.ts +64 -0
- package/dist/src/html/parser.js +521 -0
- package/dist/src/html/parser.js.map +1 -0
- package/dist/src/html/template-types.d.ts +27 -0
- package/dist/src/html/template-types.js +8 -0
- package/dist/src/html/template-types.js.map +1 -0
- package/dist/src/html/template.d.ts +45 -0
- package/dist/src/html/template.js +119 -0
- package/dist/src/html/template.js.map +1 -0
- package/dist/src/html.d.ts +16 -0
- package/dist/src/html.js +16 -0
- package/dist/src/html.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lazy.d.ts +38 -0
- package/dist/src/lazy.js +73 -0
- package/dist/src/lazy.js.map +1 -0
- package/dist/src/lifecycle.d.ts +45 -0
- package/dist/src/lifecycle.js +66 -0
- package/dist/src/lifecycle.js.map +1 -0
- package/dist/src/page.d.ts +161 -0
- package/dist/src/page.js +38 -0
- package/dist/src/page.js.map +1 -0
- package/dist/src/persisted.d.ts +47 -0
- package/dist/src/persisted.js +119 -0
- package/dist/src/persisted.js.map +1 -0
- package/dist/src/resource.d.ts +120 -0
- package/dist/src/resource.js +275 -0
- package/dist/src/resource.js.map +1 -0
- package/dist/src/router/manifest.d.ts +56 -0
- package/dist/src/router/manifest.js +302 -0
- package/dist/src/router/manifest.js.map +1 -0
- package/dist/src/router/match.d.ts +62 -0
- package/dist/src/router/match.js +117 -0
- package/dist/src/router/match.js.map +1 -0
- package/dist/src/router/navigation.d.ts +89 -0
- package/dist/src/router/navigation.js +263 -0
- package/dist/src/router/navigation.js.map +1 -0
- package/dist/src/router.d.ts +13 -0
- package/dist/src/router.js +13 -0
- package/dist/src/router.js.map +1 -0
- package/dist/src/signal.d.ts +67 -0
- package/dist/src/signal.js +238 -0
- package/dist/src/signal.js.map +1 -0
- package/docs/README.md +12 -0
- package/docs/en/00-the-mado-way.md +106 -0
- package/docs/en/01-routing.md +204 -0
- package/docs/en/02-project-layout.md +58 -0
- package/docs/en/03-static-bake.md +251 -0
- package/docs/en/04-ide-setup.md +162 -0
- package/docs/en/05-why-mado.md +193 -0
- package/docs/en/06-for-backenders.md +422 -0
- package/docs/en/07-llm-pitfalls.md +486 -0
- package/docs/en/08-llm-zero-history-test.md +56 -0
- package/docs/en/09-shadow-vs-light-dom.md +122 -0
- package/docs/en/README.md +16 -0
- package/docs/fr/00-the-mado-way.md +108 -0
- package/docs/fr/01-routing.md +202 -0
- package/docs/fr/02-project-layout.md +58 -0
- package/docs/fr/03-static-bake.md +290 -0
- package/docs/fr/04-ide-setup.md +162 -0
- package/docs/fr/05-why-mado.md +193 -0
- package/docs/fr/06-for-backenders.md +432 -0
- package/docs/fr/07-llm-pitfalls.md +487 -0
- package/docs/fr/08-llm-zero-history-test.md +60 -0
- package/docs/fr/09-shadow-vs-light-dom.md +121 -0
- package/docs/fr/README.md +16 -0
- package/docs/ru/00-the-mado-way.md +93 -0
- package/docs/ru/01-routing.md +194 -0
- package/docs/ru/02-project-layout.md +57 -0
- package/docs/ru/03-static-bake.md +251 -0
- package/docs/ru/04-ide-setup.md +144 -0
- package/docs/ru/05-why-mado.md +193 -0
- package/docs/ru/06-for-backenders.md +422 -0
- package/docs/ru/07-llm-pitfalls.md +485 -0
- package/docs/ru/08-llm-zero-history-test.md +56 -0
- package/docs/ru/09-shadow-vs-light-dom.md +122 -0
- package/docs/ru/README.md +14 -0
- package/docs/uk/00-the-mado-way.md +54 -0
- package/docs/uk/01-routing.md +82 -0
- package/docs/uk/02-project-layout.md +46 -0
- package/docs/uk/03-static-bake.md +49 -0
- package/docs/uk/04-ide-setup.md +26 -0
- package/docs/uk/05-why-mado.md +34 -0
- package/docs/uk/06-for-backenders.md +50 -0
- package/docs/uk/07-llm-pitfalls.md +82 -0
- package/docs/uk/08-llm-zero-history-test.md +31 -0
- package/docs/uk/09-shadow-vs-light-dom.md +40 -0
- package/docs/uk/README.md +16 -0
- package/llms.txt +155 -0
- package/package.json +81 -0
- package/scripts/bake.mjs +406 -0
- package/scripts/bundle.mjs +146 -0
- package/scripts/cli.mjs +382 -0
- package/scripts/new.mjs +80 -0
- package/scripts/preview.mjs +176 -0
- package/scripts/release-notes.mjs +66 -0
- package/scripts/showcase-regression.mjs +392 -0
- package/server/serve.mjs +292 -0
- package/starters/crud/README.md +21 -0
- package/starters/crud/index.html +20 -0
- package/starters/crud/package.json +17 -0
- package/starters/crud/src/components/app-shell.ts +51 -0
- package/starters/crud/src/components/ticket-detail.ts +33 -0
- package/starters/crud/src/components/ticket-form.ts +69 -0
- package/starters/crud/src/components/ticket-list.ts +66 -0
- package/starters/crud/src/lib/api.ts +76 -0
- package/starters/crud/src/main.ts +12 -0
- package/starters/crud/src/pages/home.ts +18 -0
- package/starters/crud/src/pages/not-found.ts +12 -0
- package/starters/crud/src/pages/ticket-detail.ts +6 -0
- package/starters/crud/src/pages/ticket-new.ts +6 -0
- package/starters/crud/src/pages/tickets.ts +6 -0
- package/starters/crud/src/routes.ts +9 -0
- package/starters/crud/src/styles/global.ts +155 -0
- package/starters/crud/tsconfig.json +15 -0
- package/starters/minimal/README.md +19 -0
- package/starters/minimal/index.html +20 -0
- package/starters/minimal/package.json +17 -0
- package/starters/minimal/src/components/app-counter.ts +31 -0
- package/starters/minimal/src/main.ts +9 -0
- package/starters/minimal/src/pages/home.ts +18 -0
- package/starters/minimal/src/pages/not-found.ts +14 -0
- package/starters/minimal/src/routes.ts +6 -0
- package/starters/minimal/src/styles/global.ts +60 -0
- package/starters/minimal/tsconfig.json +15 -0
- package/templates/page-detail.ts +63 -0
- package/templates/page-form.ts +94 -0
- package/templates/page-list.ts +79 -0
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@madojs/mado",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Mado — a small native-web SPA framework with Web Components, signals, tagged-template html, router, resources, and forms. TypeScript-only build, zero runtime dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"framework",
|
|
9
|
+
"spa",
|
|
10
|
+
"web-components",
|
|
11
|
+
"signals",
|
|
12
|
+
"tagged-templates",
|
|
13
|
+
"router",
|
|
14
|
+
"no-build",
|
|
15
|
+
"tsc",
|
|
16
|
+
"minimal",
|
|
17
|
+
"lit-alternative",
|
|
18
|
+
"solid-alternative"
|
|
19
|
+
],
|
|
20
|
+
"homepage": "https://github.com/madojs/mado#readme",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/madojs/mado.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/madojs/mado/issues"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"mado": "./scripts/cli.mjs"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/src/index.js",
|
|
32
|
+
"module": "./dist/src/index.js",
|
|
33
|
+
"types": "./dist/src/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/src/index.d.ts",
|
|
37
|
+
"import": "./dist/src/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./devtools.js": "./dist/src/devtools.js",
|
|
40
|
+
"./*": "./dist/src/*"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist/src",
|
|
44
|
+
"scripts",
|
|
45
|
+
"server",
|
|
46
|
+
"templates",
|
|
47
|
+
"starters",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE",
|
|
50
|
+
"CHANGELOG.md",
|
|
51
|
+
"ROADMAP.md",
|
|
52
|
+
"AGENTS.md",
|
|
53
|
+
"llms.txt",
|
|
54
|
+
"docs/**/*.md"
|
|
55
|
+
],
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "node scripts/cli.mjs build",
|
|
58
|
+
"watch": "node scripts/cli.mjs watch",
|
|
59
|
+
"serve": "node scripts/cli.mjs serve",
|
|
60
|
+
"dev": "node scripts/cli.mjs dev",
|
|
61
|
+
"bundle": "node scripts/cli.mjs bundle",
|
|
62
|
+
"bake": "node scripts/cli.mjs bake",
|
|
63
|
+
"preview": "node scripts/cli.mjs preview",
|
|
64
|
+
"test:browser": "node scripts/cli.mjs test browser",
|
|
65
|
+
"new": "node scripts/cli.mjs new",
|
|
66
|
+
"examples": "node scripts/cli.mjs examples",
|
|
67
|
+
"test": "node scripts/cli.mjs test",
|
|
68
|
+
"typecheck": "node scripts/cli.mjs typecheck",
|
|
69
|
+
"clean": "rm -rf dist out",
|
|
70
|
+
"prepublishOnly": "npm run clean && npm run build && npm test"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"esbuild": "^0.28.0",
|
|
74
|
+
"linkedom": "^0.18.12",
|
|
75
|
+
"playwright-core": "^1.47.0",
|
|
76
|
+
"typescript": "^6.0.3"
|
|
77
|
+
},
|
|
78
|
+
"engines": {
|
|
79
|
+
"node": ">=20"
|
|
80
|
+
}
|
|
81
|
+
}
|
package/scripts/bake.mjs
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// Smart Static: bake HTML for pages with `bake: { paths, data }`.
|
|
2
|
+
//
|
|
3
|
+
// node scripts/bake.mjs # reads src/routes.ts (or examples/routes.ts)
|
|
4
|
+
// ENTRY=examples/routes.ts node scripts/bake.mjs
|
|
5
|
+
//
|
|
6
|
+
// What it does:
|
|
7
|
+
// 1. Dynamically imports the routes manifest.
|
|
8
|
+
// 2. For every route whose page has `bake`:
|
|
9
|
+
// a) gets params through `bake.paths()`,
|
|
10
|
+
// b) gets data for each params object through `bake.data(params)`,
|
|
11
|
+
// c) renders TemplateResult into an HTML string (without a browser),
|
|
12
|
+
// d) bakes head() into <meta>/<link>/<script type=json-ld>,
|
|
13
|
+
// e) embeds baked data in <script id="bake" type="application/json">,
|
|
14
|
+
// f) writes out/<path>/index.html.
|
|
15
|
+
// 3. Generates out/sitemap.xml.
|
|
16
|
+
//
|
|
17
|
+
// Dependency: linkedom (~50KB pure JS DOM in Node). If it is missing, print a
|
|
18
|
+
// clear error.
|
|
19
|
+
//
|
|
20
|
+
// Design: no magic. This file does not call component methods like
|
|
21
|
+
// connectedCallback; it only expands TemplateResult structures into HTML.
|
|
22
|
+
// Web Components come alive on the client.
|
|
23
|
+
|
|
24
|
+
import { readFile, writeFile, mkdir, access, rm } from "node:fs/promises";
|
|
25
|
+
import { join, dirname, resolve } from "node:path";
|
|
26
|
+
import { pathToFileURL } from "node:url";
|
|
27
|
+
import { tmpdir } from "node:os";
|
|
28
|
+
|
|
29
|
+
// ---------- Options ----------
|
|
30
|
+
|
|
31
|
+
const ENTRY = process.env.ENTRY ?? "examples/routes.ts";
|
|
32
|
+
const OUT_DIR = process.env.OUT_DIR ?? "out";
|
|
33
|
+
const BASE_URL = process.env.BASE_URL ?? "https://example.com";
|
|
34
|
+
|
|
35
|
+
// ---------- Optional dependencies ----------
|
|
36
|
+
|
|
37
|
+
let parseHTML;
|
|
38
|
+
try {
|
|
39
|
+
({ parseHTML } = await import("linkedom"));
|
|
40
|
+
} catch {
|
|
41
|
+
console.error("[bake] package 'linkedom' is required: npm i -D linkedom");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let esbuild;
|
|
46
|
+
try {
|
|
47
|
+
esbuild = await import("esbuild");
|
|
48
|
+
} catch {
|
|
49
|
+
console.error("[bake] package 'esbuild' is required: npm i -D esbuild");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------- DOM polyfills for Node ----------
|
|
54
|
+
//
|
|
55
|
+
// router.ts/component.ts/css.ts touch window, document, location and
|
|
56
|
+
// customElements at module top-level. Node does not have those, so install
|
|
57
|
+
// linkedom stubs before importing the app graph.
|
|
58
|
+
|
|
59
|
+
const baseHtml = await readFile("examples/index.html", "utf8").catch(
|
|
60
|
+
() => "<!doctype html><html><head></head><body></body></html>",
|
|
61
|
+
);
|
|
62
|
+
const { window: linkedomWindow } = parseHTML(baseHtml);
|
|
63
|
+
|
|
64
|
+
globalThis.window = linkedomWindow;
|
|
65
|
+
globalThis.document = linkedomWindow.document;
|
|
66
|
+
globalThis.location = new URL("http://localhost/");
|
|
67
|
+
globalThis.history = {
|
|
68
|
+
pushState: () => {},
|
|
69
|
+
replaceState: () => {},
|
|
70
|
+
};
|
|
71
|
+
globalThis.customElements = {
|
|
72
|
+
define: () => {},
|
|
73
|
+
get: () => undefined,
|
|
74
|
+
whenDefined: () => Promise.resolve(),
|
|
75
|
+
};
|
|
76
|
+
globalThis.HTMLElement = linkedomWindow.HTMLElement ?? class {};
|
|
77
|
+
globalThis.CSSStyleSheet =
|
|
78
|
+
globalThis.CSSStyleSheet ??
|
|
79
|
+
class {
|
|
80
|
+
cssRules = [];
|
|
81
|
+
replaceSync() {}
|
|
82
|
+
};
|
|
83
|
+
globalThis.matchMedia = () => ({
|
|
84
|
+
matches: false,
|
|
85
|
+
addEventListener: () => {},
|
|
86
|
+
removeEventListener: () => {},
|
|
87
|
+
});
|
|
88
|
+
if (!globalThis.queueMicrotask) globalThis.queueMicrotask = (fn) => Promise.resolve().then(fn);
|
|
89
|
+
|
|
90
|
+
// ---------- Manifest import ----------
|
|
91
|
+
//
|
|
92
|
+
// Bundle routes.ts into a temporary CJS file so Node can import it without
|
|
93
|
+
// caring about paths/importmap. Pages, lib and Mado itself are bundled too.
|
|
94
|
+
|
|
95
|
+
const tmpFile = join(tmpdir(), `mado-bake-${Date.now()}.mjs`);
|
|
96
|
+
|
|
97
|
+
await esbuild.build({
|
|
98
|
+
entryPoints: [ENTRY],
|
|
99
|
+
bundle: true,
|
|
100
|
+
format: "esm",
|
|
101
|
+
platform: "node",
|
|
102
|
+
target: "es2022",
|
|
103
|
+
outfile: tmpFile,
|
|
104
|
+
// tsconfig paths are not read by esbuild here, so aliases are explicit.
|
|
105
|
+
tsconfig: "tsconfig.json",
|
|
106
|
+
// resolve '@madojs/mado' -> ./src/index.ts
|
|
107
|
+
alias: {
|
|
108
|
+
"@madojs/mado": resolve("src/index.ts"),
|
|
109
|
+
},
|
|
110
|
+
logLevel: "error",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const routesUrl = pathToFileURL(tmpFile).href;
|
|
114
|
+
const routesModule = await import(routesUrl);
|
|
115
|
+
await rm(tmpFile).catch(() => {});
|
|
116
|
+
const routeApi = routesModule.default;
|
|
117
|
+
|
|
118
|
+
if (!routeApi) {
|
|
119
|
+
console.error("[bake] routes.ts must default-export routes({...})");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Bake needs the source manifest, not RouterApi (runtime API). Therefore
|
|
124
|
+
// routes.ts must also export `manifest`.
|
|
125
|
+
//
|
|
126
|
+
// The chosen convention: routes.ts exports both `default` (RouterApi) and
|
|
127
|
+
// `manifest` (the source object). See examples/routes.ts.
|
|
128
|
+
|
|
129
|
+
const manifest = routesModule.manifest;
|
|
130
|
+
if (!manifest) {
|
|
131
|
+
console.error(
|
|
132
|
+
"[bake] routes.ts must also `export const manifest = {...}` " +
|
|
133
|
+
"(the same object passed to routes()).",
|
|
134
|
+
);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------- Main loop ----------
|
|
139
|
+
|
|
140
|
+
await mkdir(OUT_DIR, { recursive: true });
|
|
141
|
+
|
|
142
|
+
// Read the HTML template (the same index.html used by the app). Without it
|
|
143
|
+
// there is nowhere to place baked output.
|
|
144
|
+
const TEMPLATE_HTML = await readFile("examples/index.html", "utf8");
|
|
145
|
+
|
|
146
|
+
const sitemapEntries = [];
|
|
147
|
+
let total = 0;
|
|
148
|
+
|
|
149
|
+
for (const [pattern, entry] of Object.entries(manifest)) {
|
|
150
|
+
if (pattern === "*") continue;
|
|
151
|
+
|
|
152
|
+
// entry can be Page, () => import, or nested. Bake currently handles direct
|
|
153
|
+
// lazy imports and Page entries.
|
|
154
|
+
const pg = await resolvePage(entry);
|
|
155
|
+
if (!pg) continue;
|
|
156
|
+
if (!pg.bake) continue;
|
|
157
|
+
|
|
158
|
+
console.log(`[bake] ${pattern}`);
|
|
159
|
+
|
|
160
|
+
const allParams = await pg.bake.paths();
|
|
161
|
+
for (const params of allParams) {
|
|
162
|
+
const pathname = applyParams(pattern, params);
|
|
163
|
+
const data = await pg.bake.data(params);
|
|
164
|
+
const headMeta = pg.head ? pg.head(params, data) : {};
|
|
165
|
+
|
|
166
|
+
const tpl = pg.view({
|
|
167
|
+
params,
|
|
168
|
+
data,
|
|
169
|
+
path: () => pathname,
|
|
170
|
+
child: null,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const bodyHtml = renderTemplate(tpl);
|
|
174
|
+
const finalHtml = buildHtml({
|
|
175
|
+
template: TEMPLATE_HTML,
|
|
176
|
+
bodyHtml,
|
|
177
|
+
head: headMeta,
|
|
178
|
+
bakedData: data,
|
|
179
|
+
revalidate: pg.bake.revalidate,
|
|
180
|
+
canonical: headMeta.canonical ?? `${BASE_URL}${pathname}`,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const file = join(OUT_DIR, pathname === "/" ? "/index.html" : `${pathname}/index.html`);
|
|
184
|
+
await mkdir(dirname(file), { recursive: true });
|
|
185
|
+
await writeFile(file, finalHtml);
|
|
186
|
+
total++;
|
|
187
|
+
|
|
188
|
+
sitemapEntries.push({
|
|
189
|
+
loc: `${BASE_URL}${pathname}`,
|
|
190
|
+
changefreq: "weekly",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------- Sitemap ----------
|
|
196
|
+
|
|
197
|
+
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
|
198
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
199
|
+
${sitemapEntries
|
|
200
|
+
.map(
|
|
201
|
+
(e) =>
|
|
202
|
+
` <url><loc>${escapeXml(e.loc)}</loc><changefreq>${e.changefreq}</changefreq></url>`,
|
|
203
|
+
)
|
|
204
|
+
.join("\n")}
|
|
205
|
+
</urlset>
|
|
206
|
+
`;
|
|
207
|
+
await writeFile(join(OUT_DIR, "sitemap.xml"), sitemap);
|
|
208
|
+
|
|
209
|
+
console.log(`[bake] done: ${total} pages + sitemap.xml`);
|
|
210
|
+
|
|
211
|
+
// ---------- Helpers ----------
|
|
212
|
+
|
|
213
|
+
async function resolvePage(entry) {
|
|
214
|
+
if (entry && entry._page === true) return entry;
|
|
215
|
+
if (typeof entry === "function") {
|
|
216
|
+
try {
|
|
217
|
+
const mod = await entry();
|
|
218
|
+
return mod?.default;
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.warn("[bake] failed to load route:", e.message);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function applyParams(pattern, params) {
|
|
228
|
+
return pattern.replace(/:([\w]+)/g, (_, k) => {
|
|
229
|
+
const v = params[k];
|
|
230
|
+
if (v == null) throw new Error(`[bake] missing param :${k} for ${pattern}`);
|
|
231
|
+
return encodeURIComponent(String(v));
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function exists(p) {
|
|
236
|
+
try {
|
|
237
|
+
await access(p);
|
|
238
|
+
return true;
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------- Render TemplateResult → HTML string ----------
|
|
245
|
+
//
|
|
246
|
+
// Tiny server-side renderer for TemplateResult. Supports the same shapes as
|
|
247
|
+
// html.ts, but without events (@click is ignored) and without live signals
|
|
248
|
+
// (function values are called once).
|
|
249
|
+
|
|
250
|
+
function renderTemplate(tpl) {
|
|
251
|
+
if (tpl == null || tpl === false || tpl === true) return "";
|
|
252
|
+
if (typeof tpl === "string") return escapeHtml(tpl);
|
|
253
|
+
if (typeof tpl === "number") return String(tpl);
|
|
254
|
+
if (Array.isArray(tpl)) return tpl.map(renderTemplate).join("");
|
|
255
|
+
if (tpl && tpl._mado === true) return renderMadoTemplate(tpl);
|
|
256
|
+
// unknown
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function renderMadoTemplate(tpl) {
|
|
261
|
+
const { strings, values } = tpl;
|
|
262
|
+
let html = "";
|
|
263
|
+
for (let i = 0; i < strings.length; i++) {
|
|
264
|
+
html += strings[i];
|
|
265
|
+
if (i < strings.length - 1) {
|
|
266
|
+
html += renderValue(values[i], inAttributeContext(html));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Remove event marker attributes (the client html.ts does this too).
|
|
270
|
+
return html
|
|
271
|
+
.replace(/\s+@[\w-]+="[^"]*"/g, "")
|
|
272
|
+
.replace(/\s+\.([\w-]+)="([^"]*)"/g, ' $1="$2"')
|
|
273
|
+
.replace(/\s+\?([\w-]+)="(true|on|1)"/g, ' $1=""')
|
|
274
|
+
.replace(/\s+\?[\w-]+="(false|off|0|)"/g, "");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function inAttributeContext(html) {
|
|
278
|
+
const lastOpen = html.lastIndexOf("<");
|
|
279
|
+
const lastClose = html.lastIndexOf(">");
|
|
280
|
+
return lastOpen > lastClose;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renderValue(v, inAttr) {
|
|
284
|
+
if (v == null || v === false) return "";
|
|
285
|
+
if (v === true) return "";
|
|
286
|
+
if (typeof v === "function") {
|
|
287
|
+
try {
|
|
288
|
+
return renderValue(v(), inAttr);
|
|
289
|
+
} catch {
|
|
290
|
+
return "";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (Array.isArray(v)) return v.map((x) => renderValue(x, inAttr)).join("");
|
|
294
|
+
if (v && v._mado === true) return renderMadoTemplate(v);
|
|
295
|
+
if (inAttr) return escapeAttr(String(v));
|
|
296
|
+
return escapeHtml(String(v));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function escapeHtml(s) {
|
|
300
|
+
return s
|
|
301
|
+
.replace(/&/g, "&")
|
|
302
|
+
.replace(/</g, "<")
|
|
303
|
+
.replace(/>/g, ">");
|
|
304
|
+
}
|
|
305
|
+
function escapeAttr(s) {
|
|
306
|
+
return escapeHtml(s).replace(/"/g, """);
|
|
307
|
+
}
|
|
308
|
+
function escapeXml(s) {
|
|
309
|
+
return s
|
|
310
|
+
.replace(/&/g, "&")
|
|
311
|
+
.replace(/</g, "<")
|
|
312
|
+
.replace(/>/g, ">")
|
|
313
|
+
.replace(/"/g, """);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------- Final HTML with head + baked data ----------
|
|
317
|
+
|
|
318
|
+
function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical }) {
|
|
319
|
+
const { document } = parseHTML(template);
|
|
320
|
+
|
|
321
|
+
// head
|
|
322
|
+
if (head.title) document.title = head.title;
|
|
323
|
+
if (head.description) {
|
|
324
|
+
setMeta(document, { name: "description", content: head.description });
|
|
325
|
+
}
|
|
326
|
+
if (canonical) {
|
|
327
|
+
setLink(document, { rel: "canonical", href: canonical });
|
|
328
|
+
}
|
|
329
|
+
if (head.og) {
|
|
330
|
+
for (const [k, v] of Object.entries(head.og)) {
|
|
331
|
+
if (!v) continue;
|
|
332
|
+
setMeta(document, { property: `og:${k}`, content: String(v) });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (head.twitter || head.og) {
|
|
336
|
+
const tw = head.twitter ?? {};
|
|
337
|
+
const og = head.og ?? {};
|
|
338
|
+
setMeta(document, {
|
|
339
|
+
name: "twitter:card",
|
|
340
|
+
content: tw.card ?? "summary",
|
|
341
|
+
});
|
|
342
|
+
if (tw.title ?? og.title)
|
|
343
|
+
setMeta(document, { name: "twitter:title", content: tw.title ?? og.title });
|
|
344
|
+
if (tw.description ?? og.description)
|
|
345
|
+
setMeta(document, { name: "twitter:description", content: tw.description ?? og.description });
|
|
346
|
+
if (tw.image ?? og.image)
|
|
347
|
+
setMeta(document, { name: "twitter:image", content: tw.image ?? og.image });
|
|
348
|
+
}
|
|
349
|
+
for (const m of head.meta ?? []) setMeta(document, m);
|
|
350
|
+
for (const l of head.link ?? []) setLink(document, l);
|
|
351
|
+
|
|
352
|
+
// JSON-LD
|
|
353
|
+
if (head.jsonLd != null) {
|
|
354
|
+
const s = document.createElement("script");
|
|
355
|
+
s.setAttribute("type", "application/ld+json");
|
|
356
|
+
s.setAttribute("data-mado-head", "baked");
|
|
357
|
+
s.textContent = JSON.stringify(head.jsonLd);
|
|
358
|
+
document.head.appendChild(s);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// revalidate meta: for CDN or manual CI re-bake logic
|
|
362
|
+
if (revalidate) {
|
|
363
|
+
setMeta(document, {
|
|
364
|
+
name: "bake-revalidate",
|
|
365
|
+
content: String(revalidate),
|
|
366
|
+
});
|
|
367
|
+
setMeta(document, {
|
|
368
|
+
name: "bake-stamp",
|
|
369
|
+
content: String(Date.now()),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Baked data: the client can use it as initialData.
|
|
374
|
+
if (bakedData !== undefined) {
|
|
375
|
+
const s = document.createElement("script");
|
|
376
|
+
s.setAttribute("type", "application/json");
|
|
377
|
+
s.id = "bake";
|
|
378
|
+
s.textContent = JSON.stringify(bakedData);
|
|
379
|
+
document.body.appendChild(s);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// body: insert baked HTML inside #app.
|
|
383
|
+
const app = document.getElementById("app");
|
|
384
|
+
if (app) {
|
|
385
|
+
app.innerHTML = bodyHtml;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return "<!doctype html>\n" + document.documentElement.outerHTML;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function setMeta(doc, attrs) {
|
|
392
|
+
const m = doc.createElement("meta");
|
|
393
|
+
if (attrs.name) m.setAttribute("name", attrs.name);
|
|
394
|
+
if (attrs.property) m.setAttribute("property", attrs.property);
|
|
395
|
+
m.setAttribute("content", attrs.content);
|
|
396
|
+
m.setAttribute("data-mado-head", "baked");
|
|
397
|
+
doc.head.appendChild(m);
|
|
398
|
+
}
|
|
399
|
+
function setLink(doc, attrs) {
|
|
400
|
+
const l = doc.createElement("link");
|
|
401
|
+
l.setAttribute("rel", attrs.rel);
|
|
402
|
+
l.setAttribute("href", attrs.href);
|
|
403
|
+
if (attrs.hreflang) l.setAttribute("hreflang", attrs.hreflang);
|
|
404
|
+
l.setAttribute("data-mado-head", "baked");
|
|
405
|
+
doc.head.appendChild(l);
|
|
406
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Optional production bundle through esbuild. No config files.
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// node scripts/bundle.mjs # → out/<hash>.js + chunks + out/index.html
|
|
5
|
+
// ENTRY=examples/main.ts node scripts/bundle.mjs
|
|
6
|
+
//
|
|
7
|
+
// What it does:
|
|
8
|
+
// 1. Bundles entry with code splitting (each dynamic import → a chunk).
|
|
9
|
+
// 2. Writes out/ index.html with modulepreload for critical chunks.
|
|
10
|
+
// 3. Computes SRI hashes and writes integrity="...".
|
|
11
|
+
// 4. Creates .gz and .br next to each .js for nginx gzip_static.
|
|
12
|
+
//
|
|
13
|
+
// Dependency: esbuild (devDep, only needed when running bundle).
|
|
14
|
+
|
|
15
|
+
import { build } from "esbuild";
|
|
16
|
+
import {
|
|
17
|
+
readFile,
|
|
18
|
+
writeFile,
|
|
19
|
+
mkdir,
|
|
20
|
+
cp,
|
|
21
|
+
stat,
|
|
22
|
+
readdir,
|
|
23
|
+
} from "node:fs/promises";
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import { gzipSync, brotliCompressSync, constants as zlibConst } from "node:zlib";
|
|
26
|
+
import { join, basename } from "node:path";
|
|
27
|
+
import { existsSync } from "node:fs";
|
|
28
|
+
|
|
29
|
+
const ENTRY = process.env.ENTRY ?? "examples/main.ts";
|
|
30
|
+
const OUT_DIR = process.env.OUT_DIR ?? "out";
|
|
31
|
+
const HTML = process.env.HTML ?? "examples/index.html";
|
|
32
|
+
|
|
33
|
+
await mkdir(OUT_DIR, { recursive: true });
|
|
34
|
+
|
|
35
|
+
console.log(`[bundle] entry: ${ENTRY}`);
|
|
36
|
+
|
|
37
|
+
// 1) esbuild with code splitting
|
|
38
|
+
const result = await build({
|
|
39
|
+
entryPoints: [ENTRY],
|
|
40
|
+
bundle: true,
|
|
41
|
+
minify: true,
|
|
42
|
+
sourcemap: true,
|
|
43
|
+
format: "esm",
|
|
44
|
+
target: "es2022",
|
|
45
|
+
splitting: true,
|
|
46
|
+
outdir: OUT_DIR,
|
|
47
|
+
entryNames: "main-[hash]",
|
|
48
|
+
chunkNames: "chunk-[hash]",
|
|
49
|
+
assetNames: "asset-[hash]",
|
|
50
|
+
metafile: true,
|
|
51
|
+
legalComments: "none",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// 2) Find the main entry file
|
|
55
|
+
const entryFile = Object.entries(result.metafile.outputs)
|
|
56
|
+
.find(([name, info]) => info.entryPoint && name.endsWith(".js"))?.[0];
|
|
57
|
+
|
|
58
|
+
if (!entryFile) {
|
|
59
|
+
console.error("[bundle] entry not found in outputs");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const mainBundle = basename(entryFile);
|
|
63
|
+
|
|
64
|
+
// 3) Collect all js chunks (including main)
|
|
65
|
+
const allJs = (await readdir(OUT_DIR)).filter((f) => f.endsWith(".js"));
|
|
66
|
+
|
|
67
|
+
// 4) Compress every .js into .gz and .br (for nginx gzip_static)
|
|
68
|
+
let totalRaw = 0;
|
|
69
|
+
let totalGz = 0;
|
|
70
|
+
let totalBr = 0;
|
|
71
|
+
|
|
72
|
+
for (const f of allJs) {
|
|
73
|
+
const p = join(OUT_DIR, f);
|
|
74
|
+
const buf = await readFile(p);
|
|
75
|
+
totalRaw += buf.length;
|
|
76
|
+
|
|
77
|
+
const gz = gzipSync(buf, { level: 9 });
|
|
78
|
+
await writeFile(`${p}.gz`, gz);
|
|
79
|
+
totalGz += gz.length;
|
|
80
|
+
|
|
81
|
+
const br = brotliCompressSync(buf, {
|
|
82
|
+
params: { [zlibConst.BROTLI_PARAM_QUALITY]: 11 },
|
|
83
|
+
});
|
|
84
|
+
await writeFile(`${p}.br`, br);
|
|
85
|
+
totalBr += br.length;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 5) SRI for the main bundle
|
|
89
|
+
const mainBuf = await readFile(join(OUT_DIR, mainBundle));
|
|
90
|
+
const sri = "sha384-" + createHash("sha384").update(mainBuf).digest("base64");
|
|
91
|
+
|
|
92
|
+
// 6) HTML: replace <script> and add modulepreload for main
|
|
93
|
+
let html = await readFile(HTML, "utf8");
|
|
94
|
+
|
|
95
|
+
// modulepreload for main + other chunks. For now preload all chunks; this can
|
|
96
|
+
// later be filtered from metafile analysis.
|
|
97
|
+
const preloads = allJs
|
|
98
|
+
.map(
|
|
99
|
+
(f) =>
|
|
100
|
+
` <link rel="modulepreload" href="/${f}"${
|
|
101
|
+
f === mainBundle ? ` integrity="${sri}" crossorigin="anonymous"` : ""
|
|
102
|
+
} />`,
|
|
103
|
+
)
|
|
104
|
+
.join("\n");
|
|
105
|
+
|
|
106
|
+
// Remove the old importmap (it points to dev paths under /dist/src/...).
|
|
107
|
+
html = html.replace(/<script type="importmap">[\s\S]*?<\/script>/, "");
|
|
108
|
+
|
|
109
|
+
// Replace the script with the new one.
|
|
110
|
+
html = html.replace(
|
|
111
|
+
/<script\s+type="module"\s+src="[^"]+"[^>]*><\/script>/,
|
|
112
|
+
`<script type="module" src="/${mainBundle}" integrity="${sri}" crossorigin="anonymous"></script>`,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Insert preloads before </head>.
|
|
116
|
+
html = html.replace(
|
|
117
|
+
/<\/head>/,
|
|
118
|
+
`${preloads}\n </head>`,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
await writeFile(join(OUT_DIR, "index.html"), html);
|
|
122
|
+
|
|
123
|
+
// 7) Static files
|
|
124
|
+
for (const name of ["favicon.ico", "favicon.svg", "assets"]) {
|
|
125
|
+
const src = join("examples", name);
|
|
126
|
+
if (existsSync(src)) {
|
|
127
|
+
const s = await stat(src);
|
|
128
|
+
if (s.isDirectory()) {
|
|
129
|
+
await cp(src, join(OUT_DIR, name), { recursive: true });
|
|
130
|
+
} else {
|
|
131
|
+
await cp(src, join(OUT_DIR, name));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 8) Stats
|
|
137
|
+
const kib = (n) => (n / 1024).toFixed(1);
|
|
138
|
+
console.log(`[bundle] chunks: ${allJs.length}`);
|
|
139
|
+
for (const f of allJs.sort()) {
|
|
140
|
+
const sz = (await stat(join(OUT_DIR, f))).size;
|
|
141
|
+
const gz = (await stat(join(OUT_DIR, `${f}.gz`))).size;
|
|
142
|
+
const star = f === mainBundle ? " *" : "";
|
|
143
|
+
console.log(` ${f.padEnd(24)} ${kib(sz).padStart(6)} KB raw, ${kib(gz).padStart(5)} KB gz${star}`);
|
|
144
|
+
}
|
|
145
|
+
console.log(`[bundle] total: ${kib(totalRaw)} KB raw / ${kib(totalGz)} KB gz / ${kib(totalBr)} KB br`);
|
|
146
|
+
console.log(`[bundle] entry SRI: ${sri}`);
|