@madojs/mado 0.11.1 → 0.12.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.
Files changed (219) hide show
  1. package/AGENTS.md +102 -25
  2. package/CHANGELOG.md +158 -1
  3. package/README.md +135 -172
  4. package/dist/src/component.d.ts +3 -2
  5. package/dist/src/component.js +98 -6
  6. package/dist/src/component.js.map +1 -1
  7. package/dist/src/css.js +34 -5
  8. package/dist/src/css.js.map +1 -1
  9. package/dist/src/head.d.ts +6 -6
  10. package/dist/src/head.js +6 -6
  11. package/dist/src/html/parser.d.ts +1 -1
  12. package/dist/src/html/parser.js +1 -1
  13. package/dist/src/html/template.js +24 -10
  14. package/dist/src/html/template.js.map +1 -1
  15. package/dist/src/index.d.ts +8 -4
  16. package/dist/src/index.js +13 -3
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/page.d.ts +62 -28
  19. package/dist/src/page.js +5 -0
  20. package/dist/src/page.js.map +1 -1
  21. package/dist/src/resource.js +2 -1
  22. package/dist/src/resource.js.map +1 -1
  23. package/dist/src/router/base.d.ts +65 -0
  24. package/dist/src/router/base.js +168 -0
  25. package/dist/src/router/base.js.map +1 -0
  26. package/dist/src/router/manifest.js +75 -31
  27. package/dist/src/router/manifest.js.map +1 -1
  28. package/dist/src/router/match.d.ts +16 -2
  29. package/dist/src/router/match.js +41 -1
  30. package/dist/src/router/match.js.map +1 -1
  31. package/dist/src/router/navigation.js +58 -9
  32. package/dist/src/router/navigation.js.map +1 -1
  33. package/dist/src/static-runtime.d.ts +81 -0
  34. package/dist/src/static-runtime.js +209 -0
  35. package/dist/src/static-runtime.js.map +1 -0
  36. package/dist/src/vite/index.d.ts +31 -3
  37. package/dist/src/vite/index.js +49 -18
  38. package/dist/src/vite/index.js.map +1 -1
  39. package/docs/README.md +5 -8
  40. package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
  41. package/docs/en/00-the-mado-way.md +1 -1
  42. package/docs/en/01-quickstart.md +183 -0
  43. package/docs/en/10-pages-and-components.md +271 -0
  44. package/docs/en/11-templates-and-signals.md +211 -0
  45. package/docs/en/12-routing.md +229 -0
  46. package/docs/en/13-data.md +225 -0
  47. package/docs/en/14-forms.md +244 -0
  48. package/docs/en/15-static-snapshots.md +181 -0
  49. package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
  50. package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
  51. package/docs/en/{14-testing.md → 22-testing.md} +6 -5
  52. package/docs/en/23-cookbook.md +292 -0
  53. package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
  54. package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
  55. package/docs/en/40-llm-guide.md +776 -0
  56. package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
  57. package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
  58. package/docs/en/README.md +97 -27
  59. package/docs/recipes/nginx/Containerfile +26 -0
  60. package/docs/recipes/nginx/nginx.conf +42 -0
  61. package/llms.txt +246 -211
  62. package/package.json +17 -10
  63. package/scripts/bake.mjs +4 -569
  64. package/scripts/cli/generate.mjs +55 -5
  65. package/scripts/cli/help.mjs +11 -5
  66. package/scripts/cli/index.mjs +20 -4
  67. package/scripts/cli/init.mjs +7 -2
  68. package/scripts/cli/release.mjs +20 -68
  69. package/scripts/docs-lint.mjs +170 -0
  70. package/scripts/package-smoke.mjs +67 -9
  71. package/scripts/preview.mjs +95 -6
  72. package/scripts/size-budget.mjs +5 -1
  73. package/scripts/static/browser.mjs +654 -0
  74. package/scripts/static/discover.mjs +281 -0
  75. package/scripts/static/output.mjs +141 -0
  76. package/scripts/static/serialize.mjs +212 -0
  77. package/scripts/static/server.mjs +167 -0
  78. package/scripts/static.mjs +191 -0
  79. package/starters/default/README.md +57 -55
  80. package/starters/default/package.json +3 -9
  81. package/starters/default/src/app.routes.ts +20 -33
  82. package/starters/default/src/components/feature-card.component.ts +40 -0
  83. package/starters/default/src/components/live-counter.component.ts +57 -0
  84. package/starters/default/src/content/guides.ts +55 -0
  85. package/starters/default/src/main.ts +9 -13
  86. package/starters/default/src/pages/app.page.ts +41 -0
  87. package/starters/default/src/pages/guide.page.ts +40 -0
  88. package/starters/default/src/pages/home.page.ts +49 -0
  89. package/starters/default/src/pages/not-found.page.ts +18 -0
  90. package/starters/default/src/styles/document.css +39 -0
  91. package/starters/default/src/styles/reset.css +37 -0
  92. package/starters/default/src/styles/tokens.css +30 -0
  93. package/starters/default/src/styles.d.ts +3 -1
  94. package/starters/default/src/vite-env.d.ts +1 -1
  95. package/starters/default/vite.config.ts +14 -1
  96. package/starters/modular/README.md +142 -0
  97. package/starters/modular/index.html +13 -0
  98. package/starters/modular/package.json +30 -0
  99. package/starters/modular/src/app.routes.ts +39 -0
  100. package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
  101. package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
  102. package/starters/modular/src/main.ts +16 -0
  103. package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
  104. package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
  105. package/starters/modular/src/modules/home/not-found.page.ts +14 -0
  106. package/starters/modular/src/styles.d.ts +1 -0
  107. package/starters/modular/src/vite-env.d.ts +1 -0
  108. package/starters/modular/tsconfig.json +24 -0
  109. package/starters/modular/vite.config.ts +131 -0
  110. package/TODO.md +0 -79
  111. package/docs/en/01-routing.md +0 -152
  112. package/docs/en/02-project-layout.md +0 -124
  113. package/docs/en/03-static-bake.md +0 -249
  114. package/docs/en/04-ide-setup.md +0 -162
  115. package/docs/en/07-llm-pitfalls.md +0 -724
  116. package/docs/en/08-llm-zero-history-test.md +0 -56
  117. package/docs/en/09-shadow-vs-light-dom.md +0 -174
  118. package/docs/en/11-layouts.md +0 -113
  119. package/docs/en/12-auth-and-api.md +0 -124
  120. package/docs/en/16-bake-cookbook.md +0 -100
  121. package/docs/en/17-shadow-dom-forms.md +0 -192
  122. package/docs/fr/00-the-mado-way.md +0 -85
  123. package/docs/fr/01-routing.md +0 -120
  124. package/docs/fr/02-project-layout.md +0 -84
  125. package/docs/fr/03-static-bake.md +0 -289
  126. package/docs/fr/04-ide-setup.md +0 -162
  127. package/docs/fr/05-why-mado.md +0 -193
  128. package/docs/fr/06-for-backenders.md +0 -438
  129. package/docs/fr/07-llm-pitfalls.md +0 -625
  130. package/docs/fr/08-llm-zero-history-test.md +0 -38
  131. package/docs/fr/09-shadow-vs-light-dom.md +0 -65
  132. package/docs/fr/10-app-architecture.md +0 -138
  133. package/docs/fr/11-layouts.md +0 -47
  134. package/docs/fr/12-auth-and-api.md +0 -76
  135. package/docs/fr/13-deployment.md +0 -57
  136. package/docs/fr/14-testing.md +0 -41
  137. package/docs/fr/15-error-handling.md +0 -50
  138. package/docs/fr/16-bake-cookbook.md +0 -88
  139. package/docs/fr/17-shadow-dom-forms.md +0 -196
  140. package/docs/fr/18-api-freeze-map.md +0 -63
  141. package/docs/fr/19-reactivity-ordering.md +0 -97
  142. package/docs/fr/20-v1-stability.md +0 -88
  143. package/docs/fr/README.md +0 -27
  144. package/docs/ru/00-the-mado-way.md +0 -84
  145. package/docs/ru/01-routing.md +0 -119
  146. package/docs/ru/02-project-layout.md +0 -84
  147. package/docs/ru/03-static-bake.md +0 -250
  148. package/docs/ru/04-ide-setup.md +0 -144
  149. package/docs/ru/05-why-mado.md +0 -193
  150. package/docs/ru/06-for-backenders.md +0 -428
  151. package/docs/ru/07-llm-pitfalls.md +0 -624
  152. package/docs/ru/08-llm-zero-history-test.md +0 -57
  153. package/docs/ru/09-shadow-vs-light-dom.md +0 -63
  154. package/docs/ru/10-app-architecture.md +0 -152
  155. package/docs/ru/11-layouts.md +0 -47
  156. package/docs/ru/12-auth-and-api.md +0 -75
  157. package/docs/ru/13-deployment.md +0 -66
  158. package/docs/ru/14-testing.md +0 -50
  159. package/docs/ru/15-error-handling.md +0 -56
  160. package/docs/ru/16-bake-cookbook.md +0 -95
  161. package/docs/ru/17-shadow-dom-forms.md +0 -193
  162. package/docs/ru/18-api-freeze-map.md +0 -64
  163. package/docs/ru/19-reactivity-ordering.md +0 -95
  164. package/docs/ru/20-v1-stability.md +0 -82
  165. package/docs/ru/README.md +0 -25
  166. package/docs/uk/00-the-mado-way.md +0 -82
  167. package/docs/uk/01-routing.md +0 -76
  168. package/docs/uk/02-project-layout.md +0 -73
  169. package/docs/uk/03-static-bake.md +0 -48
  170. package/docs/uk/04-ide-setup.md +0 -26
  171. package/docs/uk/05-why-mado.md +0 -34
  172. package/docs/uk/06-for-backenders.md +0 -55
  173. package/docs/uk/07-llm-pitfalls.md +0 -145
  174. package/docs/uk/08-llm-zero-history-test.md +0 -34
  175. package/docs/uk/09-shadow-vs-light-dom.md +0 -58
  176. package/docs/uk/10-app-architecture.md +0 -97
  177. package/docs/uk/11-layouts.md +0 -47
  178. package/docs/uk/12-auth-and-api.md +0 -70
  179. package/docs/uk/13-deployment.md +0 -40
  180. package/docs/uk/14-testing.md +0 -34
  181. package/docs/uk/15-error-handling.md +0 -32
  182. package/docs/uk/16-bake-cookbook.md +0 -36
  183. package/docs/uk/17-shadow-dom-forms.md +0 -193
  184. package/docs/uk/18-api-freeze-map.md +0 -61
  185. package/docs/uk/19-reactivity-ordering.md +0 -95
  186. package/docs/uk/20-v1-stability.md +0 -83
  187. package/docs/uk/README.md +0 -27
  188. package/starters/default/src/modules/home/not-found.page.ts +0 -11
  189. /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
  190. /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
  191. /package/starters/{default → modular}/.editorconfig +0 -0
  192. /package/starters/{default → modular}/eslint.config.mjs +0 -0
  193. /package/starters/{default → modular}/public/favicon.svg +0 -0
  194. /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
  195. /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
  196. /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
  197. /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
  198. /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
  199. /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
  200. /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
  201. /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
  202. /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
  203. /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
  204. /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
  205. /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
  206. /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
  207. /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
  208. /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
  209. /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
  210. /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
  211. /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
  212. /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
  213. /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
  214. /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
  215. /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
  216. /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
  217. /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
  218. /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
  219. /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
