@madojs/mado 0.11.0 → 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 (221) hide show
  1. package/AGENTS.md +102 -25
  2. package/CHANGELOG.md +185 -1
  3. package/README.md +137 -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/bindings.js +3 -3
  12. package/dist/src/html/bindings.js.map +1 -1
  13. package/dist/src/html/parser.d.ts +1 -1
  14. package/dist/src/html/parser.js +1 -1
  15. package/dist/src/html/template.js +24 -10
  16. package/dist/src/html/template.js.map +1 -1
  17. package/dist/src/index.d.ts +8 -4
  18. package/dist/src/index.js +13 -3
  19. package/dist/src/index.js.map +1 -1
  20. package/dist/src/page.d.ts +62 -28
  21. package/dist/src/page.js +5 -0
  22. package/dist/src/page.js.map +1 -1
  23. package/dist/src/resource.js +2 -1
  24. package/dist/src/resource.js.map +1 -1
  25. package/dist/src/router/base.d.ts +65 -0
  26. package/dist/src/router/base.js +168 -0
  27. package/dist/src/router/base.js.map +1 -0
  28. package/dist/src/router/manifest.js +75 -31
  29. package/dist/src/router/manifest.js.map +1 -1
  30. package/dist/src/router/match.d.ts +16 -2
  31. package/dist/src/router/match.js +41 -1
  32. package/dist/src/router/match.js.map +1 -1
  33. package/dist/src/router/navigation.js +58 -9
  34. package/dist/src/router/navigation.js.map +1 -1
  35. package/dist/src/static-runtime.d.ts +81 -0
  36. package/dist/src/static-runtime.js +209 -0
  37. package/dist/src/static-runtime.js.map +1 -0
  38. package/dist/src/vite/index.d.ts +31 -3
  39. package/dist/src/vite/index.js +49 -18
  40. package/dist/src/vite/index.js.map +1 -1
  41. package/docs/README.md +5 -8
  42. package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
  43. package/docs/en/00-the-mado-way.md +1 -1
  44. package/docs/en/01-quickstart.md +183 -0
  45. package/docs/en/10-pages-and-components.md +271 -0
  46. package/docs/en/11-templates-and-signals.md +211 -0
  47. package/docs/en/12-routing.md +229 -0
  48. package/docs/en/13-data.md +225 -0
  49. package/docs/en/14-forms.md +244 -0
  50. package/docs/en/15-static-snapshots.md +181 -0
  51. package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
  52. package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
  53. package/docs/en/{14-testing.md → 22-testing.md} +6 -5
  54. package/docs/en/23-cookbook.md +292 -0
  55. package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
  56. package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
  57. package/docs/en/40-llm-guide.md +776 -0
  58. package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
  59. package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
  60. package/docs/en/README.md +97 -27
  61. package/docs/recipes/nginx/Containerfile +26 -0
  62. package/docs/recipes/nginx/nginx.conf +42 -0
  63. package/llms.txt +246 -211
  64. package/package.json +17 -10
  65. package/scripts/bake.mjs +4 -568
  66. package/scripts/cli/generate.mjs +55 -5
  67. package/scripts/cli/help.mjs +11 -5
  68. package/scripts/cli/index.mjs +20 -4
  69. package/scripts/cli/init.mjs +7 -2
  70. package/scripts/cli/release.mjs +20 -68
  71. package/scripts/docs-lint.mjs +170 -0
  72. package/scripts/package-smoke.mjs +67 -9
  73. package/scripts/preview.mjs +95 -6
  74. package/scripts/size-budget.mjs +5 -1
  75. package/scripts/static/browser.mjs +654 -0
  76. package/scripts/static/discover.mjs +281 -0
  77. package/scripts/static/output.mjs +141 -0
  78. package/scripts/static/serialize.mjs +212 -0
  79. package/scripts/static/server.mjs +167 -0
  80. package/scripts/static.mjs +191 -0
  81. package/starters/default/README.md +57 -55
  82. package/starters/default/package.json +3 -9
  83. package/starters/default/src/app.routes.ts +20 -33
  84. package/starters/default/src/components/feature-card.component.ts +40 -0
  85. package/starters/default/src/components/live-counter.component.ts +57 -0
  86. package/starters/default/src/content/guides.ts +55 -0
  87. package/starters/default/src/main.ts +9 -13
  88. package/starters/default/src/pages/app.page.ts +41 -0
  89. package/starters/default/src/pages/guide.page.ts +40 -0
  90. package/starters/default/src/pages/home.page.ts +49 -0
  91. package/starters/default/src/pages/not-found.page.ts +18 -0
  92. package/starters/default/src/styles/document.css +39 -0
  93. package/starters/default/src/styles/reset.css +37 -0
  94. package/starters/default/src/styles/tokens.css +30 -0
  95. package/starters/default/src/styles.d.ts +3 -1
  96. package/starters/default/src/vite-env.d.ts +1 -1
  97. package/starters/default/vite.config.ts +14 -1
  98. package/starters/modular/README.md +142 -0
  99. package/starters/modular/index.html +13 -0
  100. package/starters/modular/package.json +30 -0
  101. package/starters/modular/src/app.routes.ts +39 -0
  102. package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
  103. package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
  104. package/starters/modular/src/main.ts +16 -0
  105. package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
  106. package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
  107. package/starters/modular/src/modules/home/not-found.page.ts +14 -0
  108. package/starters/modular/src/styles.d.ts +1 -0
  109. package/starters/modular/src/vite-env.d.ts +1 -0
  110. package/starters/modular/tsconfig.json +24 -0
  111. package/starters/modular/vite.config.ts +131 -0
  112. package/TODO.md +0 -79
  113. package/docs/en/01-routing.md +0 -152
  114. package/docs/en/02-project-layout.md +0 -124
  115. package/docs/en/03-static-bake.md +0 -249
  116. package/docs/en/04-ide-setup.md +0 -162
  117. package/docs/en/07-llm-pitfalls.md +0 -724
  118. package/docs/en/08-llm-zero-history-test.md +0 -56
  119. package/docs/en/09-shadow-vs-light-dom.md +0 -174
  120. package/docs/en/11-layouts.md +0 -113
  121. package/docs/en/12-auth-and-api.md +0 -124
  122. package/docs/en/16-bake-cookbook.md +0 -100
  123. package/docs/en/17-shadow-dom-forms.md +0 -192
  124. package/docs/fr/00-the-mado-way.md +0 -85
  125. package/docs/fr/01-routing.md +0 -120
  126. package/docs/fr/02-project-layout.md +0 -84
  127. package/docs/fr/03-static-bake.md +0 -289
  128. package/docs/fr/04-ide-setup.md +0 -162
  129. package/docs/fr/05-why-mado.md +0 -193
  130. package/docs/fr/06-for-backenders.md +0 -438
  131. package/docs/fr/07-llm-pitfalls.md +0 -625
  132. package/docs/fr/08-llm-zero-history-test.md +0 -38
  133. package/docs/fr/09-shadow-vs-light-dom.md +0 -65
  134. package/docs/fr/10-app-architecture.md +0 -138
  135. package/docs/fr/11-layouts.md +0 -47
  136. package/docs/fr/12-auth-and-api.md +0 -76
  137. package/docs/fr/13-deployment.md +0 -57
  138. package/docs/fr/14-testing.md +0 -41
  139. package/docs/fr/15-error-handling.md +0 -50
  140. package/docs/fr/16-bake-cookbook.md +0 -88
  141. package/docs/fr/17-shadow-dom-forms.md +0 -196
  142. package/docs/fr/18-api-freeze-map.md +0 -63
  143. package/docs/fr/19-reactivity-ordering.md +0 -97
  144. package/docs/fr/20-v1-stability.md +0 -88
  145. package/docs/fr/README.md +0 -27
  146. package/docs/ru/00-the-mado-way.md +0 -84
  147. package/docs/ru/01-routing.md +0 -119
  148. package/docs/ru/02-project-layout.md +0 -84
  149. package/docs/ru/03-static-bake.md +0 -250
  150. package/docs/ru/04-ide-setup.md +0 -144
  151. package/docs/ru/05-why-mado.md +0 -193
  152. package/docs/ru/06-for-backenders.md +0 -428
  153. package/docs/ru/07-llm-pitfalls.md +0 -624
  154. package/docs/ru/08-llm-zero-history-test.md +0 -57
  155. package/docs/ru/09-shadow-vs-light-dom.md +0 -63
  156. package/docs/ru/10-app-architecture.md +0 -152
  157. package/docs/ru/11-layouts.md +0 -47
  158. package/docs/ru/12-auth-and-api.md +0 -75
  159. package/docs/ru/13-deployment.md +0 -66
  160. package/docs/ru/14-testing.md +0 -50
  161. package/docs/ru/15-error-handling.md +0 -56
  162. package/docs/ru/16-bake-cookbook.md +0 -95
  163. package/docs/ru/17-shadow-dom-forms.md +0 -193
  164. package/docs/ru/18-api-freeze-map.md +0 -64
  165. package/docs/ru/19-reactivity-ordering.md +0 -95
  166. package/docs/ru/20-v1-stability.md +0 -82
  167. package/docs/ru/README.md +0 -25
  168. package/docs/uk/00-the-mado-way.md +0 -82
  169. package/docs/uk/01-routing.md +0 -76
  170. package/docs/uk/02-project-layout.md +0 -73
  171. package/docs/uk/03-static-bake.md +0 -48
  172. package/docs/uk/04-ide-setup.md +0 -26
  173. package/docs/uk/05-why-mado.md +0 -34
  174. package/docs/uk/06-for-backenders.md +0 -55
  175. package/docs/uk/07-llm-pitfalls.md +0 -145
  176. package/docs/uk/08-llm-zero-history-test.md +0 -34
  177. package/docs/uk/09-shadow-vs-light-dom.md +0 -58
  178. package/docs/uk/10-app-architecture.md +0 -97
  179. package/docs/uk/11-layouts.md +0 -47
  180. package/docs/uk/12-auth-and-api.md +0 -70
  181. package/docs/uk/13-deployment.md +0 -40
  182. package/docs/uk/14-testing.md +0 -34
  183. package/docs/uk/15-error-handling.md +0 -32
  184. package/docs/uk/16-bake-cookbook.md +0 -36
  185. package/docs/uk/17-shadow-dom-forms.md +0 -193
  186. package/docs/uk/18-api-freeze-map.md +0 -61
  187. package/docs/uk/19-reactivity-ordering.md +0 -95
  188. package/docs/uk/20-v1-stability.md +0 -83
  189. package/docs/uk/README.md +0 -27
  190. package/starters/default/src/modules/home/not-found.page.ts +0 -11
  191. /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
  192. /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
  193. /package/starters/{default → modular}/.editorconfig +0 -0
  194. /package/starters/{default → modular}/eslint.config.mjs +0 -0
  195. /package/starters/{default → modular}/public/favicon.svg +0 -0
  196. /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
  197. /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
  198. /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
  199. /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
  200. /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
  201. /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
  202. /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
  203. /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
  204. /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
  205. /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
  206. /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
  207. /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
  208. /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
  209. /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
  210. /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
  211. /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
  212. /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
  213. /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
  214. /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
  215. /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
  216. /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
  217. /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
  218. /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
  219. /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
  220. /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
  221. /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
