@madojs/mado 0.5.1 → 0.6.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 +26 -0
- package/CHANGELOG.md +153 -0
- package/MADO_V1_PLAN.md +179 -0
- package/README.md +31 -13
- package/ROADMAP.md +28 -7
- package/TODO.md +72 -0
- package/dist/src/forms.d.ts +37 -4
- package/dist/src/forms.js +331 -57
- package/dist/src/forms.js.map +1 -1
- package/dist/src/html/bindings.d.ts +41 -0
- package/dist/src/html/bindings.js +163 -6
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/html.d.ts +2 -0
- package/dist/src/html.js +1 -0
- package/dist/src/html.js.map +1 -1
- package/dist/src/index.d.ts +6 -6
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/page.d.ts +56 -0
- package/dist/src/page.js +17 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/router/manifest.d.ts +16 -1
- package/dist/src/router/manifest.js +181 -38
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +7 -2
- package/dist/src/router/match.js +14 -4
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.d.ts +10 -0
- package/dist/src/router/navigation.js +71 -3
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/signal.d.ts +15 -1
- package/dist/src/signal.js +112 -16
- package/dist/src/signal.js.map +1 -1
- package/docs/en/02-project-layout.md +99 -40
- package/docs/en/10-app-architecture.md +141 -0
- package/docs/en/11-layouts.md +115 -0
- package/docs/en/12-auth-and-api.md +217 -0
- package/docs/en/13-deployment.md +192 -0
- package/docs/en/14-testing.md +82 -0
- package/docs/en/15-error-handling.md +100 -0
- package/docs/en/16-bake-cookbook.md +93 -0
- package/docs/en/README.md +7 -0
- package/docs/fr/10-app-architecture.md +61 -0
- package/docs/fr/11-layouts.md +35 -0
- package/docs/fr/12-auth-and-api.md +35 -0
- package/docs/fr/13-deployment.md +39 -0
- package/docs/fr/14-testing.md +41 -0
- package/docs/fr/15-error-handling.md +50 -0
- package/docs/fr/16-bake-cookbook.md +35 -0
- package/docs/fr/README.md +7 -0
- package/docs/ru/10-app-architecture.md +100 -0
- package/docs/ru/11-layouts.md +47 -0
- package/docs/ru/12-auth-and-api.md +53 -0
- package/docs/ru/13-deployment.md +60 -0
- package/docs/ru/14-testing.md +50 -0
- package/docs/ru/15-error-handling.md +56 -0
- package/docs/ru/16-bake-cookbook.md +55 -0
- package/docs/ru/README.md +7 -0
- package/docs/uk/10-app-architecture.md +56 -0
- package/docs/uk/11-layouts.md +34 -0
- package/docs/uk/12-auth-and-api.md +34 -0
- package/docs/uk/13-deployment.md +39 -0
- package/docs/uk/14-testing.md +34 -0
- package/docs/uk/15-error-handling.md +32 -0
- package/docs/uk/16-bake-cookbook.md +36 -0
- package/docs/uk/README.md +7 -0
- package/llms.txt +9 -1
- package/package.json +3 -1
- package/scripts/_config.mjs +224 -0
- package/scripts/bake.mjs +217 -120
- package/scripts/bundle.mjs +110 -67
- package/scripts/cli.mjs +119 -15
- package/scripts/preview.mjs +22 -12
- package/server/serve.mjs +82 -4
- package/starters/admin/README.md +63 -0
- package/starters/admin/index.html +21 -0
- package/starters/admin/mado.config.json +22 -0
- package/starters/admin/package.json +22 -0
- package/starters/admin/public/favicon.svg +4 -0
- package/starters/admin/src/components/x-button.ts +55 -0
- package/starters/admin/src/components/x-input.ts +74 -0
- package/starters/admin/src/layouts/app.ts +101 -0
- package/starters/admin/src/layouts/auth.ts +41 -0
- package/starters/admin/src/lib/api.ts +133 -0
- package/starters/admin/src/lib/auth.ts +83 -0
- package/starters/admin/src/main.ts +15 -0
- package/starters/admin/src/pages/admin/dashboard.ts +48 -0
- package/starters/admin/src/pages/admin/order-detail.ts +78 -0
- package/starters/admin/src/pages/admin/orders.ts +117 -0
- package/starters/admin/src/pages/home.ts +25 -0
- package/starters/admin/src/pages/login.ts +70 -0
- package/starters/admin/src/pages/not-found.ts +12 -0
- package/starters/admin/src/routes.ts +40 -0
- package/starters/admin/src/styles/global.ts +86 -0
- package/starters/admin/tsconfig.json +15 -0
- package/starters/crud/mado.config.json +20 -0
- package/starters/crud/package.json +8 -4
- package/starters/crud/src/routes.ts +4 -2
- package/starters/minimal/mado.config.json +20 -0
- package/starters/minimal/package.json +7 -3
- package/starters/minimal/src/routes.ts +4 -2
package/scripts/bake.mjs
CHANGED
|
@@ -1,36 +1,83 @@
|
|
|
1
|
-
// Smart Static: bake HTML for pages
|
|
1
|
+
// Smart Static: bake HTML for pages whose `page({ bake })` is set.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// mado bake
|
|
5
|
+
// mado bake --entry src/routes.ts --template index.html --out out/baked
|
|
6
|
+
// mado bake --base-url https://example.com
|
|
7
|
+
//
|
|
8
|
+
// Configuration precedence (low → high):
|
|
9
|
+
// built-in defaults < mado.config.json (bake.*) < CLI flags < env vars
|
|
5
10
|
//
|
|
6
11
|
// What it does:
|
|
7
|
-
// 1.
|
|
12
|
+
// 1. Bundles `entry` (routes module) with esbuild for Node consumption.
|
|
8
13
|
// 2. For every route whose page has `bake`:
|
|
9
|
-
// a) gets params
|
|
10
|
-
// b) gets data
|
|
11
|
-
// c) renders TemplateResult
|
|
12
|
-
// d)
|
|
13
|
-
// e)
|
|
14
|
-
// f) writes out
|
|
15
|
-
// 3. Generates out
|
|
14
|
+
// a) gets params via `bake.paths()`,
|
|
15
|
+
// b) gets data per params via `bake.data(params)`,
|
|
16
|
+
// c) renders the TemplateResult to an HTML string (no browser),
|
|
17
|
+
// d) materializes head() into <meta>/<link>/<script type=json-ld>,
|
|
18
|
+
// e) inlines baked data in <script id="bake" type="application/json">,
|
|
19
|
+
// f) writes <out>/<path>/index.html.
|
|
20
|
+
// 3. Generates <out>/sitemap.xml.
|
|
16
21
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
22
|
+
// Context awareness:
|
|
23
|
+
// - In app-mode (default outside the framework repository) the bundler
|
|
24
|
+
// resolves `@madojs/mado` from node_modules normally.
|
|
25
|
+
// - In repo-mode (the framework repository itself) it aliases
|
|
26
|
+
// `@madojs/mado` → ./src/index.ts so the framework can dogfood itself.
|
|
19
27
|
//
|
|
20
|
-
//
|
|
21
|
-
// connectedCallback; it only expands TemplateResult structures into HTML.
|
|
22
|
-
// Web Components come alive on the client.
|
|
28
|
+
// Required dev deps: linkedom, esbuild. We print a clear error if missing.
|
|
23
29
|
|
|
24
|
-
import { readFile, writeFile, mkdir,
|
|
30
|
+
import { readFile, writeFile, mkdir, rm } from "node:fs/promises";
|
|
31
|
+
import { existsSync, writeSync } from "node:fs";
|
|
25
32
|
import { join, dirname, resolve } from "node:path";
|
|
26
33
|
import { pathToFileURL } from "node:url";
|
|
27
34
|
import { tmpdir } from "node:os";
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
import { loadConfig, parseFlags, resolveProjectPath } from "./_config.mjs";
|
|
37
|
+
|
|
38
|
+
// ---------- Resolve options from config + flags + env ----------
|
|
39
|
+
|
|
40
|
+
const { flags } = parseFlags(process.argv.slice(2));
|
|
41
|
+
const cfg = loadConfig({
|
|
42
|
+
overrides: {
|
|
43
|
+
bake: {
|
|
44
|
+
entry: typeof flags.entry === "string" ? flags.entry : undefined,
|
|
45
|
+
template: typeof flags.template === "string" ? flags.template : undefined,
|
|
46
|
+
baseUrl: typeof flags["base-url"] === "string" ? flags["base-url"] : undefined,
|
|
47
|
+
outDir: typeof flags.out === "string" ? flags.out : undefined,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Env vars are legacy escape hatches (kept so old CI keeps working).
|
|
53
|
+
const ENTRY = process.env.ENTRY ?? resolveProjectPath(cfg, cfg.bake.entry);
|
|
54
|
+
const TEMPLATE = process.env.TEMPLATE ?? resolveProjectPath(cfg, cfg.bake.template);
|
|
55
|
+
const BASE_URL = process.env.BASE_URL ?? cfg.bake.baseUrl;
|
|
56
|
+
const OUT_DIR = process.env.OUT_DIR
|
|
57
|
+
?? resolveProjectPath(cfg, cfg.bake.outDir ?? join(cfg.build.out, "baked"));
|
|
30
58
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
/** Write message to stderr and exit. Sync write keeps CI/execFile output reliable. */
|
|
60
|
+
function fatal(...msgs) {
|
|
61
|
+
writeSync(2, msgs.join("\n") + "\n");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function error(...msgs) {
|
|
66
|
+
writeSync(2, msgs.join(" ") + "\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!existsSync(ENTRY)) {
|
|
70
|
+
fatal(
|
|
71
|
+
`[bake] entry not found: ${ENTRY}`,
|
|
72
|
+
`[bake] set bake.entry in mado.config.json or pass --entry <file>`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (!existsSync(TEMPLATE)) {
|
|
76
|
+
fatal(
|
|
77
|
+
`[bake] template not found: ${TEMPLATE}`,
|
|
78
|
+
`[bake] set bake.template in mado.config.json or pass --template <file>`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
34
81
|
|
|
35
82
|
// ---------- Optional dependencies ----------
|
|
36
83
|
|
|
@@ -38,61 +85,60 @@ let parseHTML;
|
|
|
38
85
|
try {
|
|
39
86
|
({ parseHTML } = await import("linkedom"));
|
|
40
87
|
} catch {
|
|
41
|
-
|
|
42
|
-
process.exit(1);
|
|
88
|
+
fatal("[bake] package 'linkedom' is required: npm i -D linkedom");
|
|
43
89
|
}
|
|
44
90
|
|
|
45
91
|
let esbuild;
|
|
46
92
|
try {
|
|
47
93
|
esbuild = await import("esbuild");
|
|
48
94
|
} catch {
|
|
49
|
-
|
|
50
|
-
process.exit(1);
|
|
95
|
+
fatal("[bake] package 'esbuild' is required: npm i -D esbuild");
|
|
51
96
|
}
|
|
52
97
|
|
|
53
98
|
// ---------- DOM polyfills for Node ----------
|
|
54
99
|
//
|
|
55
|
-
// router.ts/component.ts/css.ts touch window
|
|
56
|
-
//
|
|
57
|
-
//
|
|
100
|
+
// router.ts / component.ts / css.ts touch window/document/location/customElements
|
|
101
|
+
// at module top-level. Node has none, so install linkedom stubs before importing
|
|
102
|
+
// the app graph.
|
|
58
103
|
|
|
59
|
-
const baseHtml = await readFile(
|
|
60
|
-
() => "<!doctype html><html><head></head><body></body></html>",
|
|
61
|
-
);
|
|
104
|
+
const baseHtml = await readFile(TEMPLATE, "utf8");
|
|
62
105
|
const { window: linkedomWindow } = parseHTML(baseHtml);
|
|
63
106
|
|
|
64
107
|
globalThis.window = linkedomWindow;
|
|
65
108
|
globalThis.document = linkedomWindow.document;
|
|
66
109
|
globalThis.location = new URL("http://localhost/");
|
|
67
|
-
globalThis.history = {
|
|
68
|
-
pushState: () => {},
|
|
69
|
-
replaceState: () => {},
|
|
70
|
-
};
|
|
110
|
+
globalThis.history = { pushState: () => {}, replaceState: () => {} };
|
|
71
111
|
globalThis.customElements = {
|
|
72
112
|
define: () => {},
|
|
73
113
|
get: () => undefined,
|
|
74
114
|
whenDefined: () => Promise.resolve(),
|
|
75
115
|
};
|
|
76
116
|
globalThis.HTMLElement = linkedomWindow.HTMLElement ?? class {};
|
|
77
|
-
globalThis.CSSStyleSheet =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
replaceSync() {}
|
|
82
|
-
};
|
|
117
|
+
globalThis.CSSStyleSheet = globalThis.CSSStyleSheet ?? class {
|
|
118
|
+
cssRules = [];
|
|
119
|
+
replaceSync() {}
|
|
120
|
+
};
|
|
83
121
|
globalThis.matchMedia = () => ({
|
|
84
122
|
matches: false,
|
|
85
123
|
addEventListener: () => {},
|
|
86
124
|
removeEventListener: () => {},
|
|
87
125
|
});
|
|
88
|
-
if (!globalThis.queueMicrotask)
|
|
126
|
+
if (!globalThis.queueMicrotask) {
|
|
127
|
+
globalThis.queueMicrotask = (fn) => Promise.resolve().then(fn);
|
|
128
|
+
}
|
|
89
129
|
|
|
90
|
-
// ----------
|
|
130
|
+
// ---------- Bundle the routes module for Node ----------
|
|
91
131
|
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
132
|
+
// In repo-mode the framework dogfoods its own source; alias `@madojs/mado`
|
|
133
|
+
// to ./src/index.ts. In app-mode the package is resolved from node_modules.
|
|
94
134
|
|
|
95
135
|
const tmpFile = join(tmpdir(), `mado-bake-${Date.now()}.mjs`);
|
|
136
|
+
const aliases = cfg.context === "repo"
|
|
137
|
+
? { "@madojs/mado": resolve(cfg.projectRoot, "src/index.ts") }
|
|
138
|
+
: {};
|
|
139
|
+
|
|
140
|
+
const tsconfigCandidate = join(cfg.projectRoot, "tsconfig.json");
|
|
141
|
+
const tsconfig = existsSync(tsconfigCandidate) ? tsconfigCandidate : undefined;
|
|
96
142
|
|
|
97
143
|
await esbuild.build({
|
|
98
144
|
entryPoints: [ENTRY],
|
|
@@ -101,12 +147,9 @@ await esbuild.build({
|
|
|
101
147
|
platform: "node",
|
|
102
148
|
target: "es2022",
|
|
103
149
|
outfile: tmpFile,
|
|
104
|
-
|
|
105
|
-
tsconfig
|
|
106
|
-
|
|
107
|
-
alias: {
|
|
108
|
-
"@madojs/mado": resolve("src/index.ts"),
|
|
109
|
-
},
|
|
150
|
+
absWorkingDir: cfg.projectRoot,
|
|
151
|
+
tsconfig,
|
|
152
|
+
alias: aliases,
|
|
110
153
|
logLevel: "error",
|
|
111
154
|
});
|
|
112
155
|
|
|
@@ -116,51 +159,59 @@ await rm(tmpFile).catch(() => {});
|
|
|
116
159
|
const routeApi = routesModule.default;
|
|
117
160
|
|
|
118
161
|
if (!routeApi) {
|
|
119
|
-
|
|
120
|
-
process.exit(1);
|
|
162
|
+
fatal(`[bake] ${ENTRY} must default-export routes({...})`);
|
|
121
163
|
}
|
|
122
164
|
|
|
123
|
-
// Bake needs the source manifest
|
|
124
|
-
// routes.ts must also export
|
|
125
|
-
//
|
|
126
|
-
// The chosen convention: routes.ts exports both `default` (RouterApi) and
|
|
127
|
-
// `manifest` (the source object). See examples/routes.ts.
|
|
128
|
-
|
|
165
|
+
// Bake needs the source manifest (not the runtime RouterApi).
|
|
166
|
+
// routes.ts must therefore also `export const manifest = {...}`.
|
|
129
167
|
const manifest = routesModule.manifest;
|
|
130
168
|
if (!manifest) {
|
|
131
|
-
|
|
132
|
-
|
|
169
|
+
fatal(
|
|
170
|
+
`[bake] ${ENTRY} must also \`export const manifest = {...}\` ` +
|
|
133
171
|
"(the same object passed to routes()).",
|
|
134
172
|
);
|
|
135
|
-
process.exit(1);
|
|
136
173
|
}
|
|
137
174
|
|
|
138
175
|
// ---------- Main loop ----------
|
|
139
176
|
|
|
140
177
|
await mkdir(OUT_DIR, { recursive: true });
|
|
141
178
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
const TEMPLATE_HTML =
|
|
179
|
+
// Re-read template (already loaded for DOM polyfills, but we want a fresh copy
|
|
180
|
+
// per generated page so meta/link tags don't accumulate across iterations).
|
|
181
|
+
const TEMPLATE_HTML = baseHtml;
|
|
145
182
|
|
|
146
183
|
const sitemapEntries = [];
|
|
147
184
|
let total = 0;
|
|
185
|
+
let bakedErrors = 0;
|
|
148
186
|
|
|
149
187
|
for (const [pattern, entry] of Object.entries(manifest)) {
|
|
150
188
|
if (pattern === "*") continue;
|
|
151
189
|
|
|
152
|
-
// entry can be Page, () => import, or nested. Bake currently handles direct
|
|
153
|
-
// lazy imports and Page entries.
|
|
154
190
|
const pg = await resolvePage(entry);
|
|
155
191
|
if (!pg) continue;
|
|
156
192
|
if (!pg.bake) continue;
|
|
157
193
|
|
|
158
194
|
console.log(`[bake] ${pattern}`);
|
|
159
195
|
|
|
160
|
-
|
|
196
|
+
let allParams;
|
|
197
|
+
try {
|
|
198
|
+
allParams = await pg.bake.paths();
|
|
199
|
+
} catch (err) {
|
|
200
|
+
error(`[bake] ${pattern}: bake.paths() failed:`, err.message);
|
|
201
|
+
bakedErrors++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
161
205
|
for (const params of allParams) {
|
|
162
206
|
const pathname = applyParams(pattern, params);
|
|
163
|
-
|
|
207
|
+
let data;
|
|
208
|
+
try {
|
|
209
|
+
data = await pg.bake.data(params);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
error(`[bake] ${pathname}: bake.data() failed:`, err.message);
|
|
212
|
+
bakedErrors++;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
164
215
|
const headMeta = pg.head ? pg.head(params, data) : {};
|
|
165
216
|
|
|
166
217
|
const tpl = pg.view({
|
|
@@ -170,7 +221,15 @@ for (const [pattern, entry] of Object.entries(manifest)) {
|
|
|
170
221
|
child: null,
|
|
171
222
|
});
|
|
172
223
|
|
|
173
|
-
|
|
224
|
+
let bodyHtml;
|
|
225
|
+
try {
|
|
226
|
+
bodyHtml = renderTemplate(tpl, { route: pattern });
|
|
227
|
+
} catch (err) {
|
|
228
|
+
error(`[bake] ${pathname}: render failed:`, err.message);
|
|
229
|
+
bakedErrors++;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
174
233
|
const finalHtml = buildHtml({
|
|
175
234
|
template: TEMPLATE_HTML,
|
|
176
235
|
bodyHtml,
|
|
@@ -180,15 +239,14 @@ for (const [pattern, entry] of Object.entries(manifest)) {
|
|
|
180
239
|
canonical: headMeta.canonical ?? `${BASE_URL}${pathname}`,
|
|
181
240
|
});
|
|
182
241
|
|
|
183
|
-
const file = join(
|
|
242
|
+
const file = join(
|
|
243
|
+
OUT_DIR,
|
|
244
|
+
pathname === "/" ? "/index.html" : `${pathname}/index.html`,
|
|
245
|
+
);
|
|
184
246
|
await mkdir(dirname(file), { recursive: true });
|
|
185
247
|
await writeFile(file, finalHtml);
|
|
186
248
|
total++;
|
|
187
|
-
|
|
188
|
-
sitemapEntries.push({
|
|
189
|
-
loc: `${BASE_URL}${pathname}`,
|
|
190
|
-
changefreq: "weekly",
|
|
191
|
-
});
|
|
249
|
+
sitemapEntries.push({ loc: `${BASE_URL}${pathname}`, changefreq: "weekly" });
|
|
192
250
|
}
|
|
193
251
|
}
|
|
194
252
|
|
|
@@ -206,7 +264,10 @@ ${sitemapEntries
|
|
|
206
264
|
`;
|
|
207
265
|
await writeFile(join(OUT_DIR, "sitemap.xml"), sitemap);
|
|
208
266
|
|
|
209
|
-
console.log(`[bake] done: ${total} pages + sitemap.xml`);
|
|
267
|
+
console.log(`[bake] done: ${total} pages + sitemap.xml → ${OUT_DIR}`);
|
|
268
|
+
if (bakedErrors > 0) {
|
|
269
|
+
fatal(`[bake] ${bakedErrors} route(s) failed; see errors above.`);
|
|
270
|
+
}
|
|
210
271
|
|
|
211
272
|
// ---------- Helpers ----------
|
|
212
273
|
|
|
@@ -232,41 +293,44 @@ function applyParams(pattern, params) {
|
|
|
232
293
|
});
|
|
233
294
|
}
|
|
234
295
|
|
|
235
|
-
async function exists(p) {
|
|
236
|
-
try {
|
|
237
|
-
await access(p);
|
|
238
|
-
return true;
|
|
239
|
-
} catch {
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
296
|
// ---------- Render TemplateResult → HTML string ----------
|
|
245
297
|
//
|
|
246
|
-
// Tiny
|
|
247
|
-
//
|
|
248
|
-
//
|
|
298
|
+
// Tiny SSR for TemplateResult. Supports the same shapes as html.ts but
|
|
299
|
+
// without events (@click is stripped) and without live signals (function
|
|
300
|
+
// values are called once).
|
|
249
301
|
|
|
250
|
-
function renderTemplate(tpl) {
|
|
302
|
+
function renderTemplate(tpl, ctx) {
|
|
251
303
|
if (tpl == null || tpl === false || tpl === true) return "";
|
|
252
304
|
if (typeof tpl === "string") return escapeHtml(tpl);
|
|
253
305
|
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
|
-
//
|
|
306
|
+
if (Array.isArray(tpl)) return tpl.map((x) => renderTemplate(x, ctx)).join("");
|
|
307
|
+
if (tpl && tpl._mado === true) return renderMadoTemplate(tpl, ctx);
|
|
308
|
+
// Unknown shapes (e.g. each() directive results) must NOT silently render
|
|
309
|
+
// as "[object Object]". Either each() unwraps to an array here, or we
|
|
310
|
+
// throw with a meaningful location.
|
|
311
|
+
if (tpl && typeof tpl === "object") {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`bake cannot render value of type "${tpl?._type ?? tpl?.constructor?.name ?? "object"}" ` +
|
|
314
|
+
`in route ${ctx?.route ?? "?"}. ` +
|
|
315
|
+
"Hint: each() and other directives are not yet supported in bake. " +
|
|
316
|
+
"Use a plain array (items.map(render)) in baked views, or render this " +
|
|
317
|
+
"section only on the client.",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
257
320
|
return "";
|
|
258
321
|
}
|
|
259
322
|
|
|
260
|
-
function renderMadoTemplate(tpl) {
|
|
323
|
+
function renderMadoTemplate(tpl, ctx) {
|
|
261
324
|
const { strings, values } = tpl;
|
|
262
325
|
let html = "";
|
|
263
326
|
for (let i = 0; i < strings.length; i++) {
|
|
264
327
|
html += strings[i];
|
|
265
328
|
if (i < strings.length - 1) {
|
|
266
|
-
html += renderValue(values[i], inAttributeContext(html));
|
|
329
|
+
html += renderValue(values[i], inAttributeContext(html), ctx);
|
|
267
330
|
}
|
|
268
331
|
}
|
|
269
|
-
//
|
|
332
|
+
// Strip event markers and normalize property / boolean-attribute forms
|
|
333
|
+
// (mirrors the runtime bindings in src/html/bindings.ts).
|
|
270
334
|
return html
|
|
271
335
|
.replace(/\s+@[\w-]+="[^"]*"/g, "")
|
|
272
336
|
.replace(/\s+\.([\w-]+)="([^"]*)"/g, ' $1="$2"')
|
|
@@ -280,22 +344,71 @@ function inAttributeContext(html) {
|
|
|
280
344
|
return lastOpen > lastClose;
|
|
281
345
|
}
|
|
282
346
|
|
|
283
|
-
function renderValue(v, inAttr) {
|
|
347
|
+
function renderValue(v, inAttr, ctx) {
|
|
284
348
|
if (v == null || v === false) return "";
|
|
285
349
|
if (v === true) return "";
|
|
286
350
|
if (typeof v === "function") {
|
|
287
351
|
try {
|
|
288
|
-
return renderValue(v(), inAttr);
|
|
352
|
+
return renderValue(v(), inAttr, ctx);
|
|
289
353
|
} catch {
|
|
290
354
|
return "";
|
|
291
355
|
}
|
|
292
356
|
}
|
|
293
|
-
if (Array.isArray(v)) return v.map((x) => renderValue(x, inAttr)).join("");
|
|
294
|
-
if (v && v._mado === true) return renderMadoTemplate(v);
|
|
357
|
+
if (Array.isArray(v)) return v.map((x) => renderValue(x, inAttr, ctx)).join("");
|
|
358
|
+
if (v && v._mado === true) return renderMadoTemplate(v, ctx);
|
|
359
|
+
if (v && typeof v === "object" && typeof v._madoDirective === "string") {
|
|
360
|
+
return renderDirective(v, inAttr, ctx);
|
|
361
|
+
}
|
|
362
|
+
if (v && typeof v === "object") {
|
|
363
|
+
// Same defense as renderTemplate(): never silently coerce to "[object Object]".
|
|
364
|
+
throw new Error(
|
|
365
|
+
`bake cannot serialize value of type "${v?._type ?? v?.constructor?.name ?? "object"}" ` +
|
|
366
|
+
`in route ${ctx?.route ?? "?"}. ` +
|
|
367
|
+
"Hint: each() is not yet supported in bake. Use a plain array in baked views.",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
295
370
|
if (inAttr) return escapeAttr(String(v));
|
|
296
371
|
return escapeHtml(String(v));
|
|
297
372
|
}
|
|
298
373
|
|
|
374
|
+
function renderDirective(v, inAttr, ctx) {
|
|
375
|
+
switch (v._madoDirective) {
|
|
376
|
+
case "unsafeHTML": {
|
|
377
|
+
if (inAttr) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
`bake cannot render unsafeHTML() inside an attribute in route ${ctx?.route ?? "?"}.`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return String(v.value ?? "");
|
|
383
|
+
}
|
|
384
|
+
case "classMap": {
|
|
385
|
+
const value = Object.entries(v.value ?? {})
|
|
386
|
+
.filter(([, enabled]) => !!enabled)
|
|
387
|
+
.map(([className]) => className)
|
|
388
|
+
.join(" ");
|
|
389
|
+
return inAttr ? escapeAttr(value) : escapeHtml(value);
|
|
390
|
+
}
|
|
391
|
+
case "styleMap": {
|
|
392
|
+
const value = Object.entries(v.value ?? {})
|
|
393
|
+
.filter(([, raw]) => raw != null && raw !== false)
|
|
394
|
+
.map(([name, raw]) => `${toCssPropertyName(name)}:${String(raw)}`)
|
|
395
|
+
.join(";");
|
|
396
|
+
return inAttr ? escapeAttr(value) : escapeHtml(value);
|
|
397
|
+
}
|
|
398
|
+
case "ref":
|
|
399
|
+
return "";
|
|
400
|
+
default:
|
|
401
|
+
throw new Error(
|
|
402
|
+
`bake cannot render directive "${v._madoDirective}" in route ${ctx?.route ?? "?"}.`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function toCssPropertyName(name) {
|
|
408
|
+
if (name.startsWith("--")) return name;
|
|
409
|
+
return name.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
299
412
|
function escapeHtml(s) {
|
|
300
413
|
return s
|
|
301
414
|
.replace(/&/g, "&")
|
|
@@ -318,7 +431,6 @@ function escapeXml(s) {
|
|
|
318
431
|
function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical }) {
|
|
319
432
|
const { document } = parseHTML(template);
|
|
320
433
|
|
|
321
|
-
// head
|
|
322
434
|
if (head.title) document.title = head.title;
|
|
323
435
|
if (head.description) {
|
|
324
436
|
setMeta(document, { name: "description", content: head.description });
|
|
@@ -335,10 +447,7 @@ function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical
|
|
|
335
447
|
if (head.twitter || head.og) {
|
|
336
448
|
const tw = head.twitter ?? {};
|
|
337
449
|
const og = head.og ?? {};
|
|
338
|
-
setMeta(document, {
|
|
339
|
-
name: "twitter:card",
|
|
340
|
-
content: tw.card ?? "summary",
|
|
341
|
-
});
|
|
450
|
+
setMeta(document, { name: "twitter:card", content: tw.card ?? "summary" });
|
|
342
451
|
if (tw.title ?? og.title)
|
|
343
452
|
setMeta(document, { name: "twitter:title", content: tw.title ?? og.title });
|
|
344
453
|
if (tw.description ?? og.description)
|
|
@@ -349,7 +458,6 @@ function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical
|
|
|
349
458
|
for (const m of head.meta ?? []) setMeta(document, m);
|
|
350
459
|
for (const l of head.link ?? []) setLink(document, l);
|
|
351
460
|
|
|
352
|
-
// JSON-LD
|
|
353
461
|
if (head.jsonLd != null) {
|
|
354
462
|
const s = document.createElement("script");
|
|
355
463
|
s.setAttribute("type", "application/ld+json");
|
|
@@ -358,19 +466,11 @@ function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical
|
|
|
358
466
|
document.head.appendChild(s);
|
|
359
467
|
}
|
|
360
468
|
|
|
361
|
-
// revalidate meta: for CDN or manual CI re-bake logic
|
|
362
469
|
if (revalidate) {
|
|
363
|
-
setMeta(document, {
|
|
364
|
-
|
|
365
|
-
content: String(revalidate),
|
|
366
|
-
});
|
|
367
|
-
setMeta(document, {
|
|
368
|
-
name: "bake-stamp",
|
|
369
|
-
content: String(Date.now()),
|
|
370
|
-
});
|
|
470
|
+
setMeta(document, { name: "bake-revalidate", content: String(revalidate) });
|
|
471
|
+
setMeta(document, { name: "bake-stamp", content: String(Date.now()) });
|
|
371
472
|
}
|
|
372
473
|
|
|
373
|
-
// Baked data: the client can use it as initialData.
|
|
374
474
|
if (bakedData !== undefined) {
|
|
375
475
|
const s = document.createElement("script");
|
|
376
476
|
s.setAttribute("type", "application/json");
|
|
@@ -379,11 +479,8 @@ function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical
|
|
|
379
479
|
document.body.appendChild(s);
|
|
380
480
|
}
|
|
381
481
|
|
|
382
|
-
// body: insert baked HTML inside #app.
|
|
383
482
|
const app = document.getElementById("app");
|
|
384
|
-
if (app)
|
|
385
|
-
app.innerHTML = bodyHtml;
|
|
386
|
-
}
|
|
483
|
+
if (app) app.innerHTML = bodyHtml;
|
|
387
484
|
|
|
388
485
|
return "<!doctype html>\n" + document.documentElement.outerHTML;
|
|
389
486
|
}
|