package/scripts/bake.mjs CHANGED
@@ -1,570 +1,5 @@
1
- // Smart Static: bake HTML for pages whose `page({ bake })` is set.
2
- //
3
- // Usage:
4
- // mado bake
5
- // mado bake --entry src/routes.ts --template index.html --out out
6
- // mado bake --base-url https://example.com
7
- //
8
- // Defaults:
9
- // entry: src/app.routes.ts, then src/routes.ts
10
- // template: index.html
11
- // out: out/
12
- //
13
- // What it does:
14
- // 1. Loads `entry` (routes module) through Vite SSR/module loading.
15
- // 2. For every route whose page has `bake`:
16
- // a) gets params via `bake.paths()`,
17
- // b) gets data per params via `bake.data(params)`,
18
- // c) renders the TemplateResult to an HTML string (no browser),
19
- // d) materializes head() into <meta>/<link>/<script type=json-ld>,
20
- // e) inlines baked data in <script id="bake" type="application/json">,
21
- // f) writes <out>/<path>/index.html.
22
- // 3. Generates <out>/sitemap.xml.
23
- //
24
- // Context awareness:
25
- // - In app-mode (default outside the framework repository) the bundler
26
- // resolves `@madojs/mado` from node_modules normally.
27
- // - In repo-mode (the framework repository itself) it aliases
28
- // `@madojs/mado` → ./src/index.ts so the framework can dogfood itself.
29
- //
30
- // Required dev deps: linkedom, vite. We print a clear error if missing.
1
+ #!/usr/bin/env node
31
2
 