@@ -0,0 +1,167 @@
1
+ import { createServer } from "node:http";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { extname, join, resolve, sep } from "node:path";
4
+
5
+ import { injectSnapshotMode } from "./serialize.mjs";
6
+
7
+ /**
8
+ * Normalise a Vite-style base into the canonical Mado form: `"/"` for the
9
+ * root, otherwise `"/prefix/"` (leading and trailing slash, no doubles).
10
+ * Mirrors src/router/base.ts.normalizeBase so the capture pipeline never
11
+ * disagrees with the runtime router.
12
+ */
13
+ function normalizeBase(raw) {
14
+ if (!raw) return "/";
15
+ let s = String(raw).trim();
16
+ if (!s || s === "/") return "/";
17
+ if (!s.startsWith("/")) s = "/" + s;
18
+ if (!s.endsWith("/")) s = s + "/";
19
+ return s.replace(/\/+/g, "/");
20
+ }
21
+
22
+ /**
23
+ * Strip the active base prefix off a request pathname so the capture
24
+ * server can look it up against route records (which are always
25
+ * registered without a base).
26
+ */
27
+ function stripBase(pathname, base) {
28
+ const b = normalizeBase(base);
29
+ if (!pathname) return "/";
30
+ if (b === "/") return pathname.startsWith("/") ? pathname : "/" + pathname;
31
+ if (pathname === b || pathname === b.slice(0, -1)) return "/";
32
+ if (pathname.startsWith(b)) {
33
+ const rest = pathname.slice(b.length - 1);
34
+ return rest || "/";
35
+ }
36
+ return pathname;
37
+ }
38
+
39
+ const MIME = {
40
+ ".html": "text/html; charset=utf-8",
41
+ ".js": "text/javascript; charset=utf-8",
42
+ ".mjs": "text/javascript; charset=utf-8",
43
+ ".css": "text/css; charset=utf-8",
44
+ ".json": "application/json; charset=utf-8",
45
+ ".svg": "image/svg+xml",
46
+ ".ico": "image/x-icon",
47
+ ".png": "image/png",
48
+ ".jpg": "image/jpeg",
49
+ ".jpeg": "image/jpeg",
50
+ ".webp": "image/webp",
51
+ ".woff2": "font/woff2",
52
+ ".xml": "application/xml; charset=utf-8",
53
+ ".map": "application/json; charset=utf-8",
54
+ };
55
+
56
+ /**
57
+ * Internal HTTP server that hosts the Vite build output during snapshot
58
+ * capture. It speaks the real deployment URL shape (the active Vite
59
+ * `base` is honoured for both static routes and asset lookups), so the
60
+ * runtime router sees the same `location.pathname` as production.
61
+ */
62
+ export async function createStaticCaptureServer({ outDir, shellHtml, records, base = "/" }) {
63
+ const normalizedBase = normalizeBase(base);
64
+ const routeRecords = new Map(records.map((record) => [record.pathname, record]));
65
+
66
+ const server = createServer(async (req, res) => {
67
+ try {
68
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
69
+ const browserPath = normalizePathname(url.pathname);
70
+ // The router uses BASE-FREE route pathnames, but assets in
71
+ // `out/assets/...` are served from the same base-relative URLs the
72
+ // production CDN exposes. Strip the active base before any record
73
+ // lookup or file resolution so both halves use one consistent
74
+ // pathname space.
75
+ const routePath = stripBase(browserPath, normalizedBase);
76
+
77
+ const record = routeRecords.get(routePath);
78
+ if (record) {
79
+ sendHtml(res, injectSnapshotMode(shellHtml, record));
80
+ return;
81
+ }
82
+
83
+ const file = await resolveStaticFile(outDir, routePath);
84
+ if (file) {
85
+ const data = await readFile(file);
86
+ res.writeHead(200, {
87
+ "content-type": MIME[extname(file).toLowerCase()] ?? "application/octet-stream",
88
+ "cache-control": "no-store",
89
+ });
90
+ res.end(data);
91
+ return;
92
+ }
93
+
94
+ // SPA fallback: any non-asset path falls back to the snapshot shell
95
+ // so client-side routing handles it. We use the original browser
96
+ // pathname (not the stripped one) inside `injectSnapshotMode` so
97
+ // that the runtime `location.pathname` matches production.
98
+ if (!extname(routePath)) {
99
+ sendHtml(
100
+ res,
101
+ injectSnapshotMode(shellHtml, { pathname: routePath, params: {} }),
102
+ );
103
+ return;
104
+ }
105
+
106
+ res.writeHead(404, { "cache-control": "no-store" });
107
+ res.end("not found");
108
+ } catch (err) {
109
+ res.writeHead(500, {
110
+ "content-type": "text/plain; charset=utf-8",
111
+ "cache-control": "no-store",
112
+ });
113
+ res.end(String(err?.stack ?? err));
114
+ }
115
+ });
116
+
117
+ await new Promise((resolveListen, rejectListen) => {
118
+ server.once("error", rejectListen);
119
+ server.listen(0, "127.0.0.1", () => {
120
+ server.off("error", rejectListen);
121
+ resolveListen();
122
+ });
123
+ });
124
+
125
+ const address = server.address();
126
+ if (!address || typeof address === "string") {
127
+ throw new Error("[mado:static] failed to bind internal capture server.");
128
+ }
129
+
130
+ return {
131
+ origin: `http://127.0.0.1:${address.port}`,
132
+ close: () => new Promise((resolveClose) => server.close(resolveClose)),
133
+ };
134
+ }
135
+
136
+ function sendHtml(res, html) {
137
+ res.writeHead(200, {
138
+ "content-type": "text/html; charset=utf-8",
139
+ "cache-control": "no-store",
140
+ });
141
+ res.end(html);
142
+ }
143
+
144
+ async function resolveStaticFile(outDir, pathname) {
145
+ const raw = pathname === "/" ? "/index.html" : pathname;
146
+ const candidate = resolve(join(outDir, decodeURIComponent(raw)));
147
+ if (!candidate.startsWith(resolve(outDir) + sep) && candidate !== resolve(outDir)) {
148
+ return null;
149
+ }
150
+ try {
151
+ const s = await stat(candidate);
152
+ if (s.isFile()) return candidate;
153
+ if (s.isDirectory()) {
154
+ const index = join(candidate, "index.html");
155
+ if ((await stat(index)).isFile()) return index;
156
+ }
157
+ } catch {
158
+ /* not found */
159
+ }
160
+ return null;
161
+ }
162
+
163
+ function normalizePathname(pathname) {
164
+ if (!pathname.startsWith("/")) return `/${pathname}`;
165
+ if (pathname.length > 1 && pathname.endsWith("/")) return pathname.slice(0, -1);
166
+ return pathname;
167
+ }
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, writeSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+
6
+ import { parseFlags } from "./_config.mjs";
7
+ import { discoverStaticRoutes } from "./static/discover.mjs";
8
+ import { createStaticCaptureServer } from "./static/server.mjs";
9
+ import { captureStaticRoutes } from "./static/browser.mjs";
10
+ import {
11
+ cleanupTemp,
12
+ dropBuildBridge,
13
+ prepareStaticOutput,
14
+ promoteCapturedRoutes,
15
+ promoteSpaShell,
16
+ writeCapturedRoutes,
17
+ writeStaticDeploymentFiles,
18
+ } from "./static/output.mjs";
19
+
20
+ const { flags } = parseFlags(process.argv.slice(2));
21
+ const projectRoot = resolve(process.cwd());
22
+ const outDir = resolve(
23
+ projectRoot,
24
+ typeof flags.out === "string" ? flags.out : "out",
25
+ );
26
+ const timeout = Number(flags.timeout ?? 30_000);
27
+
28
+ let tempRootForCleanup = null;
29
+ try {
30
+ console.log(`[static] artifact: ${outDir}`);
31
+
32
+ // Source the public origin and Vite base from (in order):
33
+ // --base-url, --base / MADO_SITE env, _mado/build.json (the bridge
34
+ // emitted by the @madojs/mado/vite plugin). Static routes REQUIRE
35
+ // a non-localhost public origin; we never invent one.
36
+ const buildMeta = readBuildMeta(outDir);
37
+ const site = pickSite({ flags, buildMeta });
38
+ const base = pickBase({ flags, buildMeta });
39
+ validateSite(site, "site");
40
+
41
+ // Ordering matters: discover and validate the manifest BEFORE we
42
+ // touch the deployed artefact. A manifest that throws (missing
43
+ // params, duplicate URL, ...) used to leave a half-prepared
44
+ // staging directory behind because we provisioned the temp tree
45
+ // before discovery; now the temp tree is created only after we know
46
+ // the inputs are sound.
47
+ const { records } = await discoverStaticRoutes({
48
+ projectRoot,
49
+ entry: typeof flags.entry === "string" ? flags.entry : undefined,
50
+ });
51
+
52
+ console.log(`[static] discovered ${records.length} static route(s)`);
53
+
54
+ if (records.length > 0 && !site) {
55
+ throw new Error(
56
+ "[mado:static] missing public origin for static routes.\n" +
57
+ "Provide one of:\n" +
58
+ " mado static --base-url https://your.site\n" +
59
+ " MADO_SITE=https://your.site mado static\n" +
60
+ " mado({ site: \"https://your.site\" }) in vite.config.ts",
61
+ );
62
+ }
63
+
64
+ const { shellHtml, tempRoot, routesDir, stagedSpaPath } =
65
+ await prepareStaticOutput(outDir);
66
+ tempRootForCleanup = tempRoot;
67
+
68
+ const publicOrigin = site ?? "";
69
+ const baseUrl = publicOrigin
70
+ ? joinSite(publicOrigin, base)
71
+ : "/";
72
+
73
+ if (records.length > 0) {
74
+ const server = await createStaticCaptureServer({
75
+ outDir,
76
+ shellHtml,
77
+ records,
78
+ base,
79
+ });
80
+ try {
81
+ const captured = await captureStaticRoutes({
82
+ records,
83
+ serverOrigin: server.origin,
84
+ baseUrl,
85
+ base,
86
+ site: publicOrigin,
87
+ timeout,
88
+ browserChannel:
89
+ typeof flags["browser-channel"] === "string"
90
+ ? flags["browser-channel"]
91
+ : undefined,
92
+ browserPath:
93
+ typeof flags["browser-path"] === "string"
94
+ ? flags["browser-path"]
95
+ : undefined,
96
+ });
97
+ // First write everything to the temp staging tree, only then
98
+ // promote into `out/`. A capture failure aborts before any of the
99
+ // existing deployment files are touched.
100
+ await writeCapturedRoutes(routesDir, captured);
101
+ await promoteCapturedRoutes({ outDir, routesDir, captured });
102
+ console.log(`[static] captured ${captured.length} route snapshot(s)`);
103
+ } finally {
104
+ await server.close();
105
+ }
106
+ }
107
+
108
+ // SPA shell is promoted only after every route survived capture and
109
+ // promotion. On a re-run with broken pages this guarantees that the
110
+ // previous (working) `_mado/spa.html` is preserved.
111
+ await promoteSpaShell({ outDir, stagedSpaPath });
112
+
113
+ await writeStaticDeploymentFiles({
114
+ outDir,
115
+ records,
116
+ baseUrl,
117
+ site: publicOrigin,
118
+ base,
119
+ });
120
+
121
+ // Drop the internal build bridge so the production artifact does not
122
+ // ship Vite's resolved view of the project (site, base, assetsDir).
123
+ await dropBuildBridge(outDir);
124
+ console.log(`[static] done`);
125
+ } catch (err) {
126
+ // Defer the actual termination: `process.exit()` is synchronous and
127
+ // would race the awaited cleanup in the `finally` block, leaving the
128
+ // OS to garbage-collect a half-promoted staging directory. Setting
129
+ // `exitCode` lets Node finish the microtask queue (including the
130
+ // cleanup below) and then exit with the failure status.
131
+ writeSync(2, `${err?.stack ?? err}\n`);
132
+ process.exitCode = 1;
133
+ } finally {
134
+ if (tempRootForCleanup) {
135
+ try {
136
+ await cleanupTemp(tempRootForCleanup);
137
+ } catch {
138
+ /* best effort */
139
+ }
140
+ }
141
+ }
142
+
143
+ function readBuildMeta(outDir) {
144
+ const file = join(outDir, "_mado/build.json");
145
+ if (!existsSync(file)) return null;
146
+ try {
147
+ return JSON.parse(readFileSync(file, "utf8"));
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function pickSite({ flags, buildMeta }) {
154
+ const fromFlag = flags["base-url"] ?? flags.site;
155
+ if (typeof fromFlag === "string" && fromFlag.length > 0) return fromFlag;
156
+ if (typeof process.env.MADO_SITE === "string" && process.env.MADO_SITE.length > 0) {
157
+ return process.env.MADO_SITE;
158
+ }
159
+ if (buildMeta?.site) return buildMeta.site;
160
+ return null;
161
+ }
162
+
163
+ function pickBase({ flags, buildMeta }) {
164
+ const fromFlag = flags.base;
165
+ if (typeof fromFlag === "string" && fromFlag.length > 0) return fromFlag;
166
+ if (buildMeta?.base) return buildMeta.base;
167
+ return "/";
168
+ }
169
+
170
+ function validateSite(site, label) {
171
+ if (!site) return;
172
+ let u;
173
+ try {
174
+ u = new URL(site);
175
+ } catch {
176
+ throw new Error(`[mado:static] ${label} is not a valid URL: ${site}`);
177
+ }
178
+ if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1") {
179
+ throw new Error(
180
+ `[mado:static] ${label} cannot point at localhost (${site}). ` +
181
+ "Set the public origin via --base-url or mado({ site }) in vite.config.ts.",
182
+ );
183
+ }
184
+ }
185
+
186
+ function joinSite(site, base) {
187
+ const left = site.replace(/\/+$/, "");
188
+ const right = (base ?? "/").startsWith("/") ? base : `/${base ?? "/"}`;
189
+ const merged = `${left}${right}`;
190
+ return merged.endsWith("/") ? merged.slice(0, -1) : merged;
191
+ }
@@ -1,74 +1,76 @@
1
- # Mado Starter
1
+ # Mado universal starter
2
2
 
3
- Canonical starter for Mado business apps: admin panels, internal tools and
4
- long-lived SPAs.
3
+ A calm native-first web framework for sites and apps. This starter
4
+ proves the whole promise in ~15 files:
5
5
 
6
- ## Commands
6
+ - one Shadow Component (`feature-card`) shared by the static landing
7
+ and the live SPA,
8
+ - one component model end-to-end,
9
+ - a build-time seed for a dynamic static route (`/guide/:slug`),
10
+ - a private SPA route (`/app`) that falls back through `_mado/spa.html`,
11
+ - per-route head metadata, sitemap, canonical and `og:url`
12
+ auto-fallback.
13
+
14
+ ## Run it
7
15
 
8
16
  ```bash
9
17
  npm install
10
- npm run dev
11
- npm run release
18
+ npm run dev # Vite dev server
19
+ npm run build # Vite production SPA build
20
+ npm run release # vite build + browser-rendered snapshots → out/
21
+ npm run preview # serve out/ like a real static host
12
22
  ```
13
23
 
14
- `npm run release` writes the deploy artifact to `out/`.
24
+ `mado release` requires a public origin so it can build absolute
25
+ canonical URLs. Set it once in `vite.config.ts`:
15
26
 
16
- ## Shape
27
+ ```ts
28
+ mado({ site: "https://your-app.example" })
29
+ ```
17
30
 
18
- ```txt
19
- public/ static assets copied by Vite
20
- src/
21
- main.ts imports global CSS and mounts the router
22
- app.routes.ts app map: zones, layouts, guards and modules
23
- layouts/ app-zone shells
24
- shared/
25
- http/ HTTP client and interceptors
26
- lib/ pure utilities
27
- styles/ tokens, reset, shell and content CSS
28
- ui/ reusable x-* components
29
- modules/ business modules
30
- <name>/
31
- <name>.routes.ts
32
- <name>.public.ts
33
- <name>.types.ts
34
- pages/
35
- data/
36
- api/
37
- components/
38
- _contracts/
31
+ …or override per environment:
32
+
33
+ ```bash
34
+ mado release --base-url https://staging.example
35
+ MADO_SITE=https://staging.example mado release
39
36
  ```
40
37
 
41
- ## CSS Contract
38
+ ## File map
39
+
40
+ ```
41
+ src/
42
+ main.ts # mounts the router into #app
43
+ app.routes.ts # the URL → page table
42
44
 
43
- - `tokens.css` defines CSS custom properties and is safe for Shadow DOM
44
- components through `var(...)`.
45
- - `reset.css` normalizes the document/light DOM surface.
46
- - `shell.css` styles app-zone layouts from `src/layouts/`.
47
- - `content.css` styles page-level light DOM: forms, tables, prose and simple
48
- states.
49
- - Reusable leaf components keep their own styles in ``css`...` `` inside
50
- `component()` options.
45
+ pages/
46
+ home.page.ts # public, static
47
+ guide.page.ts # dynamic static (/guide/:slug)
48
+ app.page.ts # SPA-only interactive route
49
+ not-found.page.ts # 404 (SPA fallback)
51
50
 
52
- Vite uses Lightning CSS for CSS transforms/minification in this starter.
51
+ components/
52
+ feature-card.component.ts # shared by landing + app
53
+ live-counter.component.ts # reactivity demo
53
54
 
54
- ## Generate Files
55
+ content/
56
+ guides.ts # browser-safe content module
55
57
 
56
- ```bash
57
- npm run new -- module billing
58
- npm run new -- page billing/pages/invoices-list
59
- npm run new -- connector billing/api/stripe
60
- npm run new -- resource billing/data/invoices
61
- npm run new -- service billing/cart
62
- npm run new -- form billing/invoice
63
- npm run new -- component billing/components/invoice-status-badge
64
- npm run new -- guard billing/billing
65
- npm run new -- layout admin-shell
58
+ styles/
59
+ tokens.css # design tokens (custom properties)
60
+ reset.css # tiny modern reset
61
+ document.css # light-DOM document layout
66
62
  ```
67
63
 
68
- The generator writes files only. Wire new routes in `src/app.routes.ts` or the
69
- module route map by hand.
64
+ ## Going further
70
65
 
71
- ## More
66
+ If you need module boundaries, layouts, guards, an auth shell, an HTTP
67
+ client and a Stripe-style billing example, scaffold the modular
68
+ starter instead:
69
+
70
+ ```bash
71
+ mado init my-app --starter modular
72
+ ```
72
73
 
73
- The full architecture guide lives in the framework docs:
74
- https://github.com/madojs/mado/tree/main/docs/en
74
+ The modular starter is the reference architecture for long-lived
75
+ business apps. Both starters target the same Mado runtime — they only
76
+ differ in how much structure they pre-create for you.
@@ -5,25 +5,19 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "mado dev",
8
- "build": "mado release",
8
+ "build": "mado build",
9
9
  "typecheck": "mado typecheck",
10
- "test": "npm run typecheck && npm run lint",
10
+ "test": "mado typecheck",
11
11
  "release": "mado release",
12
12
  "preview": "mado preview",
13
- "lint": "eslint \"src/**/*.ts\"",
14
- "lint:fix": "eslint \"src/**/*.ts\" --fix",
15
13
  "new": "mado new"
16
14
  },
