@madojs/mado 0.5.1 → 0.6.1
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 +265 -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/resource.js +11 -0
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/manifest.d.ts +16 -1
- package/dist/src/router/manifest.js +210 -40
- 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 +266 -121
- package/scripts/bundle.mjs +133 -67
- package/scripts/cli.mjs +195 -27
- package/scripts/preview.mjs +125 -21
- package/server/serve.mjs +161 -10
- package/starters/admin/README.md +63 -0
- package/starters/admin/index.html +28 -0
- package/starters/admin/mado.config.json +22 -0
- package/starters/admin/package.json +24 -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 +80 -0
- package/starters/admin/src/pages/admin/orders.ts +117 -0
- package/starters/admin/src/pages/home.ts +34 -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/index.html +12 -4
- package/starters/crud/mado.config.json +20 -0
- package/starters/crud/package.json +9 -3
- package/starters/crud/src/pages/home.ts +16 -0
- package/starters/crud/src/routes.ts +4 -2
- package/starters/minimal/index.html +12 -4
- package/starters/minimal/mado.config.json +20 -0
- package/starters/minimal/package.json +9 -3
- package/starters/minimal/src/pages/home.ts +17 -0
- 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
|
+
});
|
|
30
51
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
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"));
|
|
58
|
+
|
|
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,72 @@ let parseHTML;
|
|
|
38
85
|
try {
|
|
39
86
|
({ parseHTML } = await import("linkedom"));
|
|
40
87
|
} catch {
|
|
41
|
-
|
|
42
|
-
|
|
88
|
+
fatal(
|
|
89
|
+
"[bake] package 'linkedom' is required.",
|
|
90
|
+
"[bake] Install it as a dev dependency in this project:",
|
|
91
|
+
"[bake] npm i -D linkedom esbuild",
|
|
92
|
+
"[bake] (esbuild is also required, see next check).",
|
|
93
|
+
"[bake] These are not bundled into @madojs/mado on purpose: bake is an",
|
|
94
|
+
"[bake] optional build step and we don't want to add transitive deps to",
|
|
95
|
+
"[bake] every Mado install.",
|
|
96
|
+
);
|
|
43
97
|
}
|
|
44
98
|
|
|
45
99
|
let esbuild;
|
|
46
100
|
try {
|
|
47
101
|
esbuild = await import("esbuild");
|
|
48
102
|
} catch {
|
|
49
|
-
|
|
50
|
-
|
|
103
|
+
fatal(
|
|
104
|
+
"[bake] package 'esbuild' is required.",
|
|
105
|
+
"[bake] Install it as a dev dependency in this project:",
|
|
106
|
+
"[bake] npm i -D esbuild linkedom",
|
|
107
|
+
);
|
|
51
108
|
}
|
|
52
109
|
|
|
53
110
|
// ---------- DOM polyfills for Node ----------
|
|
54
111
|
//
|
|
55
|
-
// router.ts/component.ts/css.ts touch window
|
|
56
|
-
//
|
|
57
|
-
//
|
|
112
|
+
// router.ts / component.ts / css.ts touch window/document/location/customElements
|
|
113
|
+
// at module top-level. Node has none, so install linkedom stubs before importing
|
|
114
|
+
// the app graph.
|
|
58
115
|
|
|
59
|
-
const baseHtml = await readFile(
|
|
60
|
-
() => "<!doctype html><html><head></head><body></body></html>",
|
|
61
|
-
);
|
|
116
|
+
const baseHtml = await readFile(TEMPLATE, "utf8");
|
|
62
117
|
const { window: linkedomWindow } = parseHTML(baseHtml);
|
|
63
118
|
|
|
64
119
|
globalThis.window = linkedomWindow;
|
|
65
120
|
globalThis.document = linkedomWindow.document;
|
|
66
121
|
globalThis.location = new URL("http://localhost/");
|
|
67
|
-
globalThis.history = {
|
|
68
|
-
pushState: () => {},
|
|
69
|
-
replaceState: () => {},
|
|
70
|
-
};
|
|
122
|
+
globalThis.history = { pushState: () => {}, replaceState: () => {} };
|
|
71
123
|
globalThis.customElements = {
|
|
72
124
|
define: () => {},
|
|
73
125
|
get: () => undefined,
|
|
74
126
|
whenDefined: () => Promise.resolve(),
|
|
75
127
|
};
|
|
76
128
|
globalThis.HTMLElement = linkedomWindow.HTMLElement ?? class {};
|
|
77
|
-
globalThis.CSSStyleSheet =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
replaceSync() {}
|
|
82
|
-
};
|
|
129
|
+
globalThis.CSSStyleSheet = globalThis.CSSStyleSheet ?? class {
|
|
130
|
+
cssRules = [];
|
|
131
|
+
replaceSync() {}
|
|
132
|
+
};
|
|
83
133
|
globalThis.matchMedia = () => ({
|
|
84
134
|
matches: false,
|
|
85
135
|
addEventListener: () => {},
|
|
86
136
|
removeEventListener: () => {},
|
|
87
137
|
});
|
|
88
|
-
if (!globalThis.queueMicrotask)
|
|
138
|
+
if (!globalThis.queueMicrotask) {
|
|
139
|
+
globalThis.queueMicrotask = (fn) => Promise.resolve().then(fn);
|
|
140
|
+
}
|
|
89
141
|
|
|
90
|
-
// ----------
|
|
142
|
+
// ---------- Bundle the routes module for Node ----------
|
|
91
143
|
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
144
|
+
// In repo-mode the framework dogfoods its own source; alias `@madojs/mado`
|
|
145
|
+
// to ./src/index.ts. In app-mode the package is resolved from node_modules.
|
|
94
146
|
|
|
95
147
|
const tmpFile = join(tmpdir(), `mado-bake-${Date.now()}.mjs`);
|
|
148
|
+
const aliases = cfg.context === "repo"
|
|
149
|
+
? { "@madojs/mado": resolve(cfg.projectRoot, "src/index.ts") }
|
|
150
|
+
: {};
|
|
151
|
+
|
|
152
|
+
const tsconfigCandidate = join(cfg.projectRoot, "tsconfig.json");
|
|
153
|
+
const tsconfig = existsSync(tsconfigCandidate) ? tsconfigCandidate : undefined;
|
|
96
154
|
|
|
97
155
|
await esbuild.build({
|
|
98
156
|
entryPoints: [ENTRY],
|
|
@@ -101,12 +159,9 @@ await esbuild.build({
|
|
|
101
159
|
platform: "node",
|
|
102
160
|
target: "es2022",
|
|
103
161
|
outfile: tmpFile,
|
|
104
|
-
|
|
105
|
-
tsconfig
|
|
106
|
-
|
|
107
|
-
alias: {
|
|
108
|
-
"@madojs/mado": resolve("src/index.ts"),
|
|
109
|
-
},
|
|
162
|
+
absWorkingDir: cfg.projectRoot,
|
|
163
|
+
tsconfig,
|
|
164
|
+
alias: aliases,
|
|
110
165
|
logLevel: "error",
|
|
111
166
|
});
|
|
112
167
|
|
|
@@ -116,51 +171,65 @@ await rm(tmpFile).catch(() => {});
|
|
|
116
171
|
const routeApi = routesModule.default;
|
|
117
172
|
|
|
118
173
|
if (!routeApi) {
|
|
119
|
-
|
|
120
|
-
process.exit(1);
|
|
174
|
+
fatal(`[bake] ${ENTRY} must default-export routes({...})`);
|
|
121
175
|
}
|
|
122
176
|
|
|
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
|
-
|
|
177
|
+
// Bake needs the source manifest (not the runtime RouterApi).
|
|
178
|
+
// routes.ts must therefore also `export const manifest = {...}`.
|
|
129
179
|
const manifest = routesModule.manifest;
|
|
130
180
|
if (!manifest) {
|
|
131
|
-
|
|
132
|
-
|
|
181
|
+
fatal(
|
|
182
|
+
`[bake] ${ENTRY} must also \`export const manifest = {...}\` ` +
|
|
133
183
|
"(the same object passed to routes()).",
|
|
134
184
|
);
|
|
135
|
-
process.exit(1);
|
|
136
185
|
}
|
|
137
186
|
|
|
138
187
|
// ---------- Main loop ----------
|
|
139
188
|
|
|
140
189
|
await mkdir(OUT_DIR, { recursive: true });
|
|
141
190
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
const TEMPLATE_HTML =
|
|
191
|
+
// Re-read template (already loaded for DOM polyfills, but we want a fresh copy
|
|
192
|
+
// per generated page so meta/link tags don't accumulate across iterations).
|
|
193
|
+
const TEMPLATE_HTML = baseHtml;
|
|
145
194
|
|
|
146
195
|
const sitemapEntries = [];
|
|
147
196
|
let total = 0;
|
|
197
|
+
let bakedErrors = 0;
|
|
198
|
+
let bakeablePages = 0;
|
|
199
|
+
const skippedNoBake = [];
|
|
148
200
|
|
|
149
201
|
for (const [pattern, entry] of Object.entries(manifest)) {
|
|
150
202
|
if (pattern === "*") continue;
|
|
151
203
|
|
|
152
|
-
// entry can be Page, () => import, or nested. Bake currently handles direct
|
|
153
|
-
// lazy imports and Page entries.
|
|
154
204
|
const pg = await resolvePage(entry);
|
|
155
205
|
if (!pg) continue;
|
|
156
|
-
if (!pg.bake)
|
|
206
|
+
if (!pg.bake) {
|
|
207
|
+
skippedNoBake.push(pattern);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
bakeablePages++;
|
|
157
211
|
|
|
158
212
|
console.log(`[bake] ${pattern}`);
|
|
159
213
|
|
|
160
|
-
|
|
214
|
+
let allParams;
|
|
215
|
+
try {
|
|
216
|
+
allParams = await pg.bake.paths();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
error(`[bake] ${pattern}: bake.paths() failed:`, err.message);
|
|
219
|
+
bakedErrors++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
161
223
|
for (const params of allParams) {
|
|
162
224
|
const pathname = applyParams(pattern, params);
|
|
163
|
-
|
|
225
|
+
let data;
|
|
226
|
+
try {
|
|
227
|
+
data = await pg.bake.data(params);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
error(`[bake] ${pathname}: bake.data() failed:`, err.message);
|
|
230
|
+
bakedErrors++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
164
233
|
const headMeta = pg.head ? pg.head(params, data) : {};
|
|
165
234
|
|
|
166
235
|
const tpl = pg.view({
|
|
@@ -170,7 +239,15 @@ for (const [pattern, entry] of Object.entries(manifest)) {
|
|
|
170
239
|
child: null,
|
|
171
240
|
});
|
|
172
241
|
|
|
173
|
-
|
|
242
|
+
let bodyHtml;
|
|
243
|
+
try {
|
|
244
|
+
bodyHtml = renderTemplate(tpl, { route: pattern });
|
|
245
|
+
} catch (err) {
|
|
246
|
+
error(`[bake] ${pathname}: render failed:`, err.message);
|
|
247
|
+
bakedErrors++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
174
251
|
const finalHtml = buildHtml({
|
|
175
252
|
template: TEMPLATE_HTML,
|
|
176
253
|
bodyHtml,
|
|
@@ -180,15 +257,14 @@ for (const [pattern, entry] of Object.entries(manifest)) {
|
|
|
180
257
|
canonical: headMeta.canonical ?? `${BASE_URL}${pathname}`,
|
|
181
258
|
});
|
|
182
259
|
|
|
183
|
-
const file = join(
|
|
260
|
+
const file = join(
|
|
261
|
+
OUT_DIR,
|
|
262
|
+
pathname === "/" ? "/index.html" : `${pathname}/index.html`,
|
|
263
|
+
);
|
|
184
264
|
await mkdir(dirname(file), { recursive: true });
|
|
185
265
|
await writeFile(file, finalHtml);
|
|
186
266
|
total++;
|
|
187
|
-
|
|
188
|
-
sitemapEntries.push({
|
|
189
|
-
loc: `${BASE_URL}${pathname}`,
|
|
190
|
-
changefreq: "weekly",
|
|
191
|
-
});
|
|
267
|
+
sitemapEntries.push({ loc: `${BASE_URL}${pathname}`, changefreq: "weekly" });
|
|
192
268
|
}
|
|
193
269
|
}
|
|
194
270
|
|
|
@@ -206,7 +282,40 @@ ${sitemapEntries
|
|
|
206
282
|
`;
|
|
207
283
|
await writeFile(join(OUT_DIR, "sitemap.xml"), sitemap);
|
|
208
284
|
|
|
209
|
-
console.log(`[bake] done: ${total} pages + sitemap.xml`);
|
|
285
|
+
console.log(`[bake] done: ${total} pages + sitemap.xml → ${OUT_DIR}`);
|
|
286
|
+
if (bakedErrors > 0) {
|
|
287
|
+
fatal(`[bake] ${bakedErrors} route(s) failed; see errors above.`);
|
|
288
|
+
}
|
|
289
|
+
// Loud diagnostic when the manifest exists but no page declares `bake`.
|
|
290
|
+
// Previously bake silently produced 0 pages + an empty sitemap and exited
|
|
291
|
+
// 0, which made `mado release` look successful while shipping no static
|
|
292
|
+
// HTML for crawlers. Fail loudly so the user notices.
|
|
293
|
+
if (bakeablePages === 0) {
|
|
294
|
+
error("");
|
|
295
|
+
error(
|
|
296
|
+
`[bake] WARNING: no page in ${ENTRY} declares \`bake: { paths, data }\`.`,
|
|
297
|
+
);
|
|
298
|
+
error(
|
|
299
|
+
`[bake] ${skippedNoBake.length} route(s) skipped: ${skippedNoBake
|
|
300
|
+
.slice(0, 6)
|
|
301
|
+
.join(", ")}${skippedNoBake.length > 6 ? ", …" : ""}`,
|
|
302
|
+
);
|
|
303
|
+
error("[bake] Add `bake` to at least one page (e.g. your landing route):");
|
|
304
|
+
error("[bake] export default page({");
|
|
305
|
+
error("[bake] view: …,");
|
|
306
|
+
error("[bake] bake: { paths: () => [{}], data: () => ({}) },");
|
|
307
|
+
error("[bake] });");
|
|
308
|
+
error(
|
|
309
|
+
"[bake] Without bake the build ships only the SPA shell — search engines",
|
|
310
|
+
);
|
|
311
|
+
error("[bake] and link previews see an empty <body>.");
|
|
312
|
+
// Exit non-zero so `mado release` halts and the user is forced to address
|
|
313
|
+
// it. If you intentionally have an SPA-only deploy, drop `mado bake` from
|
|
314
|
+
// the release pipeline (or set MADO_BAKE_ALLOW_EMPTY=1).
|
|
315
|
+
if (process.env.MADO_BAKE_ALLOW_EMPTY !== "1") {
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
210
319
|
|
|
211
320
|
// ---------- Helpers ----------
|
|
212
321
|
|
|
@@ -232,41 +341,44 @@ function applyParams(pattern, params) {
|
|
|
232
341
|
});
|
|
233
342
|
}
|
|
234
343
|
|
|
235
|
-
async function exists(p) {
|
|
236
|
-
try {
|
|
237
|
-
await access(p);
|
|
238
|
-
return true;
|
|
239
|
-
} catch {
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
344
|
// ---------- Render TemplateResult → HTML string ----------
|
|
245
345
|
//
|
|
246
|
-
// Tiny
|
|
247
|
-
//
|
|
248
|
-
//
|
|
346
|
+
// Tiny SSR for TemplateResult. Supports the same shapes as html.ts but
|
|
347
|
+
// without events (@click is stripped) and without live signals (function
|
|
348
|
+
// values are called once).
|
|
249
349
|
|
|
250
|
-
function renderTemplate(tpl) {
|
|
350
|
+
function renderTemplate(tpl, ctx) {
|
|
251
351
|
if (tpl == null || tpl === false || tpl === true) return "";
|
|
252
352
|
if (typeof tpl === "string") return escapeHtml(tpl);
|
|
253
353
|
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
|
-
//
|
|
354
|
+
if (Array.isArray(tpl)) return tpl.map((x) => renderTemplate(x, ctx)).join("");
|
|
355
|
+
if (tpl && tpl._mado === true) return renderMadoTemplate(tpl, ctx);
|
|
356
|
+
// Unknown shapes (e.g. each() directive results) must NOT silently render
|
|
357
|
+
// as "[object Object]". Either each() unwraps to an array here, or we
|
|
358
|
+
// throw with a meaningful location.
|
|
359
|
+
if (tpl && typeof tpl === "object") {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`bake cannot render value of type "${tpl?._type ?? tpl?.constructor?.name ?? "object"}" ` +
|
|
362
|
+
`in route ${ctx?.route ?? "?"}. ` +
|
|
363
|
+
"Hint: each() and other directives are not yet supported in bake. " +
|
|
364
|
+
"Use a plain array (items.map(render)) in baked views, or render this " +
|
|
365
|
+
"section only on the client.",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
257
368
|
return "";
|
|
258
369
|
}
|
|
259
370
|
|
|
260
|
-
function renderMadoTemplate(tpl) {
|
|
371
|
+
function renderMadoTemplate(tpl, ctx) {
|
|
261
372
|
const { strings, values } = tpl;
|
|
262
373
|
let html = "";
|
|
263
374
|
for (let i = 0; i < strings.length; i++) {
|
|
264
375
|
html += strings[i];
|
|
265
376
|
if (i < strings.length - 1) {
|
|
266
|
-
html += renderValue(values[i], inAttributeContext(html));
|
|
377
|
+
html += renderValue(values[i], inAttributeContext(html), ctx);
|
|
267
378
|
}
|
|
268
379
|
}
|
|
269
|
-
//
|
|
380
|
+
// Strip event markers and normalize property / boolean-attribute forms
|
|
381
|
+
// (mirrors the runtime bindings in src/html/bindings.ts).
|
|
270
382
|
return html
|
|
271
383
|
.replace(/\s+@[\w-]+="[^"]*"/g, "")
|
|
272
384
|
.replace(/\s+\.([\w-]+)="([^"]*)"/g, ' $1="$2"')
|
|
@@ -280,22 +392,71 @@ function inAttributeContext(html) {
|
|
|
280
392
|
return lastOpen > lastClose;
|
|
281
393
|
}
|
|
282
394
|
|
|
283
|
-
function renderValue(v, inAttr) {
|
|
395
|
+
function renderValue(v, inAttr, ctx) {
|
|
284
396
|
if (v == null || v === false) return "";
|
|
285
397
|
if (v === true) return "";
|
|
286
398
|
if (typeof v === "function") {
|
|
287
399
|
try {
|
|
288
|
-
return renderValue(v(), inAttr);
|
|
400
|
+
return renderValue(v(), inAttr, ctx);
|
|
289
401
|
} catch {
|
|
290
402
|
return "";
|
|
291
403
|
}
|
|
292
404
|
}
|
|
293
|
-
if (Array.isArray(v)) return v.map((x) => renderValue(x, inAttr)).join("");
|
|
294
|
-
if (v && v._mado === true) return renderMadoTemplate(v);
|
|
405
|
+
if (Array.isArray(v)) return v.map((x) => renderValue(x, inAttr, ctx)).join("");
|
|
406
|
+
if (v && v._mado === true) return renderMadoTemplate(v, ctx);
|
|
407
|
+
if (v && typeof v === "object" && typeof v._madoDirective === "string") {
|
|
408
|
+
return renderDirective(v, inAttr, ctx);
|
|
409
|
+
}
|
|
410
|
+
if (v && typeof v === "object") {
|
|
411
|
+
// Same defense as renderTemplate(): never silently coerce to "[object Object]".
|
|
412
|
+
throw new Error(
|
|
413
|
+
`bake cannot serialize value of type "${v?._type ?? v?.constructor?.name ?? "object"}" ` +
|
|
414
|
+
`in route ${ctx?.route ?? "?"}. ` +
|
|
415
|
+
"Hint: each() is not yet supported in bake. Use a plain array in baked views.",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
295
418
|
if (inAttr) return escapeAttr(String(v));
|
|
296
419
|
return escapeHtml(String(v));
|
|
297
420
|
}
|
|
298
421
|
|
|
422
|
+
function renderDirective(v, inAttr, ctx) {
|
|
423
|
+
switch (v._madoDirective) {
|
|
424
|
+
case "unsafeHTML": {
|
|
425
|
+
if (inAttr) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`bake cannot render unsafeHTML() inside an attribute in route ${ctx?.route ?? "?"}.`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
return String(v.value ?? "");
|
|
431
|
+
}
|
|
432
|
+
case "classMap": {
|
|
433
|
+
const value = Object.entries(v.value ?? {})
|
|
434
|
+
.filter(([, enabled]) => !!enabled)
|
|
435
|
+
.map(([className]) => className)
|
|
436
|
+
.join(" ");
|
|
437
|
+
return inAttr ? escapeAttr(value) : escapeHtml(value);
|
|
438
|
+
}
|
|
439
|
+
case "styleMap": {
|
|
440
|
+
const value = Object.entries(v.value ?? {})
|
|
441
|
+
.filter(([, raw]) => raw != null && raw !== false)
|
|
442
|
+
.map(([name, raw]) => `${toCssPropertyName(name)}:${String(raw)}`)
|
|
443
|
+
.join(";");
|
|
444
|
+
return inAttr ? escapeAttr(value) : escapeHtml(value);
|
|
445
|
+
}
|
|
446
|
+
case "ref":
|
|
447
|
+
return "";
|
|
448
|
+
default:
|
|
449
|
+
throw new Error(
|
|
450
|
+
`bake cannot render directive "${v._madoDirective}" in route ${ctx?.route ?? "?"}.`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function toCssPropertyName(name) {
|
|
456
|
+
if (name.startsWith("--")) return name;
|
|
457
|
+
return name.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
299
460
|
function escapeHtml(s) {
|
|
300
461
|
return s
|
|
301
462
|
.replace(/&/g, "&")
|
|
@@ -318,7 +479,6 @@ function escapeXml(s) {
|
|
|
318
479
|
function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical }) {
|
|
319
480
|
const { document } = parseHTML(template);
|
|
320
481
|
|
|
321
|
-
// head
|
|
322
482
|
if (head.title) document.title = head.title;
|
|
323
483
|
if (head.description) {
|
|
324
484
|
setMeta(document, { name: "description", content: head.description });
|
|
@@ -335,10 +495,7 @@ function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical
|
|
|
335
495
|
if (head.twitter || head.og) {
|
|
336
496
|
const tw = head.twitter ?? {};
|
|
337
497
|
const og = head.og ?? {};
|
|
338
|
-
setMeta(document, {
|
|
339
|
-
name: "twitter:card",
|
|
340
|
-
content: tw.card ?? "summary",
|
|
341
|
-
});
|
|
498
|
+
setMeta(document, { name: "twitter:card", content: tw.card ?? "summary" });
|
|
342
499
|
if (tw.title ?? og.title)
|
|
343
500
|
setMeta(document, { name: "twitter:title", content: tw.title ?? og.title });
|
|
344
501
|
if (tw.description ?? og.description)
|
|
@@ -349,7 +506,6 @@ function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical
|
|
|
349
506
|
for (const m of head.meta ?? []) setMeta(document, m);
|
|
350
507
|
for (const l of head.link ?? []) setLink(document, l);
|
|
351
508
|
|
|
352
|
-
// JSON-LD
|
|
353
509
|
if (head.jsonLd != null) {
|
|
354
510
|
const s = document.createElement("script");
|
|
355
511
|
s.setAttribute("type", "application/ld+json");
|
|
@@ -358,19 +514,11 @@ function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical
|
|
|
358
514
|
document.head.appendChild(s);
|
|
359
515
|
}
|
|
360
516
|
|
|
361
|
-
// revalidate meta: for CDN or manual CI re-bake logic
|
|
362
517
|
if (revalidate) {
|
|
363
|
-
setMeta(document, {
|
|
364
|
-
|
|
365
|
-
content: String(revalidate),
|
|
366
|
-
});
|
|
367
|
-
setMeta(document, {
|
|
368
|
-
name: "bake-stamp",
|
|
369
|
-
content: String(Date.now()),
|
|
370
|
-
});
|
|
518
|
+
setMeta(document, { name: "bake-revalidate", content: String(revalidate) });
|
|
519
|
+
setMeta(document, { name: "bake-stamp", content: String(Date.now()) });
|
|
371
520
|
}
|
|
372
521
|
|
|
373
|
-
// Baked data: the client can use it as initialData.
|
|
374
522
|
if (bakedData !== undefined) {
|
|
375
523
|
const s = document.createElement("script");
|
|
376
524
|
s.setAttribute("type", "application/json");
|
|
@@ -379,11 +527,8 @@ function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical
|
|
|
379
527
|
document.body.appendChild(s);
|
|
380
528
|
}
|
|
381
529
|
|
|
382
|
-
// body: insert baked HTML inside #app.
|
|
383
530
|
const app = document.getElementById("app");
|
|
384
|
-
if (app)
|
|
385
|
-
app.innerHTML = bodyHtml;
|
|
386
|
-
}
|
|
531
|
+
if (app) app.innerHTML = bodyHtml;
|
|
387
532
|
|
|
388
533
|
return "<!doctype html>\n" + document.documentElement.outerHTML;
|
|
389
534
|
}
|