32
- import { readFile, writeFile, mkdir } from "node:fs/promises";
33
- import { existsSync, writeSync } from "node:fs";
34
- import { join, dirname, resolve } from "node:path";
35
-
36
- import { detectContext, parseFlags, resolveProjectPath } from "./_config.mjs";
37
-
38
- // ---------- Resolve options from config + flags + env ----------
39
-
40
- const { flags } = parseFlags(process.argv.slice(2));
41
- const PROJECT_ROOT = resolve(process.cwd());
42
- const CONTEXT = detectContext(PROJECT_ROOT);
43
- const ENTRY = resolveProjectPath(
44
- PROJECT_ROOT,
45
- typeof flags.entry === "string" ? flags.entry : pickDefaultEntry(PROJECT_ROOT),
46
- );
47
- const TEMPLATE = resolveProjectPath(
48
- PROJECT_ROOT,
49
- typeof flags.template === "string" ? flags.template : "index.html",
50
- );
51
- const BASE_URL = typeof flags["base-url"] === "string" ? flags["base-url"] : "https://example.com";
52
- const OUT_DIR = resolveProjectPath(
53
- PROJECT_ROOT,
54
- typeof flags.out === "string" ? flags.out : "out",
55
- );
56
-
57
- /** Write message to stderr and exit. Sync write keeps CI/execFile output reliable. */
58
- function fatal(...msgs) {
59
- writeSync(2, msgs.join("\n") + "\n");
60
- process.exit(1);
61
- }
62
-
63
- function error(...msgs) {
64
- writeSync(2, msgs.join(" ") + "\n");
65
- }
66
-
67
- if (!existsSync(ENTRY)) {
68
- fatal(
69
- `[bake] entry not found: ${ENTRY}`,
70
- `[bake] expected src/app.routes.ts or src/routes.ts; pass --entry <file> to override`,
71
- );
72
- }
73
- if (!existsSync(TEMPLATE)) {
74
- fatal(
75
- `[bake] template not found: ${TEMPLATE}`,
76
- `[bake] expected index.html; pass --template <file> to override`,
77
- );
78
- }
79
-
80
- // ---------- Optional dependencies ----------
81
-
82
- let parseHTML;
83
- try {
84
- ({ parseHTML } = await import("linkedom"));
85
- } catch {
86
- fatal(
87
- "[bake] package 'linkedom' is required.",
88
- "[bake] Install it as a dev dependency in this project:",
89
- "[bake] npm i -D linkedom vite",
90
- "[bake] (vite is also required, see next check).",
91
- "[bake] These are not bundled into @madojs/mado on purpose: bake is an",
92
- "[bake] optional build step and we don't want to add transitive deps to",
93
- "[bake] every Mado install.",
94
- );
95
- }
96
-
97
- let createViteServer;
98
- try {
99
- ({ createServer: createViteServer } = await import("vite"));
100
- } catch {
101
- fatal(
102
- "[bake] package 'vite' is required.",
103
- "[bake] Install it as a dev dependency in this project:",
104
- "[bake] npm i -D vite linkedom",
105
- );
106
- }
107
-
108
- // ---------- DOM polyfills for Node ----------
109
- //
110
- // router.ts / component.ts / css.ts touch window/document/location/customElements
111
- // at module top-level. Node has none, so install linkedom stubs before importing
112
- // the app graph.
113
-
114
- const baseHtml = await readFile(TEMPLATE, "utf8");
115
- const { window: linkedomWindow } = parseHTML(baseHtml);
116
-
117
- globalThis.window = linkedomWindow;
118
- globalThis.document = linkedomWindow.document;
119
- globalThis.location = new URL("http://localhost/");
120
- globalThis.history = { pushState: () => { }, replaceState: () => { } };
121
- globalThis.customElements = {
122
- define: () => { },
123
- get: () => undefined,
124
- whenDefined: () => Promise.resolve(),
125
- };
126
- globalThis.HTMLElement = linkedomWindow.HTMLElement ?? class { };
127
- globalThis.CSSStyleSheet = globalThis.CSSStyleSheet ?? class {
128
- cssRules = [];
129
- replaceSync() { }
130
- };
131
- globalThis.matchMedia = () => ({
132
- matches: false,
133
- addEventListener: () => { },
134
- removeEventListener: () => { },
135
- });
136
- if (!globalThis.queueMicrotask) {
137
- globalThis.queueMicrotask = (fn) => Promise.resolve().then(fn);
138
- }
139
-
140
- // ---------- Load the routes module through Vite ----------
141
- //
142
- // In repo-mode the framework dogfoods its own source; alias `@madojs/mado`
143
- // to ./src/index.ts. In app-mode the package is resolved from node_modules.
144
-
145
- const aliases = CONTEXT === "repo"
146
- ? { "@madojs/mado": resolve(PROJECT_ROOT, "src/index.ts") }
147
- : {};
148
-
149
- const viteServer = await createViteServer({
150
- root: PROJECT_ROOT,
151
- logLevel: "error",
152
- server: { middlewareMode: true },
153
- appType: "custom",
154
- resolve: { alias: aliases },
155
- });
156
-
157
- let routesModule;
158
- try {
159
- routesModule = await viteServer.ssrLoadModule(toViteId(ENTRY));
160
- } catch (err) {
161
- await closeAndFatal(`[bake] failed to load ${ENTRY}: ${err.message}`);
162
- }
163
- const routeApi = routesModule.default;
164
-
165
- if (!routeApi) {
166
- await closeAndFatal(`[bake] ${ENTRY} must default-export routes({...})`);
167
- }
168
-
169
- // Bake needs the source manifest (not the runtime RouterApi).
170
- // routes.ts must therefore also `export const manifest = {...}`.
171
- const manifest = routesModule.manifest;
172
- if (!manifest) {
173
- await closeAndFatal(
174
- `[bake] ${ENTRY} must also \`export const manifest = {...}\` ` +
175
- "(the same object passed to routes()).",
176
- );
177
- }
178
-
179
- // ---------- Main loop ----------
180
-
181
- await mkdir(OUT_DIR, { recursive: true });
182
-
183
- // Re-read template (already loaded for DOM polyfills, but we want a fresh copy
184
- // per generated page so meta/link tags don't accumulate across iterations).
185
- const TEMPLATE_HTML = baseHtml;
186
-
187
- const sitemapEntries = [];
188
- let total = 0;
189
- let bakedErrors = 0;
190
- let bakeablePages = 0;
191
- const skippedNoBake = [];
192
-
193
- for (const [pattern, entry] of Object.entries(manifest)) {
194
- if (pattern === "*") continue;
195
-
196
- const pg = await resolvePage(entry);
197
- if (!pg) continue;
198
- if (!pg.bake) {
199
- skippedNoBake.push(pattern);
200
- continue;
201
- }
202
- bakeablePages++;
203
-
204
- console.log(`[bake] ${pattern}`);
205
-
206
- let allParams;
207
- try {
208
- allParams = await pg.bake.paths();
209
- } catch (err) {
210
- error(`[bake] ${pattern}: bake.paths() failed:`, err.message);
211
- bakedErrors++;
212
- continue;
213
- }
214
-
215
- for (const params of allParams) {
216
- const pathname = applyParams(pattern, params);
217
- let data;
218
- try {
219
- data = await pg.bake.data(params);
220
- } catch (err) {
221
- error(`[bake] ${pathname}: bake.data() failed:`, err.message);
222
- bakedErrors++;
223
- continue;
224
- }
225
- const headMeta = pg.head ? pg.head(params, data) : {};
226
-
227
- // Ensure <title> is always set in baked HTML. page.title is the primary
228
- // source (string or function of params); head().title overrides it.
229
- if (!headMeta.title && pg.title) {
230
- headMeta.title = typeof pg.title === "function" ? pg.title(params) : pg.title;
231
- }
232
-
233
- const tpl = pg.view({
234
- params,
235
- data,
236
- path: () => pathname,
237
- child: null,
238
- });
239
-
240
- let bodyHtml;
241
- try {
242
- bodyHtml = renderTemplate(tpl, { route: pattern });
243
- } catch (err) {
244
- error(`[bake] ${pathname}: render failed:`, err.message);
245
- bakedErrors++;
246
- continue;
247
- }
248
-
249
- const finalHtml = buildHtml({
250
- template: TEMPLATE_HTML,
251
- bodyHtml,
252
- head: headMeta,
253
- bakedData: data,
254
- revalidate: pg.bake.revalidate,
255
- canonical: headMeta.canonical ?? `${BASE_URL}${pathname}`,
256
- });
257
-
258
- const file = join(
259
- OUT_DIR,
260
- pathname === "/" ? "/index.html" : `${pathname}/index.html`,
261
- );
262
- await mkdir(dirname(file), { recursive: true });
263
- await writeFile(file, finalHtml);
264
- total++;
265
- sitemapEntries.push({ loc: `${BASE_URL}${pathname}`, changefreq: "weekly" });
266
- }
267
- }
268
-
269
- // ---------- Sitemap ----------
270
-
271
- const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
272
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
273
- ${sitemapEntries
274
- .map(
275
- (e) =>
276
- ` <url><loc>${escapeXml(e.loc)}</loc><changefreq>${e.changefreq}</changefreq></url>`,
277
- )
278
- .join("\n")}
279
- </urlset>
280
- `;
281
- await writeFile(join(OUT_DIR, "sitemap.xml"), sitemap);
282
-
283
- console.log(`[bake] done: ${total} pages + sitemap.xml → ${OUT_DIR}`);
284
- if (bakedErrors > 0) {
285
- await closeAndFatal(`[bake] ${bakedErrors} route(s) failed; see errors above.`);
286
- }
287
- // Loud diagnostic when the manifest exists but no page declares `bake`.
288
- // Previously bake silently produced 0 pages + an empty sitemap and exited
289
- // 0, which made `mado release` look successful while shipping no static
290
- // HTML for crawlers. Fail loudly so the user notices.
291
- if (bakeablePages === 0) {
292
- error("");
293
- error(
294
- `[bake] WARNING: no page in ${ENTRY} declares \`bake: { paths, data }\`.`,
295
- );
296
- error(
297
- `[bake] ${skippedNoBake.length} route(s) skipped: ${skippedNoBake
298
- .slice(0, 6)
299
- .join(", ")}${skippedNoBake.length > 6 ? ", …" : ""}`,
300
- );
301
- error("[bake] Add `bake` to at least one page (e.g. your landing route):");
302
- error("[bake] export default page({");
303
- error("[bake] view: …,");
304
- error("[bake] bake: { paths: () => [{}], data: () => ({}) },");
305
- error("[bake] });");
306
- error(
307
- "[bake] Without bake the build ships only the SPA shell — search engines",
308
- );
309
- error("[bake] and link previews see an empty <body>.");
310
- // Exit non-zero so `mado release` halts and the user is forced to address
311
- // it. If you intentionally have an SPA-only deploy, drop `mado bake` from
312
- // the release pipeline (or set MADO_BAKE_ALLOW_EMPTY=1).
313
- if (process.env.MADO_BAKE_ALLOW_EMPTY !== "1") {
314
- await viteServer.close();
315
- process.exit(1);
316
- }
317
- }
318
-
319
- await viteServer.close();
320
-
321
- // ---------- Helpers ----------
322
-
323
- async function resolvePage(entry) {
324
- if (entry && entry._page === true) return entry;
325
- if (typeof entry === "function") {
326
- try {
327
- const mod = await entry();
328
- return mod?.default;
329
- } catch (e) {
330
- console.warn("[bake] failed to load route:", e.message);
331
- return null;
332
- }
333
- }
334
- return null;
335
- }
336
-
337
- function toViteId(path) {
338
- return path.split("\\").join("/");
339
- }
340
-
341
- function pickDefaultEntry(projectRoot) {
342
- const appRoutes = "src/app.routes.ts";
343
- if (existsSync(resolve(projectRoot, appRoutes))) return appRoutes;
344
- return "src/routes.ts";
345
- }
346
-
347
- async function closeAndFatal(...msgs) {
348
- await viteServer.close().catch(() => { });
349
- fatal(...msgs);
350
- }
351
-
352
- function applyParams(pattern, params) {
353
- return pattern.replace(/:([\w]+)/g, (_, k) => {
354
- const v = params[k];
355
- if (v == null) throw new Error(`[bake] missing param :${k} for ${pattern}`);
356
- return encodeURIComponent(String(v));
357
- });
358
- }
359
-
360
- // ---------- Render TemplateResult → HTML string ----------
361
- //
362
- // Tiny SSR for TemplateResult. Supports the same shapes as html.ts but
363
- // without events (@click is stripped) and without live signals (function
364
- // values are called once).
365
-
366
- function renderTemplate(tpl, ctx) {
367
- if (tpl == null || tpl === false || tpl === true) return "";
368
- if (typeof tpl === "string") return escapeHtml(tpl);
369
- if (typeof tpl === "number") return String(tpl);
370
- if (Array.isArray(tpl)) return tpl.map((x) => renderTemplate(x, ctx)).join("");
371
- if (typeof tpl === "object") {
372
- if (tpl._mado === true) return renderMadoTemplate(tpl, ctx);
373
-
374
- // Unknown shapes (e.g. each() directive results) must NOT silently render
375
- // as "[object Object]". Either each() unwraps to an array here, or we
376
- // throw with a meaningful location.
377
- throw new Error(
378
- `bake cannot render value of type "${tpl._type ?? tpl.constructor?.name ?? "object"}" ` +
379
- `in route ${ctx?.route ?? "?"}. ` +
380
- "Hint: each() and other directives are not yet supported in bake. " +
381
- "Use a plain array (items.map(render)) in baked views, or render this " +
382
- "section only on the client.",
383
- );
384
- }
385
- return "";
386
- }
387
-
388
- function renderMadoTemplate(tpl, ctx) {
389
- const { strings, values } = tpl;
390
- let html = "";
391
- for (let i = 0; i < strings.length; i++) {
392
- html += strings[i];
393
- if (i < strings.length - 1) {
394
- html += renderValue(values[i], inAttributeContext(html), ctx);
395
- }
396
- }
397
- // Strip event markers and normalize property / boolean-attribute forms
398
- // (mirrors the runtime bindings in src/html/bindings.ts).
399
- return html
400
- .replace(/\s+@[\w-]+="[^"]*"/g, "")
401
- .replace(/\s+\.([\w-]+)="([^"]*)"/g, ' $1="$2"')
402
- .replace(/\s+\?([\w-]+)="(true|on|1)"/g, ' $1=""')
403
- .replace(/\s+\?[\w-]+="(false|off|0|)"/g, "");
404
- }
405
-
406
- function inAttributeContext(html) {
407
- const lastOpen = html.lastIndexOf("<");
408
- const lastClose = html.lastIndexOf(">");
409
- return lastOpen > lastClose;
410
- }
411
-
412
- function renderValue(v, inAttr, ctx) {
413
- if (v == null || v === false) return "";
414
- if (v === true) return "";
415
- if (typeof v === "function") {
416
- try {
417
- return renderValue(v(), inAttr, ctx);
418
- } catch {
419
- return "";
420
- }
421
- }
422
- if (Array.isArray(v)) return v.map((x) => renderValue(x, inAttr, ctx)).join("");
423
- if (v && v._mado === true) return renderMadoTemplate(v, ctx);
424
- if (v && typeof v === "object" && typeof v._madoDirective === "string") {
425
- return renderDirective(v, inAttr, ctx);
426
- }
427
- if (v && typeof v === "object") {
428
- // Same defense as renderTemplate(): never silently coerce to "[object Object]".
429
- throw new Error(
430
- `bake cannot serialize value of type "${v?._type ?? v?.constructor?.name ?? "object"}" ` +
431
- `in route ${ctx?.route ?? "?"}. ` +
432
- "Hint: each() is not yet supported in bake. Use a plain array in baked views.",
433
- );
434
- }
435
- if (inAttr) return escapeAttr(String(v));
436
- return escapeHtml(String(v));
437
- }
438
-
439
- function renderDirective(v, inAttr, ctx) {
440
- switch (v._madoDirective) {
441
- case "unsafeHTML": {
442
- if (inAttr) {
443
- throw new Error(
444
- `bake cannot render unsafeHTML() inside an attribute in route ${ctx?.route ?? "?"}.`,
445
- );
446
- }
447
- return String(v.value ?? "");
448
- }
449
- case "classMap": {
450
- const value = Object.entries(v.value ?? {})
451
- .filter(([, enabled]) => !!enabled)
452
- .map(([className]) => className)
453
- .join(" ");
454
- return inAttr ? escapeAttr(value) : escapeHtml(value);
455
- }
456
- case "styleMap": {
457
- const value = Object.entries(v.value ?? {})
458
- .filter(([, raw]) => raw != null && raw !== false)
459
- .map(([name, raw]) => `${toCssPropertyName(name)}:${String(raw)}`)
460
- .join(";");
461
- return inAttr ? escapeAttr(value) : escapeHtml(value);
462
- }
463
- case "ref":
464
- return "";
465
- default:
466
- throw new Error(
467
- `bake cannot render directive "${v._madoDirective}" in route ${ctx?.route ?? "?"}.`,
468
- );
469
- }
470
- }
471
-
472
- function toCssPropertyName(name) {
473
- if (name.startsWith("--")) return name;
474
- return name.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
475
- }
476
-
477
- function escapeHtml(s) {
478
- return s
479
- .replace(/&/g, "&amp;")
480
- .replace(/</g, "&lt;")
481
- .replace(/>/g, "&gt;");
482
- }
483
- function escapeAttr(s) {
484
- return escapeHtml(s).replace(/"/g, "&quot;");
485
- }
486
- function escapeXml(s) {
487
- return s
488
- .replace(/&/g, "&amp;")
489
- .replace(/</g, "&lt;")
490
- .replace(/>/g, "&gt;")
491
- .replace(/"/g, "&quot;");
492
- }
493
-
494
- // ---------- Final HTML with head + baked data ----------
495
-
496
- function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical }) {
497
- const { document } = parseHTML(template);
498
-
499
- if (head.title) document.title = head.title;
500
- if (head.description) {
501
- setMeta(document, { name: "description", content: head.description });
502
- }
503
- if (canonical) {
504
- setLink(document, { rel: "canonical", href: canonical });
505
- }
506
- if (head.og) {
507
- for (const [k, v] of Object.entries(head.og)) {
508
- if (!v) continue;
509
- setMeta(document, { property: `og:${k}`, content: String(v) });
510
- }
511
- }
512
- if (head.twitter || head.og) {
513
- const tw = head.twitter ?? {};
514
- const og = head.og ?? {};
515
- setMeta(document, { name: "twitter:card", content: tw.card ?? "summary" });
516
- if (tw.title ?? og.title)
517
- setMeta(document, { name: "twitter:title", content: tw.title ?? og.title });
518
- if (tw.description ?? og.description)
519
- setMeta(document, { name: "twitter:description", content: tw.description ?? og.description });
520
- if (tw.image ?? og.image)
521
- setMeta(document, { name: "twitter:image", content: tw.image ?? og.image });
522
- }
523
- for (const m of head.meta ?? []) setMeta(document, m);
524
- for (const l of head.link ?? []) setLink(document, l);
525
-
526
- if (head.jsonLd != null) {
527
- const s = document.createElement("script");
528
- s.setAttribute("type", "application/ld+json");
529
- s.setAttribute("data-mado-head", "baked");
530
- s.textContent = JSON.stringify(head.jsonLd);
531
- document.head.appendChild(s);
532
- }
533
-
534
- if (revalidate) {
535
- setMeta(document, { name: "bake-revalidate", content: String(revalidate) });
536
- }
537
-
538
- if (bakedData !== undefined) {
539
- const s = document.createElement("script");
540
- s.setAttribute("type", "application/json");
541
- s.id = "bake";
542
- s.textContent = JSON.stringify(bakedData);
543
- document.body.appendChild(s);
544
- }
545
-
546
- const app = document.getElementById("app");
547
- if (app) {
548
- app.setAttribute("data-mado-baked", "");
549
- app.innerHTML = bodyHtml;
550
- }
551
-
552
- return "<!doctype html>\n" + document.documentElement.outerHTML;
553
- }
554
-
555
- function setMeta(doc, attrs) {
556
- const m = doc.createElement("meta");
557
- if (attrs.name) m.setAttribute("name", attrs.name);
558
- if (attrs.property) m.setAttribute("property", attrs.property);
559
- m.setAttribute("content", attrs.content);
560
- m.setAttribute("data-mado-head", "baked");
561
- doc.head.appendChild(m);
562
- }
563
- function setLink(doc, attrs) {
564
- const l = doc.createElement("link");
565
- l.setAttribute("rel", attrs.rel);
566
- l.setAttribute("href", attrs.href);
567
- if (attrs.hreflang) l.setAttribute("hreflang", attrs.hreflang);
568
- l.setAttribute("data-mado-head", "baked");
569
- doc.head.appendChild(l);
570
- }
3
+ console.error("[mado] `mado bake` was removed.");
4
+ console.error("Use `mado static`, or run the complete pipeline with `mado release`.");
5
+ process.exit(1);
@@ -1,3 +1,4 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { access, mkdir, writeFile } from "node:fs/promises";
2
3
  import { dirname, join, relative } from "node:path";