17
15
  "dependencies": {
18
16
  "@madojs/mado": "__MADOJS_VERSION__"
19
17
  },
20
18
  "devDependencies": {
21
- "@typescript-eslint/eslint-plugin": "^8.0.0",
22
- "@typescript-eslint/parser": "^8.0.0",
23
- "eslint": "^9.0.0",
24
- "eslint-plugin-boundaries": "^5.0.0",
25
- "linkedom": "^0.18.12",
26
19
  "lightningcss": "^1.32.0",
20
+ "playwright-core": "^1.47.0",
27
21
  "typescript": "^5.5.0",
28
22
  "vite": "^8.0.16"
29
23
  }
@@ -1,39 +1,26 @@
1
- // Single source of truth for the application route table.
1
+ // Single source of truth for the app's URL → page mapping.
2
2
  //
3
- // This file is the APP MAP. Reading it = understanding the app.
4
- //
5
- // One rule:
6
- // Modules export plain `routes` maps. The choice of SHELL and GUARD for
7
- // each zone of the app is made HERE, by wrapping a module's routes with
8
- // a `layout({...})` block.
9
- //
10
- // `manifest` is exported separately so `mado bake` can discover pages that
11
- // declare `bake: { paths, data }`.
12
-
13
- import { layout, routes } from "@madojs/mado";
14
-
15
- import { requireAuth } from "./modules/auth/auth.public";
16
- import { authRoutes } from "./modules/auth/auth.routes";
17
- import { billingRoutes } from "./modules/billing/billing.routes";
3
+ // Mado has no "file-based routes". You list pages explicitly, in order
4
+ // of specificity, and the router takes care of code-splitting via the
5
+ // dynamic imports below.
6
+ import { routes } from "@madojs/mado";
18
7
 
