@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
@@ -31,10 +31,21 @@ export async function main(argv) {
31
31
  await runInit(ctx, args);
32
32
  break;
33
33
  case "build":
34
- await runNodeBin(ctx, "typescript/bin/tsc", args);
34
+ // Framework repo bootstraps via `tsc` (so the published package
35
+ // ships compiled .js). Every other context — user apps, the
36
+ // starter — gets a production Vite build of the deployable SPA.
37
+ if (ctx.isRepo) {
38
+ await runNodeBin(ctx, "typescript/bin/tsc", args);
39
+ } else {
40
+ await runVite(ctx, ["build", ...args], { defaultConfig: true });
41
+ }
35
42
  break;
36
43
  case "watch":
37
- await runNodeBin(ctx, "typescript/bin/tsc", ["-w", ...args]);
44
+ if (ctx.isRepo) {
45
+ await runNodeBin(ctx, "typescript/bin/tsc", ["-w", ...args]);
46
+ } else {
47
+ await runVite(ctx, ["build", "--watch", ...args], { defaultConfig: true });
48
+ }
38
49
  break;
39
50
  case "typecheck":
40
51
  await runNodeBin(ctx, "typescript/bin/tsc", ["--noEmit", ...args]);
@@ -42,7 +53,7 @@ export async function main(argv) {
42
53
  case "test": {
43
54
  await runNodeBin(ctx, "typescript/bin/tsc", []);
44
55
  const files = await listTestFiles(projectRoot);
45
- await run(process.execPath, ["--test", "--test-timeout=30000", ...files, ...args], {
56
+ await run(process.execPath, ["--test", "--test-timeout=120000", ...files, ...args], {
46
57
  cwd: projectRoot,
47
58
  });
48
59
  break;
@@ -55,7 +66,12 @@ export async function main(argv) {
55
66
  );
56
67
  break;
57
68
  case "bake":
58
- await runNodeScript(ctx, "scripts/bake.mjs", args);
69
+ console.error("[mado] `mado bake` was removed.");
70
+ console.error("Use `mado static`, or run the complete pipeline with `mado release`.");
71
+ process.exit(1);
72
+ break;
73
+ case "static":
74
+ await runNodeScript(ctx, "scripts/static.mjs", args);
59
75
  break;
60
76
  case "preview":
61
77
  await runNodeScript(ctx, "scripts/preview.mjs", args);
@@ -4,7 +4,12 @@ import { join, resolve } from "node:path";
4
4
 
5
5
  import { parseFlags } from "../_config.mjs";
6
6
 
7
- const STARTERS = ["default"];
7
+ // `default` is the universal starter: minimal, runnable, zero backend.
8
+ // `modular` is the long-lived business-app reference architecture:
9
+ // auth, billing, guards, layouts, services, connectors, contracts.
10
+ // Both target the same Mado runtime — they only differ in how much
11
+ // structure they pre-create.
12
+ const STARTERS = ["default", "modular"];
8
13
 
9
14
  export async function runInit(ctx, rawArgs) {
10
15
  const { flags, positional } = parseFlags(rawArgs);
@@ -109,8 +114,8 @@ async function ensureStarterPackageJson(ctx, target) {
109
114
  pkg.devDependencies = {
110
115
  ...(pkg.devDependencies ?? {}),
111
116
  typescript: rootDev.typescript ?? "^6.0.3",
112
- linkedom: rootDev.linkedom ?? "^0.18.12",
113
117
  lightningcss: rootDev.lightningcss ?? "^1.32.0",
118
+ "playwright-core": rootDev["playwright-core"] ?? "^1.47.0",
114
119
  vite: rootDev.vite ?? "^8.0.16",
115
120
  };
116
121
 
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
- import { copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import { dirname, join, resolve } from "node:path";
2
+ import { readdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
4
  import { brotliCompressSync, constants as zlibConst, gzipSync } from "node:zlib";
5
5
 
6
6
  import { parseFlags } from "../_config.mjs";
@@ -12,8 +12,6 @@ export async function runRelease(ctx, rawArgs) {
12
12
  ctx.projectRoot,
13
13
  typeof releaseFlags.out === "string" ? releaseFlags.out : "out",
14
14
  );
15
- const bundledHtml = join(outDir, "index.html");
16
- const bakedDir = join(outDir, "baked");
17
15
 
18
16
  console.log(`[release] context: ${ctx.context}`);
19
17
  console.log(`[release] artifact: ${outDir}`);
@@ -34,37 +32,22 @@ export async function runRelease(ctx, rawArgs) {
34
32
  console.log("[release] step 2/5 vite build");
35
33
  await runVite(ctx, ["build", "--outDir", outDir], { defaultConfig: true });
36
34
 
37
- console.log("[release] step 3/5 bake");
38
- if (releaseFlags["keep-bake-dir"]) {
39
- await runNodeScript(ctx, "scripts/bake.mjs", [
40
- ...rawArgs.filter((a) => a !== "--keep-bake-dir"),
41
- "--template",
42
- bundledHtml,
43
- "--out",
44
- bakedDir,
45
- ]);
46
- const promoted = await promoteBakedHtml(bakedDir, outDir);
47
- if (promoted.html > 0) {
48
- console.log(`[release] promoted ${promoted.html} baked HTML page(s) into out/`);
49
- }
50
- if (promoted.sitemap) {
51
- console.log(`[release] copied sitemap.xml -> ${join(outDir, "sitemap.xml")}`);
52
- }
53
- } else {
54
- await runNodeScript(ctx, "scripts/bake.mjs", [
55
- ...rawArgs,
56
- "--template",
57
- bundledHtml,
58
- "--out",
59
- outDir,
60
- ]);
35
+ console.log("[release] step 3/5 static snapshots");
36
+ await runNodeScript(ctx, "scripts/static.mjs", [
37
+ ...rawArgs.filter((a) => a !== "--no-clean"),
38
+ "--out",
39
+ outDir,
40
+ ]);
41
+
42
+ console.log("[release] step 4/5 deployment files");
43
+ // GitHub Pages / Netlify / Cloudflare Pages fallback. SPA fallback shell
44
+ // is written by `mado static`; here we only register the deployment
45
+ // bindings, and respect user-supplied files (writeIfMissing).
46
+ const spaShell = join(outDir, "_mado/spa.html");
47
+ if (existsSync(spaShell)) {
48
+ await writeIfMissing(join(outDir, "404.html"), await readFile(spaShell, "utf8"), "[release] ");
61
49
  }
62
-
63
- console.log("[release] step 4/5 precompress assets");
64
- await precompressOut(outDir);
65
-
66
- console.log("[release] step 5/5 CDN config");
67
- await writeIfMissing(join(outDir, "_redirects"), "/* /index.html 200\n", "[release] ");
50
+ await writeIfMissing(join(outDir, "_redirects"), "/* /_mado/spa.html 200\n", "[release] ");
68
51
  await writeIfMissing(
69
52
  join(outDir, "_headers"),
70
53
  [
@@ -78,45 +61,14 @@ export async function runRelease(ctx, rawArgs) {
78
61
  "[release] ",
79
62
  );
80
63
 
64
+ console.log("[release] step 5/5 precompress assets");
65
+ await precompressOut(outDir);
66
+
81
67
  console.log("");
82
68
  console.log(`[release] done. Deploy artifact: ${outDir}`);
83
69
  console.log("[release] try: mado preview");
84
70
  }
85
71
 
86
- async function promoteBakedHtml(bakedDir, outDir) {
87
- if (!existsSync(bakedDir)) return { html: 0, sitemap: false };
88
-
89
- let html = 0;
90
-
91
- async function walk(dir, rel = "") {
92
- for (const entry of await readdir(dir, { withFileTypes: true })) {
93
- const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
94
- const source = join(dir, entry.name);
95
- if (entry.isDirectory()) {
96
- await walk(source, nextRel);
97
- continue;
98
- }
99
- if (!entry.isFile() || !entry.name.endsWith(".html")) continue;
100
- const target = join(outDir, nextRel);
101
- await mkdir(dirname(target), { recursive: true });
102
- await copyFile(source, target);
103
- html++;
104
- }
105
- }
106
-
107
- await walk(bakedDir);
108
-
109
- const bakedSitemap = join(bakedDir, "sitemap.xml");
110
- const rootSitemap = join(outDir, "sitemap.xml");
111
- let sitemap = false;
112
- if (existsSync(bakedSitemap)) {
113
- await copyFile(bakedSitemap, rootSitemap);
114
- sitemap = true;
115
- }
116
-
117
- return { html, sitemap };
118
- }
119
-
120
72
  async function precompressOut(outDir) {
121
73
  if (!existsSync(outDir)) return;
122
74
  const files = await listCompressibleFiles(outDir);
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ // Docs lint: refuses to ship documentation that still uses the legacy
3
+ // vocabulary the 0.12 release replaced. Active reference docs, README,
4
+ // llms.txt, AGENTS.md, CONTRIBUTING.md, TODO.md and the starter READMEs
5
+ // must use the new terms; migration guides, CHANGELOG, release notes
6
+ // and ADRs are allowed to mention the old names by name so users can
7
+ // find them.
8
+
9
+ import { readdirSync, readFileSync, statSync } from "node:fs";
10
+ import { dirname, join, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const REPO_ROOT = resolve(__dirname, "..");
15
+
16
+ // Phrases that MUST disappear from current docs/README/llms/AGENTS. They
17
+ // remain legal inside the allow-listed contexts below.
18
+ const FORBIDDEN = [
19
+ // Page API renamed from `bake` to `static`.
20
+ { pattern: /\bpage\.bake\b/, replacement: "page.static" },
21
+ { pattern: /\bbake\.paths\b/, replacement: "static.paths" },
22
+ { pattern: /\bbake\.data\b/, replacement: "static.initialData" },
23
+ { pattern: /\bbake\.revalidate\b/, replacement: "drop; no revalidate API" },
24
+
25
+ // CLI renamed from `bake` to `static`.
26
+ { pattern: /\bmado bake\b/, replacement: "mado static" },
27
+ { pattern: /\bnpm run bake\b/, replacement: "npm run release" },
28
+
29
+ // Output / marker shape changed.
30
+ { pattern: /\bout\/baked\b/, replacement: "out/<route>/index.html" },
31
+ { pattern: /#bake\b/, replacement: "data-mado-static-data" },
32
+
33
+ // Old "Smart Static (`bake`)" headline.
34
+ { pattern: /Smart Static \(`bake`\)/, replacement: "Static snapshots (`mado static`)" },
35
+
36
+ // Old transport / renderer claims.
37
+ { pattern: /No Vite required/i, replacement: "drop the phrase; Vite is the canonical transport" },
38
+ { pattern: /\bNo Chromium needed\b/i, replacement: "drop the phrase; mado static REQUIRES Chromium" },
39
+ // `linkedom` is also a legitimate Node-side test helper (the framework
40
+ // uses it in test/router/* for DOM unit tests). We only flag the
41
+ // phrases that mis-described the snapshot pipeline as a linkedom
42
+ // renderer.
43
+ { pattern: /linkedom (?:as|is) (?:the |a )?(?:static|snapshot|production) renderer/i,
44
+ replacement: "drop the claim; capture uses Playwright/Chromium" },
45
+ { pattern: /linkedom-?based renderer/i,
46
+ replacement: "drop the claim; capture uses Playwright/Chromium" },
47
+ { pattern: /server-rendered snapshot/i, replacement: "browser-rendered snapshot" },
48
+ { pattern: /\bmeta[- ]shell\b/i, replacement: "static snapshot" },
49
+
50
+ // Old positioning.
51
+ {
52
+ pattern: /SEO-heavy public sites are not supported/i,
53
+ replacement: "drop the phrase — Mado now snapshots public sites",
54
+ },
55
+ {
56
+ pattern: /\binternal tools only\b/i,
57
+ replacement: "drop the phrase — Mado is for sites and apps",
58
+ },
59
+ {
60
+ pattern: /SPA framework for internal tools/i,
61
+ replacement: "drop the phrase — Mado is for sites and apps",
62
+ },
63
+ {
64
+ pattern: /shadow:\s*false[^.\n]*SEO/i,
65
+ replacement: "drop the recommendation; SEO is handled by snapshots",
66
+ },
67
+
68
+ // Old pre-Vite transport language.
69
+ { pattern: /\btsc[- ]only\b/i, replacement: "drop the phrase — Vite is the canonical transport" },
70
+ { pattern: /\bno bundler\b/i, replacement: "drop the phrase — Vite is the canonical bundler" },
71
+ { pattern: /\btsc\s*(?:→|->)+\s*browser\b/i,
72
+ replacement: "drop the phrase — generated apps go through Vite" },
73
+ { pattern: /\bimport[\s-]?map(?:s)?\b/i, replacement: "drop the phrase — apps use Vite, not import maps" },
74
+
75
+ // Old dev / static / edge claims.
76
+ { pattern: /SSE[- ]?reload/i, replacement: "drop the phrase — dev uses Vite HMR" },
77
+ { pattern: /\bedge[- ]prerender\b/i, replacement: "drop the phrase — capture is `mado static` only" },
78
+
79
+ // Old doc filenames in active docs (the migration table in
80
+ // docs/en/README.md is allow-listed via docs-lint:allow-legacy-mention
81
+ // markers around it if needed).
82
+ { pattern: /\b03-static-bake\.md\b/, replacement: "15-static-snapshots.md" },
83
+ { pattern: /\b09-shadow-vs-light-dom\.md\b/, replacement: "10-pages-and-components.md" },
84
+ { pattern: /\b10-app-architecture\.md\b/, replacement: "16-app-architecture.md" },
85
+ { pattern: /\b13-deployment\.md\b/, replacement: "20-deployment.md" },
86
+ { pattern: /\b16-bake-cookbook\.md\b/, replacement: "23-cookbook.md" },
87
+ { pattern: /\b18-api-freeze-map\.md\b/, replacement: "30-api-freeze-map.md" },
88
+ { pattern: /\b19-reactivity-ordering\.md\b/, replacement: "31-reactivity-ordering.md" },
89
+ { pattern: /\b20-v1-stability\.md\b/, replacement: "32-v1-stability.md" },
90
+ { pattern: /\b07-llm-pitfalls\.md\b/, replacement: "40-llm-guide.md" },
91
+ { pattern: /\b08-llm-zero-history-test\.md\b/, replacement: "40-llm-guide.md" },
92
+ ];
93
+
94
+ const ALLOW_LISTED = [
95
+ /(^|\/)CHANGELOG\.md$/,
96
+ /(^|\/)docs\/architecture\/adr\//,
97
+ /(^|\/)docs\/.+migration/i,
98
+ /(^|\/)docs\/.+v1-stability/i,
99
+ /(^|\/)scripts\/docs-lint\.mjs$/,
100
+ ];
101
+
102
+ // Files / trees that must stay on-message about the post-0.12 API. New
103
+ // roots must be added here AND covered by the CI step.
104
+ const ROOTS = [
105
+ "README.md",
106
+ "AGENTS.md",
107
+ "llms.txt",
108
+ "CONTRIBUTING.md",
109
+ "TODO.md",
110
+ "docs/README.md",
111
+ "docs/en",
112
+ "starters/default/README.md",
113
+ "starters/modular/README.md",
114
+ ];
115
+
116
+ let errors = 0;
117
+
118
+ for (const root of ROOTS) {
119
+ const full = join(REPO_ROOT, root);
120
+ try {
121
+ statSync(full);
122
+ } catch {
123
+ continue;
124
+ }
125
+ scan(full);
126
+ }
127
+
128
+ if (errors > 0) {
129
+ console.error(`\n[docs-lint] ${errors} forbidden term(s) in current docs.`);
130
+ process.exit(1);
131
+ }
132
+ console.log("[docs-lint] OK");
133
+
134
+ function scan(path) {
135
+ const stat = statSync(path);
136
+ if (stat.isDirectory()) {
137
+ for (const entry of readdirSync(path)) scan(join(path, entry));
138
+ return;
139
+ }
140
+ if (!path.endsWith(".md") && !path.endsWith(".txt")) return;
141
+ const rel = path.slice(REPO_ROOT.length + 1);
142
+ if (ALLOW_LISTED.some((rx) => rx.test(rel))) return;
143
+
144
+ const body = readFileSync(path, "utf8");
145
+ const lines = body.split("\n");
146
+ let inIgnoreBlock = false;
147
+ for (let i = 0; i < lines.length; i++) {
148
+ const line = lines[i];
149
+ // Block-scoped allowance: paragraphs that teach LLMs which names
150
+ // are obsolete must be free to mention those names verbatim.
151
+ // <!-- docs-lint:allow-legacy-mention -->
152
+ // ...legacy terms...
153
+ // <!-- /docs-lint:allow-legacy-mention -->
154
+ if (line.includes("docs-lint:allow-legacy-mention")) {
155
+ inIgnoreBlock = !line.includes("/docs-lint:allow-legacy-mention");
156
+ continue;
157
+ }
158
+ if (inIgnoreBlock) continue;
159
+
160
+ for (const rule of FORBIDDEN) {
161
+ if (rule.pattern.test(line)) {
162
+ errors++;
163
+ console.error(
164
+ `${rel}:${i + 1}: forbidden term matches /${rule.pattern.source}/. ` +
165
+ `Replace with: ${rule.replacement}`,
166
+ );
167
+ }
168
+ }
169
+ }
170
+ }
@@ -21,18 +21,24 @@ try {
21
21
  await mkdir(installRoot, { recursive: true });
22
22
  await run("npm", ["install", tarball], { cwd: installRoot });
23
23
 
24
+ // Public-API surface smoke: prove the published tarball exposes the
25
+ // root entry, the devtools side-effect import and the Vite plugin,
26
+ // while still hiding internal subpaths behind `exports`.
24
27
  await run(
25
28
  process.execPath,
26
29
  [
27
30
  "--input-type=module",
28
31
  "--eval",
29
32
  `
30
- import { html, signal } from "@madojs/mado";
33
+ import { html, signal, routeUrl, appBase } from "@madojs/mado";
31
34
  import "@madojs/mado/devtools.js";
32
35
  import { mado } from "@madojs/mado/vite";
33
36
  if (typeof html !== "function" || typeof signal !== "function") {
34
37
  throw new Error("public root import failed");
35
38
  }
39
+ if (typeof routeUrl !== "function" || typeof appBase !== "string") {
40
+ throw new Error("routing helpers missing from public root");
41
+ }
36
42
  if (typeof mado !== "function") throw new Error("vite plugin import failed");
37
43
  try {
38
44
  await import("@madojs/mado/lifecycle.js");
@@ -45,15 +51,56 @@ try {
45
51
  { cwd: installRoot },
46
52
  );
47
53
 
48
- await run("npx", ["mado", "init", "smoke-app"], {
49
- cwd: installRoot,
50
- env: { ...process.env, MADO_PACKAGE_SPEC: tarball },
54
+ // We exercise BOTH published starters end-to-end:
55
+ //
56
+ // smoke-universal `mado init` (default), npm install, typecheck,
57
+ // release. This is the path the README quick-start
58
+ // points new users to; if it ever breaks for a
59
+ // published tarball the first-run experience is
60
+ // broken.
61
+ //
62
+ // smoke-modular — `mado init --starter modular`, npm install,
63
+ // `mado new module`, release. Long-lived apps
64
+ // deploy this shape; the `mado new` generator
65
+ // also lives here.
66
+ //
67
+ // Each run uses MADO_SITE so static routes resolve canonical URLs,
68
+ // and points the starter at the freshly packed tarball through
69
+ // MADO_PACKAGE_SPEC so package.json doesn't reference an unpublished
70
+ // version.
71
+
72
+ await smokeStarter({
73
+ label: "universal",
74
+ appName: "smoke-universal",
75
+ initArgs: ["mado", "init", "smoke-universal"],
76
+ after: async (appRoot) => {
77
+ await run("npm", ["run", "typecheck"], { cwd: appRoot });
78
+ await run("npm", ["run", "release"], {
79
+ cwd: appRoot,
80
+ env: { ...process.env, MADO_SITE: "https://package-smoke.test" },
81
+ });
82
+ },
83
+ installRoot,
84
+ tarball,
51
85
  });
52
86
 
53
- const appRoot = join(installRoot, "smoke-app");
54
- await run("npm", ["install"], { cwd: appRoot });
55
- await run("npm", ["run", "new", "--", "module", "smoke"], { cwd: appRoot });
56
- await run("npm", ["run", "release"], { cwd: appRoot });
87
+ await smokeStarter({
88
+ label: "modular",
89
+ appName: "smoke-modular",
90
+ initArgs: ["mado", "init", "smoke-modular", "--starter", "modular"],
91
+ after: async (appRoot) => {
92
+ await run("npm", ["run", "new", "--", "module", "smoke"], {
93
+ cwd: appRoot,
94
+ });
95
+ await run("npm", ["run", "typecheck"], { cwd: appRoot });
96
+ await run("npm", ["run", "release"], {
97
+ cwd: appRoot,
98
+ env: { ...process.env, MADO_SITE: "https://package-smoke.test" },
99
+ });
100
+ },
101
+ installRoot,
102
+ tarball,
103
+ });
57
104
 
58
105
  console.log(`[package-smoke] ok ${basename(tarball)}`);
59
106
  } finally {
@@ -61,6 +108,17 @@ try {
61
108
  if (tarball) await rm(tarball, { force: true });
62
109
  }
63
110
 
111
+ async function smokeStarter({ label, appName, initArgs, after, installRoot, tarball }) {
112
+ console.log(`[package-smoke] === ${label} starter ===`);
113
+ await run("npx", initArgs, {
114
+ cwd: installRoot,
115
+ env: { ...process.env, MADO_PACKAGE_SPEC: tarball },
116
+ });
117
+ const appRoot = join(installRoot, appName);
118
+ await run("npm", ["install"], { cwd: appRoot });
119
+ await after(appRoot);
120
+ }
121
+
64
122
  async function run(cmd, args, options) {
65
123
  console.log(`[package-smoke] ${cmd} ${args.join(" ")}`);
66
124
  try {
@@ -74,4 +132,4 @@ async function run(cmd, args, options) {
74
132
  if (err.stderr) process.stderr.write(err.stderr);
75
133
  throw err;
76
134
  }
77
- }
135
+ }
@@ -9,7 +9,7 @@
9
9
  // PREVIEW_AUTOBUILD=1.
10
10
  // 3. Starts a static server with:
11
11
  // - immutable cache for hashed bundles;
12
- // - SPA fallback to index.html;
12
+ // - SPA fallback to _mado/spa.html when present, else index.html;
13
13
  // - exact `out/` route files before SPA fallback;
14
14
  // - precompressed .gz / .br serving via Accept-Encoding.
15
15
  //
@@ -17,6 +17,7 @@
17
17
  // what a static host (nginx / Cloudflare Pages / S3) would serve.
18
18
 
19
19
  import { createServer } from "node:http";
20
+ import { existsSync, readFileSync } from "node:fs";
20
21
  import { readFile, stat, access } from "node:fs/promises";
21
22
  import { extname, join, resolve, sep } from "node:path";
22
23
  import { spawnSync } from "node:child_process";
@@ -55,6 +56,68 @@ const HOST = String(PREVIEW_FLAGS.host ?? process.env.HOST ?? "localhost");
55
56
  const AUTOBUILD = process.env.PREVIEW_AUTOBUILD === "1";
56
57
  const SKIP_BUILD = process.env.SKIP_BUILD === "1" || !AUTOBUILD;
57
58
 
59
+ // Active Vite base. We try the internal build bridge first (the
60
+ // `@madojs/mado/vite` plugin writes `_mado/build.json` during `vite
61
+ // build`); if `mado static` has already dropped that file before
62
+ // shipping the production artifact, we fall back to parsing the asset
63
+ // prefix out of `out/index.html`. Both paths converge on the same
64
+ // canonical "/" or "/prefix/" form so the preview server replays the
65
+ // real deployment URL shape regardless of pipeline stage.
66
+ const BASE = detectDeployedBase(OUT);
67
+
68
+ function detectDeployedBase(out) {
69
+ const fromBridge = readBridgeBase(out);
70
+ if (fromBridge != null) return fromBridge;
71
+ const fromHtml = readHtmlBase(out);
72
+ if (fromHtml != null) return fromHtml;
73
+ return "/";
74
+ }
75
+
76
+ function readBridgeBase(out) {
77
+ try {
78
+ const meta = JSON.parse(
79
+ readFileSync(join(out, "_mado/build.json"), "utf8"),
80
+ );
81
+ return meta?.base ? normalizeBase(meta.base) : null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function readHtmlBase(out) {
88
+ try {
89
+ const html = readFileSync(join(out, "index.html"), "utf8");
90
+ // Look at the first hashed asset; Vite always prefixes it with
91
+ // the deployed base.
92
+ const match = /(?:href|src)="(\/[^"]*?\/)assets\//.exec(html);
93
+ if (match && match[1]) return normalizeBase(match[1]);
94
+ // No prefix means root-deployed.
95
+ if (/(?:href|src)="\/assets\//.test(html)) return "/";
96
+ return null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ function normalizeBase(raw) {
103
+ if (!raw) return "/";
104
+ let s = String(raw).trim();
105
+ if (!s || s === "/") return "/";
106
+ if (!s.startsWith("/")) s = "/" + s;
107
+ if (!s.endsWith("/")) s = s + "/";
108
+ return s.replace(/\/+/g, "/");
109
+ }
110
+
111
+ function stripBase(pathname) {
112
+ if (BASE === "/") return pathname.startsWith("/") ? pathname : "/" + pathname;
113
+ if (pathname === BASE || pathname === BASE.slice(0, -1)) return "/";
114
+ if (pathname.startsWith(BASE)) {
115
+ const rest = pathname.slice(BASE.length - 1);
116
+ return rest || "/";
117
+ }
118
+ return pathname;
119
+ }
120
+
58
121
  const MIME = {
59
122
  ".html": "text/html; charset=utf-8",
60
123
  ".js": "text/javascript; charset=utf-8",
@@ -86,11 +149,13 @@ if (!(await exists(OUT))) {
86
149
  process.exit(1);
87
150
  }
88
151
 
89
- const spaShell = join(OUT, "index.html");
152
+ const spaShell = existsSync(join(OUT, "_mado", "spa.html"))
153
+ ? join(OUT, "_mado", "spa.html")
154
+ : join(OUT, "index.html");
90
155
  if (!(await exists(spaShell))) {
91
156
  console.error(
92
157
  `[preview] missing ${spaShell} — \`mado release\` did not produce an HTML entry.\n` +
93
- `[preview] Without it any non-baked route will 404 instead of falling back to the SPA.`,
158
+ `[preview] Without it any non-static route will 404 instead of falling back to the SPA.`,
94
159
  );
95
160
  process.exit(1);
96
161
  }
@@ -103,7 +168,24 @@ const isImmutable = (filename) =>
103
168
  const server = createServer(async (req, res) => {
104
169
  try {
105
170
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
106
- const pathname = decodeURIComponent(url.pathname);
171
+ const fullPathname = decodeURIComponent(url.pathname);
172
+ // Honour the deployed Vite base: redirect bare `/` to `/${base}/` and
173
+ // strip the prefix before any lookup, so `out/index.html`,
174
+ // `out/assets/...` and `out/docs/index.html` are reachable through
175
+ // the same URL shape the CDN serves.
176
+ if (BASE !== "/") {
177
+ const bareBase = BASE.slice(0, -1);
178
+ if (fullPathname === "/" || fullPathname === bareBase) {
179
+ res.writeHead(302, { location: BASE });
180
+ res.end();
181
+ return;
182
+ }
183
+ if (!fullPathname.startsWith(BASE) && fullPathname !== bareBase) {
184
+ res.writeHead(404).end("not found");
185
+ return;
186
+ }
187
+ }
188
+ const pathname = stripBase(fullPathname);
107
189
  const accepts = (req.headers["accept-encoding"] ?? "").toString();
108
190
 
109
191
  const target = await resolveTarget(pathname);
@@ -158,7 +240,12 @@ server.listen(PORT, HOST, async () => {
158
240
  const urlHost = HOST === "0.0.0.0" || HOST === "::" ? "localhost" : HOST;
159
241
  console.log("");
160
242
  console.log("Mado preview (production-like)");
161
- console.log(` url: http://${urlHost}:${PORT}/`);
243
+ // Print the URL the server actually serves the app at. When the
244
+ // deployment uses a non-trivial Vite `base`, the bare root just
245
+ // 302-redirects to the base prefix, which surprises users who copy
246
+ // the logged URL into a browser and land on "Not Found".
247
+ console.log(` url: http://${urlHost}:${PORT}${BASE}`);
248
+ console.log(` base: ${BASE}`);
162
249
  console.log(` out: ${OUT}`);
163
250
  console.log(" (Ctrl-C to stop)");
164
251
  console.log("");
@@ -216,7 +303,9 @@ async function resolveTarget(pathname) {
216
303
  // extension) deliberately 404 instead — otherwise a 200 on
217
304
  // /missing.png would mask real bugs.
218
305
  if (!extname(pathname)) {
219
- const spa = join(OUT, "index.html");
306
+ const spa = existsSync(join(OUT, "_mado", "spa.html"))
307
+ ? join(OUT, "_mado", "spa.html")
308
+ : join(OUT, "index.html");
220
309
  if (await exists(spa)) return spa;
221
310
  }
222
311
 
@@ -8,7 +8,11 @@ import { gzipSync } from "node:zlib";
8
8
 
9
9
  const API_ENTRY = "src/index.ts";
10
10
  const SAMPLE_ENTRY = "starters/default/src/main.ts";
11
- const API_GZIP_LIMIT = readLimit("MADO_SIZE_API_GZIP_LIMIT", 16 * 1024);
11
+ // Public API budget bumped from 16 KiB → 17 KiB to accommodate the
12
+ // base-aware router/navigation pack added in 0.12.0 (`src/router/base.ts`,
13
+ // stripBase/withBase wiring in navigation, manifest and static-runtime).
14
+ // Override per-environment via MADO_SIZE_API_GZIP_LIMIT.
15
+ const API_GZIP_LIMIT = readLimit("MADO_SIZE_API_GZIP_LIMIT", 17 * 1024);
12
16
  const SAMPLE_GZIP_LIMIT = readLimit("MADO_SIZE_SAMPLE_GZIP_LIMIT", 42 * 1024);
13
17
 
14
18
  let failed = false;