3
4
 
@@ -13,6 +14,35 @@ const KINDS = [
13
14
  "layout",
14
15
  ];
15
16
 
17
+ /**
18
+ * Generators support two starter shapes:
19
+ *
20
+ * modular — src/modules/<name>/<...> (default behaviour)
21
+ * universal— src/pages, src/components, src/content (the default starter)
22
+ *
23
+ * Detection is structural: a project with `src/modules/` is treated as
24
+ * modular; otherwise the universal layout is assumed. This lets users of
25
+ * either starter run `mado new` without configuring anything, and keeps
26
+ * the modular contract intact for projects that opt into it.
27
+ */
28
+ function detectStarterShape(ctx) {
29
+ if (existsSync(join(ctx.projectRoot, "src/modules"))) return "modular";
30
+ if (existsSync(join(ctx.projectRoot, "src/pages"))) return "universal";
31
+ // No explicit hint — fall back to modular, which matches the
32
+ // historical behaviour and is documented in the generator help.
33
+ return "modular";
34
+ }
35
+
36
+ /** Generators that only make sense inside the modular starter. */
37
+ const MODULAR_ONLY = new Set([
38
+ "module",
39
+ "connector",
40
+ "resource",
41
+ "service",
42
+ "form",
43
+ "guard",
44
+ ]);
45
+
16
46
  export async function runNew(ctx, args) {
17
47
  const [kind, target] = args;
18
48
  if (!kind || !target) {
@@ -20,6 +50,20 @@ export async function runNew(ctx, args) {
20
50
  process.exit(1);
21
51
  }
22
52
 
53
+ const shape = detectStarterShape(ctx);
54
+ if (shape === "universal" && MODULAR_ONLY.has(kind)) {
55
+ console.error(
56
+ `[mado] '${kind}' is a modular-starter generator and is not ` +
57
+ "available in the universal starter.",
58
+ );
59
+ console.error(
60
+ "[mado] Either move this project to the modular layout " +
61
+ "(create src/modules/<name>/) or scaffold a fresh modular " +
62
+ "project with: mado init my-app --starter modular",
63
+ );
64
+ process.exit(1);
65
+ }
66
+
23
67
  const generators = {
24
68
  module: scaffoldModule,
25
69
  page: scaffoldPage,
@@ -39,7 +83,7 @@ export async function runNew(ctx, args) {
39
83
  process.exit(1);
40
84
  }
41
85
 
42
- await fn(ctx, normalizeTarget(target));
86
+ await fn(ctx, normalizeTarget(target), shape);
43
87
  }
44
88
 
45
89
  function printUsage() {
@@ -86,8 +130,11 @@ async function scaffoldModule(ctx, name) {
86
130
  );
87
131
  }
88
132
 
89
- async function scaffoldPage(ctx, target) {
90
- const file = moduleFile(ctx, target, ".page.ts");
133
+ async function scaffoldPage(ctx, target, shape) {
134
+ const file =
135
+ shape === "universal"
136
+ ? join(srcDir(ctx), "pages", `${leafName(target)}.page.ts`)
137
+ : moduleFile(ctx, target, ".page.ts");
91
138
  const name = leafName(target);
92
139
  await writeOnce(
93
140
  file,
@@ -225,8 +272,11 @@ export const ${useName} = () =>
225
272
  );
226
273
  }
227
274
 
228
- async function scaffoldComponent(ctx, target) {
229
- const file = moduleFile(ctx, target, ".component.ts");
275
+ async function scaffoldComponent(ctx, target, shape) {
276
+ const file =
277
+ shape === "universal"
278
+ ? join(srcDir(ctx), "components", `${leafName(target)}.component.ts`)
279
+ : moduleFile(ctx, target, ".component.ts");
230
280
  const name = leafName(target);
231
281
  const tag = name.includes("-") ? name : `x-${name}`;
232
282
  await writeOnce(
@@ -1,20 +1,26 @@
1
1
  export function printHelp(ctx) {
2
2
  const mode = ctx.isRepo ? "repo-mode (framework repository)" : "app-mode";
3
+ const buildLine = ctx.isRepo
4
+ ? " mado build tsc package compile (writes internal dist/)"
5
+ : " mado build Vite production SPA build -> out/";
3
6
  console.log(`mado commands (${mode}):
4
7
 
5
8
  Project lifecycle:
6
9
  mado init <name> [--starter default] [--force]
7
10
  scaffold a new app
8
11
  mado dev Vite dev server
9
- mado build tsc package compile (writes internal dist/)
12
+ ${buildLine}
10
13
  mado typecheck tsc --noEmit
11
14
  mado test run unit tests
12
15
 
13
16
  Production:
14
- mado bake [--entry <file>] [--template <html>] [--out <dir>] [--base-url <url>]
15
- prerender baked routes -> out/
16
- mado release typecheck + vite build + bake -> out/
17
- mado preview serve exactly out/ locally
17
+ mado static [--entry <file>] [--out <dir>] [--base-url <url>]
18
+ [--browser-channel chrome | --browser-path <path>]
19
+ browser-snapshot static routes -> out/
20
+ (low-level; prefer \`mado release\`)
21
+ mado release typecheck + vite build + static snapshots
22
+ + deployment files -> out/
23
+ mado preview serve exactly out/ locally (production-like)
18
24
 
19
25
  Generators:
20
26
  mado new <module|page|connector|resource|service|form|component|guard|layout> <path>