19
8
  export const manifest = {
20
- // Public landing (no shell, no guard).
21
- "/": () => import("./modules/home/home.page"),
22
-
23
- // AUTH ZONE — centered card, no guard.
24
- "/login": layout({
25
- layout: () => import("./layouts/auth-shell.layout"),
26
- routes: authRoutes,
27
- }),
28
-
29
- // APP ZONE header + nav, guarded by requireAuth.
30
- "/billing": layout({
31
- layout: () => import("./layouts/app-shell.layout"),
32
- guard: requireAuth,
33
- routes: billingRoutes,
34
- }),
35
-
36
- "*": () => import("./modules/home/not-found.page"),
9
+ // Public landing `static: true` makes `mado release` snapshot this
10
+ // route into `out/index.html` so search engines see a fully rendered
11
+ // document without running JS.
12
+ "/": () => import("./pages/home.page"),
13
+ // Dynamic static route: `mado static` calls `paths()` at build time,
14
+ // captures one HTML file per slug, and seeds `initialData` so the
15
+ // first client render does not re-fetch what the snapshot already
16
+ // shows.
17
+ "/guide/:slug": () => import("./pages/guide.page"),
18
+ // SPA-only route: deliberately non-static. Lives in `_mado/spa.html`
19
+ // at deploy time and is served whenever the CDN cannot find a static
20
+ // document.
21
+ "/app": () => import("./pages/app.page"),
22
+ // 404 — matched after every literal pattern.
23
+ "*": () => import("./pages/not-found.page"),
37
24
  };
38
25
 
39
26
  export default routes(manifest);
@@ -0,0 +1,40 @@
1
+ // `feature-card` is a leaf component shared by the home page (in the
2
+ // static snapshot) and by `app.page.ts` (in the live SPA). The same
3
+ // open-shadow custom element runs in both contexts — that is the core
4
+ // promise of the universal model.
5
+ import { component, css, html } from "@madojs/mado";
6
+
7
+ component(
8
+ "feature-card",
9
+ (ctx) => {
10
+ const title = ctx.attr("title", "");
11
+ return () => html`
12
+ <article class="card">
13
+ <h3>${() => title()}</h3>
14
+ <p><slot></slot></p>
15
+ </article>
16
+ `;
17
+ },
18
+ {
19
+ styles: css`
20
+ :host {
21
+ display: block;
22
+ }
23
+ .card {
24
+ padding: 1.25rem 1.5rem;
25
+ border: 1px solid var(--mado-border);
26
+ border-radius: var(--mado-radius);
27
+ background: var(--mado-surface);
28
+ }
29
+ h3 {
30
+ margin: 0 0 0.5rem;
31
+ font-size: 1.1rem;
32
+ font-weight: 600;
33
+ }
34
+ p {
35
+ margin: 0;
36
+ color: var(--mado-fg-muted);
37
+ }
38
+ `,
39
+ },
40
+ );
@@ -0,0 +1,57 @@
1
+ // `live-counter` showcases reactivity: signal-driven text, a click
2
+ // handler that mutates the signal, and an open shadow root that
3
+ // participates in the static snapshot via Declarative Shadow DOM.
4
+ import { component, css, html, signal } from "@madojs/mado";
5
+
6
+ component(
7
+ "live-counter",
8
+ () => {
9
+ const count = signal(0);
10
+ return () => html`
11
+ <div class="counter">
12
+ <button
13
+ class="bump"
14
+ @click=${() => count.update((n) => n + 1)}
15
+ aria-label="Increment"
16
+ >
17
+ +
18
+ </button>
19
+ <span class="value">${() => count()}</span>
20
+ </div>
21
+ `;
22
+ },
23
+ {
24
+ styles: css`
25
+ :host {
26
+ display: inline-block;
27
+ }
28
+ .counter {
29
+ display: inline-flex;
30
+ align-items: center;
31
+ gap: 0.75rem;
32
+ padding: 0.5rem 0.75rem;
33
+ border: 1px solid var(--mado-border);
34
+ border-radius: var(--mado-radius);
35
+ background: var(--mado-surface);
36
+ }
37
+ .bump {
38
+ font: inherit;
39
+ width: 2rem;
40
+ height: 2rem;
41
+ border-radius: 999px;
42
+ border: 1px solid var(--mado-border);
43
+ background: var(--mado-bg);
44
+ cursor: pointer;
45
+ }
46
+ .bump:hover {
47
+ background: var(--mado-surface-hover);
48
+ }
49
+ .value {
50
+ font-variant-numeric: tabular-nums;
51
+ font-weight: 600;
52
+ min-width: 2ch;
53
+ text-align: right;
54
+ }
55
+ `,
56
+ },
57
+ );