@madojs/mado 0.11.1 → 0.13.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 (298) hide show
  1. package/AGENTS.md +116 -32
  2. package/CHANGELOG.md +216 -1
  3. package/README.md +158 -173
  4. package/dist/src/component.d.ts +6 -3
  5. package/dist/src/component.d.ts.map +1 -0
  6. package/dist/src/component.js +163 -36
  7. package/dist/src/component.js.map +1 -1
  8. package/dist/src/context.d.ts +13 -32
  9. package/dist/src/context.d.ts.map +1 -0
  10. package/dist/src/context.js +73 -48
  11. package/dist/src/context.js.map +1 -1
  12. package/dist/src/css.d.ts +1 -0
  13. package/dist/src/css.d.ts.map +1 -0
  14. package/dist/src/css.js +34 -5
  15. package/dist/src/css.js.map +1 -1
  16. package/dist/src/devtools-hook.d.ts +14 -0
  17. package/dist/src/devtools-hook.d.ts.map +1 -0
  18. package/dist/src/devtools-hook.js +27 -0
  19. package/dist/src/devtools-hook.js.map +1 -0
  20. package/dist/src/devtools.d.ts +24 -22
  21. package/dist/src/devtools.d.ts.map +1 -0
  22. package/dist/src/devtools.js +230 -58
  23. package/dist/src/devtools.js.map +1 -1
  24. package/dist/src/diagnostics.d.ts +12 -0
  25. package/dist/src/diagnostics.d.ts.map +1 -0
  26. package/dist/src/diagnostics.js +68 -8
  27. package/dist/src/diagnostics.js.map +1 -1
  28. package/dist/src/each.d.ts +1 -5
  29. package/dist/src/each.d.ts.map +1 -0
  30. package/dist/src/each.js +0 -8
  31. package/dist/src/each.js.map +1 -1
  32. package/dist/src/forms.d.ts +27 -96
  33. package/dist/src/forms.d.ts.map +1 -0
  34. package/dist/src/forms.js +184 -394
  35. package/dist/src/forms.js.map +1 -1
  36. package/dist/src/head.d.ts +7 -6
  37. package/dist/src/head.d.ts.map +1 -0
  38. package/dist/src/head.js +8 -7
  39. package/dist/src/head.js.map +1 -1
  40. package/dist/src/html/bindings.d.ts +1 -0
  41. package/dist/src/html/bindings.d.ts.map +1 -0
  42. package/dist/src/html/bindings.js.map +1 -1
  43. package/dist/src/html/parser.d.ts +4 -5
  44. package/dist/src/html/parser.d.ts.map +1 -0
  45. package/dist/src/html/parser.js +64 -15
  46. package/dist/src/html/parser.js.map +1 -1
  47. package/dist/src/html/template-types.d.ts +1 -0
  48. package/dist/src/html/template-types.d.ts.map +1 -0
  49. package/dist/src/html/template-types.js.map +1 -1
  50. package/dist/src/html/template.d.ts +5 -1
  51. package/dist/src/html/template.d.ts.map +1 -0
  52. package/dist/src/html/template.js +102 -13
  53. package/dist/src/html/template.js.map +1 -1
  54. package/dist/src/index.d.ts +15 -12
  55. package/dist/src/index.d.ts.map +1 -0
  56. package/dist/src/index.js +18 -10
  57. package/dist/src/index.js.map +1 -1
  58. package/dist/src/json.d.ts +3 -0
  59. package/dist/src/json.d.ts.map +1 -0
  60. package/dist/src/json.js +14 -0
  61. package/dist/src/json.js.map +1 -0
  62. package/dist/src/lifecycle.d.ts +1 -0
  63. package/dist/src/lifecycle.d.ts.map +1 -0
  64. package/dist/src/lifecycle.js +3 -2
  65. package/dist/src/lifecycle.js.map +1 -1
  66. package/dist/src/page.d.ts +63 -28
  67. package/dist/src/page.d.ts.map +1 -0
  68. package/dist/src/page.js +5 -0
  69. package/dist/src/page.js.map +1 -1
  70. package/dist/src/persisted.d.ts +6 -1
  71. package/dist/src/persisted.d.ts.map +1 -0
  72. package/dist/src/persisted.js +17 -11
  73. package/dist/src/persisted.js.map +1 -1
  74. package/dist/src/resource.d.ts +2 -1
  75. package/dist/src/resource.d.ts.map +1 -0
  76. package/dist/src/resource.js +128 -50
  77. package/dist/src/resource.js.map +1 -1
  78. package/dist/src/router/base.d.ts +66 -0
  79. package/dist/src/router/base.d.ts.map +1 -0
  80. package/dist/src/router/base.js +168 -0
  81. package/dist/src/router/base.js.map +1 -0
  82. package/dist/src/router/manifest.d.ts +1 -0
  83. package/dist/src/router/manifest.d.ts.map +1 -0
  84. package/dist/src/router/manifest.js +156 -171
  85. package/dist/src/router/manifest.js.map +1 -1
  86. package/dist/src/router/match.d.ts +17 -2
  87. package/dist/src/router/match.d.ts.map +1 -0
  88. package/dist/src/router/match.js +60 -6
  89. package/dist/src/router/match.js.map +1 -1
  90. package/dist/src/router/navigation.d.ts +1 -0
  91. package/dist/src/router/navigation.d.ts.map +1 -0
  92. package/dist/src/router/navigation.js +58 -9
  93. package/dist/src/router/navigation.js.map +1 -1
  94. package/dist/src/signal.d.ts +1 -0
  95. package/dist/src/signal.d.ts.map +1 -0
  96. package/dist/src/signal.js +46 -17
  97. package/dist/src/signal.js.map +1 -1
  98. package/dist/src/static-runtime.d.ts +82 -0
  99. package/dist/src/static-runtime.d.ts.map +1 -0
  100. package/dist/src/static-runtime.js +209 -0
  101. package/dist/src/static-runtime.js.map +1 -0
  102. package/dist/src/vite/index.d.ts +32 -3
  103. package/dist/src/vite/index.d.ts.map +1 -0
  104. package/dist/src/vite/index.js +49 -17
  105. package/dist/src/vite/index.js.map +1 -1
  106. package/docs/README.md +5 -8
  107. package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
  108. package/docs/architecture/v1-roadmap.md +19 -0
  109. package/docs/en/00-the-mado-way.md +1 -1
  110. package/docs/en/01-quickstart.md +183 -0
  111. package/docs/en/10-pages-and-components.md +279 -0
  112. package/docs/en/11-templates-and-signals.md +211 -0
  113. package/docs/en/12-routing.md +229 -0
  114. package/docs/en/13-data.md +227 -0
  115. package/docs/en/14-forms.md +91 -0
  116. package/docs/en/15-static-snapshots.md +181 -0
  117. package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +56 -4
  118. package/docs/en/{13-deployment.md → 20-deployment.md} +79 -17
  119. package/docs/en/{15-error-handling.md → 21-error-handling.md} +9 -6
  120. package/docs/en/{14-testing.md → 22-testing.md} +6 -5
  121. package/docs/en/23-cookbook.md +292 -0
  122. package/docs/en/24-devtools-and-diagnostics.md +45 -0
  123. package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +17 -9
  124. package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +5 -5
  125. package/docs/en/33-migration-0.12-0.13.md +56 -0
  126. package/docs/en/40-llm-guide.md +774 -0
  127. package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +28 -25
  128. package/docs/en/{05-why-mado.md → 42-why-mado.md} +3 -3
  129. package/docs/en/README.md +99 -27
  130. package/docs/recipes/nginx/Containerfile +26 -0
  131. package/docs/recipes/nginx/nginx.conf +42 -0
  132. package/llms.txt +263 -210
  133. package/package.json +51 -23
  134. package/scripts/clean.mjs +8 -0
  135. package/scripts/cli/generate.mjs +57 -10
  136. package/scripts/cli/help.mjs +13 -5
  137. package/scripts/cli/index.mjs +31 -7
  138. package/scripts/cli/init.mjs +19 -19
  139. package/scripts/cli/release.mjs +38 -83
  140. package/scripts/cli/run.mjs +5 -3
  141. package/scripts/logger.mjs +117 -0
  142. package/scripts/output-guard.mjs +128 -0
  143. package/scripts/preview.mjs +105 -24
  144. package/scripts/static/browser.mjs +695 -0
  145. package/scripts/static/discover.mjs +284 -0
  146. package/scripts/static/output.mjs +141 -0
  147. package/scripts/static/serialize.mjs +212 -0
  148. package/scripts/static/server.mjs +167 -0
  149. package/scripts/static.mjs +205 -0
  150. package/starters/default/README.md +57 -55
  151. package/starters/default/package.json +6 -12
  152. package/starters/default/src/app.routes.ts +20 -33
  153. package/starters/default/src/components/feature-card.component.ts +40 -0
  154. package/starters/default/src/components/live-counter.component.ts +57 -0
  155. package/starters/default/src/content/guides.ts +55 -0
  156. package/starters/default/src/main.ts +9 -13
  157. package/starters/default/src/pages/app.page.ts +41 -0
  158. package/starters/default/src/pages/guide.page.ts +40 -0
  159. package/starters/default/src/pages/home.page.ts +49 -0
  160. package/starters/default/src/pages/not-found.page.ts +18 -0
  161. package/starters/default/src/styles/document.css +39 -0
  162. package/starters/default/src/styles/reset.css +37 -0
  163. package/starters/default/src/styles/tokens.css +30 -0
  164. package/starters/default/src/styles.d.ts +3 -1
  165. package/starters/default/src/vite-env.d.ts +1 -1
  166. package/starters/default/tsconfig.node.json +13 -0
  167. package/starters/default/vite.config.ts +14 -4
  168. package/starters/modular/README.md +142 -0
  169. package/starters/modular/index.html +13 -0
  170. package/starters/modular/package.json +30 -0
  171. package/starters/modular/src/app.routes.ts +39 -0
  172. package/starters/{default → modular}/src/layouts/app-shell.layout.ts +9 -7
  173. package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
  174. package/starters/modular/src/main.ts +16 -0
  175. package/starters/{default → modular}/src/modules/auth/login.page.ts +5 -12
  176. package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +3 -4
  177. package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +4 -4
  178. package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
  179. package/starters/modular/src/modules/home/not-found.page.ts +14 -0
  180. package/starters/{default → modular}/src/shared/styles/reset.css +21 -1
  181. package/starters/modular/src/styles.d.ts +1 -0
  182. package/starters/modular/src/vite-env.d.ts +1 -0
  183. package/starters/modular/tsconfig.json +33 -0
  184. package/starters/modular/tsconfig.node.json +13 -0
  185. package/starters/modular/vite.config.ts +132 -0
  186. package/TODO.md +0 -79
  187. package/dist/src/lazy.d.ts +0 -38
  188. package/dist/src/lazy.js +0 -73
  189. package/dist/src/lazy.js.map +0 -1
  190. package/docs/en/01-routing.md +0 -152
  191. package/docs/en/02-project-layout.md +0 -124
  192. package/docs/en/03-static-bake.md +0 -249
  193. package/docs/en/04-ide-setup.md +0 -162
  194. package/docs/en/07-llm-pitfalls.md +0 -724
  195. package/docs/en/08-llm-zero-history-test.md +0 -56
  196. package/docs/en/09-shadow-vs-light-dom.md +0 -174
  197. package/docs/en/11-layouts.md +0 -113
  198. package/docs/en/12-auth-and-api.md +0 -124
  199. package/docs/en/16-bake-cookbook.md +0 -100
  200. package/docs/en/17-shadow-dom-forms.md +0 -192
  201. package/docs/fr/00-the-mado-way.md +0 -85
  202. package/docs/fr/01-routing.md +0 -120
  203. package/docs/fr/02-project-layout.md +0 -84
  204. package/docs/fr/03-static-bake.md +0 -289
  205. package/docs/fr/04-ide-setup.md +0 -162
  206. package/docs/fr/05-why-mado.md +0 -193
  207. package/docs/fr/06-for-backenders.md +0 -438
  208. package/docs/fr/07-llm-pitfalls.md +0 -625
  209. package/docs/fr/08-llm-zero-history-test.md +0 -38
  210. package/docs/fr/09-shadow-vs-light-dom.md +0 -65
  211. package/docs/fr/10-app-architecture.md +0 -138
  212. package/docs/fr/11-layouts.md +0 -47
  213. package/docs/fr/12-auth-and-api.md +0 -76
  214. package/docs/fr/13-deployment.md +0 -57
  215. package/docs/fr/14-testing.md +0 -41
  216. package/docs/fr/15-error-handling.md +0 -50
  217. package/docs/fr/16-bake-cookbook.md +0 -88
  218. package/docs/fr/17-shadow-dom-forms.md +0 -196
  219. package/docs/fr/18-api-freeze-map.md +0 -63
  220. package/docs/fr/19-reactivity-ordering.md +0 -97
  221. package/docs/fr/20-v1-stability.md +0 -88
  222. package/docs/fr/README.md +0 -27
  223. package/docs/ru/00-the-mado-way.md +0 -84
  224. package/docs/ru/01-routing.md +0 -119
  225. package/docs/ru/02-project-layout.md +0 -84
  226. package/docs/ru/03-static-bake.md +0 -250
  227. package/docs/ru/04-ide-setup.md +0 -144
  228. package/docs/ru/05-why-mado.md +0 -193
  229. package/docs/ru/06-for-backenders.md +0 -428
  230. package/docs/ru/07-llm-pitfalls.md +0 -624
  231. package/docs/ru/08-llm-zero-history-test.md +0 -57
  232. package/docs/ru/09-shadow-vs-light-dom.md +0 -63
  233. package/docs/ru/10-app-architecture.md +0 -152
  234. package/docs/ru/11-layouts.md +0 -47
  235. package/docs/ru/12-auth-and-api.md +0 -75
  236. package/docs/ru/13-deployment.md +0 -66
  237. package/docs/ru/14-testing.md +0 -50
  238. package/docs/ru/15-error-handling.md +0 -56
  239. package/docs/ru/16-bake-cookbook.md +0 -95
  240. package/docs/ru/17-shadow-dom-forms.md +0 -193
  241. package/docs/ru/18-api-freeze-map.md +0 -64
  242. package/docs/ru/19-reactivity-ordering.md +0 -95
  243. package/docs/ru/20-v1-stability.md +0 -82
  244. package/docs/ru/README.md +0 -25
  245. package/docs/uk/00-the-mado-way.md +0 -82
  246. package/docs/uk/01-routing.md +0 -76
  247. package/docs/uk/02-project-layout.md +0 -73
  248. package/docs/uk/03-static-bake.md +0 -48
  249. package/docs/uk/04-ide-setup.md +0 -26
  250. package/docs/uk/05-why-mado.md +0 -34
  251. package/docs/uk/06-for-backenders.md +0 -55
  252. package/docs/uk/07-llm-pitfalls.md +0 -145
  253. package/docs/uk/08-llm-zero-history-test.md +0 -34
  254. package/docs/uk/09-shadow-vs-light-dom.md +0 -58
  255. package/docs/uk/10-app-architecture.md +0 -97
  256. package/docs/uk/11-layouts.md +0 -47
  257. package/docs/uk/12-auth-and-api.md +0 -70
  258. package/docs/uk/13-deployment.md +0 -40
  259. package/docs/uk/14-testing.md +0 -34
  260. package/docs/uk/15-error-handling.md +0 -32
  261. package/docs/uk/16-bake-cookbook.md +0 -36
  262. package/docs/uk/17-shadow-dom-forms.md +0 -193
  263. package/docs/uk/18-api-freeze-map.md +0 -61
  264. package/docs/uk/19-reactivity-ordering.md +0 -95
  265. package/docs/uk/20-v1-stability.md +0 -83
  266. package/docs/uk/README.md +0 -27
  267. package/scripts/bake.mjs +0 -570
  268. package/scripts/package-smoke.mjs +0 -77
  269. package/scripts/release-notes.mjs +0 -66
  270. package/scripts/size-budget.mjs +0 -91
  271. package/starters/default/src/modules/home/not-found.page.ts +0 -11
  272. package/starters/default/src/shared/ui/x-button.component.ts +0 -49
  273. /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
  274. /package/starters/{default → modular}/.editorconfig +0 -0
  275. /package/starters/{default → modular}/eslint.config.mjs +0 -0
  276. /package/starters/{default → modular}/public/favicon.svg +0 -0
  277. /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
  278. /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
  279. /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
  280. /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
  281. /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
  282. /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
  283. /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
  284. /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
  285. /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
  286. /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
  287. /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
  288. /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
  289. /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
  290. /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
  291. /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
  292. /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
  293. /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
  294. /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
  295. /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
  296. /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
  297. /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
  298. /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
@@ -3,6 +3,8 @@ import { existsSync } from "node:fs";
3
3
  import { readdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
 
6
+ import { logger } from "../logger.mjs";
7
+
6
8
  export async function run(cmd, args, options = {}) {
7
9
  const child = spawn(cmd, args, {
8
10
  cwd: options.cwd,
@@ -48,7 +50,7 @@ export function resolvePackageBin(ctx, pkg, binPath) {
48
50
  if (existsSync(projectPath)) return projectPath;
49
51
  const packagePath = join(ctx.packageRoot, "node_modules", pkg, binPath);
50
52
  if (existsSync(packagePath)) return packagePath;
51
- console.error(`[mado] missing ${pkg}. Install it in this project: npm i -D ${pkg}`);
53
+ logger.error("mado", "missing-package", `missing ${pkg}. Install it in this project: npm i -D ${pkg}`);
52
54
  process.exit(1);
53
55
  }
54
56
 
@@ -57,7 +59,7 @@ export function resolveBin(ctx, bin) {
57
59
  if (existsSync(projectPath)) return projectPath;
58
60
  const packagePath = join(ctx.packageRoot, "node_modules", bin);
59
61
  if (!existsSync(packagePath)) {
60
- console.error(`[mado] missing ${bin}. Run npm install first.`);
62
+ logger.error("mado", "missing-binary", `missing ${bin}. Run npm install first.`);
61
63
  process.exit(1);
62
64
  }
63
65
  return packagePath;
@@ -66,7 +68,7 @@ export function resolveBin(ctx, bin) {
66
68
  export async function writeIfMissing(path, content, logPrefix = "[mado]") {
67
69
  if (existsSync(path)) return;
68
70
  await writeFile(path, content);
69
- console.log(`${logPrefix} wrote ${path}`);
71
+ logger.info("release", "write", `wrote ${path}`, { prefix: logPrefix });
70
72
  }
71
73
 
72
74
  export async function listTestFiles(projectRoot) {
@@ -0,0 +1,117 @@
1
+ const LEVELS = { debug: 10, info: 20, warn: 30, error: 40, silent: 50 };
2
+ const COLORS = {
3
+ debug: "\u001b[2m",
4
+ info: "\u001b[36m",
5
+ warn: "\u001b[33m",
6
+ error: "\u001b[31m",
7
+ };
8
+
9
+ let level = normaliseLevel(process.env.MADO_LOG_LEVEL) ?? "info";
10
+ let format = normaliseFormat(process.env.MADO_LOG_FORMAT) ?? "auto";
11
+
12
+ export function configureLogger(argv) {
13
+ const clean = [];
14
+ for (let index = 0; index < argv.length; index++) {
15
+ const arg = argv[index];
16
+ const [name, inline] = arg.split("=", 2);
17
+ if (name === "--log-level" || name === "--log-format") {
18
+ const value = inline ?? argv[++index];
19
+ if (!value) throw new Error(`${name} requires a value`);
20
+ if (name === "--log-level") {
21
+ const next = normaliseLevel(value);
22
+ if (!next) throw new Error(`invalid log level: ${value}`);
23
+ level = next;
24
+ } else {
25
+ const next = normaliseFormat(value);
26
+ if (!next) throw new Error(`invalid log format: ${value}`);
27
+ format = next;
28
+ }
29
+ continue;
30
+ }
31
+ if (arg === "--no-color") {
32
+ format = "plain";
33
+ continue;
34
+ }
35
+ clean.push(arg);
36
+ }
37
+ return clean;
38
+ }
39
+
40
+ export function diagnostic(levelName, scope, code, message, data) {
41
+ if (LEVELS[levelName] < LEVELS[level]) return;
42
+ const record = {
43
+ timestamp: new Date().toISOString(),
44
+ level: levelName,
45
+ scope,
46
+ code,
47
+ message,
48
+ ...(data === undefined ? {} : { data }),
49
+ };
50
+ const stream = levelName === "warn" || levelName === "error"
51
+ ? process.stderr
52
+ : process.stdout;
53
+ if (resolvedFormat(stream) === "json") {
54
+ stream.write(`${JSON.stringify(record, safeJson)}\n`);
55
+ return;
56
+ }
57
+ const prefix = scope === "mado" ? "[mado]" : `[${scope}]`;
58
+ const suffix = data === undefined ? "" : ` ${safePreview(data)}`;
59
+ const line = `${prefix}${code ? ` ${code}` : ""} ${message}${suffix}`;
60
+ if (resolvedFormat(stream) === "pretty") {
61
+ stream.write(`${COLORS[levelName] ?? ""}${line}\u001b[0m\n`);
62
+ } else {
63
+ stream.write(`${line}\n`);
64
+ }
65
+ }
66
+
67
+ export const logger = {
68
+ debug(scope, code, message, data) {
69
+ diagnostic("debug", scope, code, message, data);
70
+ },
71
+ info(scope, code, message, data) {
72
+ diagnostic("info", scope, code, message, data);
73
+ },
74
+ warn(scope, code, message, data) {
75
+ diagnostic("warn", scope, code, message, data);
76
+ },
77
+ error(scope, code, message, data) {
78
+ diagnostic("error", scope, code, message, data);
79
+ },
80
+ };
81
+
82
+ function resolvedFormat(stream) {
83
+ if (format !== "auto") return format;
84
+ if (process.env.NO_COLOR !== undefined || !stream.isTTY) return "plain";
85
+ return "pretty";
86
+ }
87
+
88
+ function normaliseLevel(value) {
89
+ if (!value) return null;
90
+ const candidate = String(value).toLowerCase();
91
+ return Object.hasOwn(LEVELS, candidate) ? candidate : null;
92
+ }
93
+
94
+ function normaliseFormat(value) {
95
+ if (!value) return null;
96
+ const candidate = String(value).toLowerCase();
97
+ return ["auto", "pretty", "plain", "json"].includes(candidate)
98
+ ? candidate
99
+ : null;
100
+ }
101
+
102
+ function safeJson(_key, value) {
103
+ if (typeof value === "bigint") return `${value}n`;
104
+ if (value instanceof Error) {
105
+ return { name: value.name, message: value.message, stack: value.stack };
106
+ }
107
+ return value;
108
+ }
109
+
110
+ function safePreview(value) {
111
+ try {
112
+ const text = JSON.stringify(value, safeJson);
113
+ return text.length > 500 ? `${text.slice(0, 497)}...` : text;
114
+ } catch {
115
+ return String(value);
116
+ }
117
+ }
@@ -0,0 +1,128 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readdir, realpath, rm, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+
6
+ export const OUTPUT_SENTINEL = ".mado-output";
7
+ const BUILD_BRIDGE = join("_mado", "build.json");
8
+ const PROTECTED_PROJECT_DIRS = new Set([
9
+ ".git",
10
+ "docs",
11
+ "node_modules",
12
+ "scripts",
13
+ "src",
14
+ "starters",
15
+ "test",
16
+ ]);
17
+
18
+ export async function prepareOutputDirectory({
19
+ projectRoot,
20
+ outDir,
21
+ clean = true,
22
+ force = false,
23
+ }) {
24
+ const safe = await resolveSafeOutput(projectRoot, outDir);
25
+ const state = await ownershipState(safe.outDir);
26
+
27
+ if (state === "foreign" && !force) {
28
+ throw new Error(
29
+ `[mado:output] refusing to use non-empty directory not owned by Mado: ${safe.outDir}\n` +
30
+ `Choose an empty directory or pass --force-output after checking the path.`,
31
+ );
32
+ }
33
+
34
+ if (clean && existsSync(safe.outDir)) {
35
+ await rm(safe.outDir, { recursive: true, force: true });
36
+ }
37
+ await mkdir(safe.outDir, { recursive: true });
38
+ await writeSentinel(safe.outDir, safe.projectRoot);
39
+ return safe.outDir;
40
+ }
41
+
42
+ export async function claimOutputDirectory({ projectRoot, outDir, force = false }) {
43
+ const safe = await validateOutputDirectory({ projectRoot, outDir, force });
44
+ await mkdir(safe.outDir, { recursive: true });
45
+ await writeSentinel(safe.outDir, safe.projectRoot);
46
+ return safe.outDir;
47
+ }
48
+
49
+ export async function validateOutputDirectory({ projectRoot, outDir, force = false }) {
50
+ const safe = await resolveSafeOutput(projectRoot, outDir);
51
+ const state = await ownershipState(safe.outDir);
52
+ if (state === "foreign" && !force) {
53
+ throw new Error(
54
+ `[mado:output] refusing to write into non-empty directory not owned by Mado: ${safe.outDir}\n` +
55
+ `Run a Mado/Vite build first, choose another --out, or pass --force-output.`,
56
+ );
57
+ }
58
+ return safe;
59
+ }
60
+
61
+ export async function resolveSafeOutput(projectRoot, outDir) {
62
+ const project = await canonicalPath(resolve(projectRoot));
63
+ const output = await canonicalPath(resolve(projectRoot, outDir));
64
+ const home = await canonicalPath(homedir());
65
+
66
+ if (isFilesystemRoot(output)) {
67
+ throw new Error(`[mado:output] refusing filesystem root: ${output}`);
68
+ }
69
+ if (output === project) {
70
+ throw new Error(`[mado:output] refusing project root: ${output}`);
71
+ }
72
+ if (output === home) {
73
+ throw new Error(`[mado:output] refusing home directory: ${output}`);
74
+ }
75
+ if (isInside(output, project)) {
76
+ throw new Error(`[mado:output] refusing a parent of the project: ${output}`);
77
+ }
78
+
79
+ const projectRelative = relative(project, output);
80
+ if (projectRelative && !projectRelative.startsWith("..") && !isAbsolute(projectRelative)) {
81
+ const first = projectRelative.split(/[\\/]/, 1)[0];
82
+ if (PROTECTED_PROJECT_DIRS.has(first)) {
83
+ throw new Error(`[mado:output] refusing protected project directory: ${output}`);
84
+ }
85
+ }
86
+
87
+ return { projectRoot: project, outDir: output };
88
+ }
89
+
90
+ async function ownershipState(outDir) {
91
+ if (!existsSync(outDir)) return "empty";
92
+ const entries = await readdir(outDir);
93
+ if (entries.length === 0) return "empty";
94
+ if (existsSync(join(outDir, OUTPUT_SENTINEL))) return "owned";
95
+ // A build bridge is emitted by the Mado Vite plugin and is sufficient
96
+ // evidence for a direct `mado static` invocation after `mado build`.
97
+ if (existsSync(join(outDir, BUILD_BRIDGE))) return "owned";
98
+ return "foreign";
99
+ }
100
+
101
+ async function writeSentinel(outDir, projectRoot) {
102
+ await writeFile(
103
+ join(outDir, OUTPUT_SENTINEL),
104
+ `${JSON.stringify({ owner: "@madojs/mado", projectRoot }, null, 2)}\n`,
105
+ );
106
+ }
107
+
108
+ async function canonicalPath(path) {
109
+ let cursor = path;
110
+ const suffix = [];
111
+ while (!existsSync(cursor)) {
112
+ const parent = dirname(cursor);
113
+ if (parent === cursor) break;
114
+ suffix.unshift(relative(parent, cursor));
115
+ cursor = parent;
116
+ }
117
+ const base = await realpath(cursor).catch(() => resolve(cursor));
118
+ return resolve(base, ...suffix);
119
+ }
120
+
121
+ function isFilesystemRoot(path) {
122
+ return dirname(path) === path;
123
+ }
124
+
125
+ function isInside(parent, child) {
126
+ const rel = relative(parent, child);
127
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
128
+ }
@@ -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,10 +17,13 @@
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";
23
24
 
25
+ import { configureLogger, logger } from "./logger.mjs";
26
+
24
27
  // Tiny argv parser. Supports --flag, --flag=value, --flag value.
25
28
  function parsePreviewArgs(argv) {
26
29
  const flags = {};
@@ -46,7 +49,7 @@ function parsePreviewArgs(argv) {
46
49
  return flags;
47
50
  }
48
51
 
49
- const PREVIEW_FLAGS = parsePreviewArgs(process.argv.slice(2));
52
+ const PREVIEW_FLAGS = parsePreviewArgs(configureLogger(process.argv.slice(2)));
50
53
 
51
54
  const ROOT = resolve(process.cwd());
52
55
  const OUT = resolve(ROOT, process.env.OUT_DIR ?? "out");
@@ -55,6 +58,68 @@ const HOST = String(PREVIEW_FLAGS.host ?? process.env.HOST ?? "localhost");
55
58
  const AUTOBUILD = process.env.PREVIEW_AUTOBUILD === "1";
56
59
  const SKIP_BUILD = process.env.SKIP_BUILD === "1" || !AUTOBUILD;
57
60
 
61
+ // Active Vite base. We try the internal build bridge first (the
62
+ // `@madojs/mado/vite` plugin writes `_mado/build.json` during `vite
63
+ // build`); if `mado static` has already dropped that file before
64
+ // shipping the production artifact, we fall back to parsing the asset
65
+ // prefix out of `out/index.html`. Both paths converge on the same
66
+ // canonical "/" or "/prefix/" form so the preview server replays the
67
+ // real deployment URL shape regardless of pipeline stage.
68
+ const BASE = detectDeployedBase(OUT);
69
+
70
+ function detectDeployedBase(out) {
71
+ const fromBridge = readBridgeBase(out);
72
+ if (fromBridge != null) return fromBridge;
73
+ const fromHtml = readHtmlBase(out);
74
+ if (fromHtml != null) return fromHtml;
75
+ return "/";
76
+ }
77
+
78
+ function readBridgeBase(out) {
79
+ try {
80
+ const meta = JSON.parse(
81
+ readFileSync(join(out, "_mado/build.json"), "utf8"),
82
+ );
83
+ return meta?.base ? normalizeBase(meta.base) : null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function readHtmlBase(out) {
90
+ try {
91
+ const html = readFileSync(join(out, "index.html"), "utf8");
92
+ // Look at the first hashed asset; Vite always prefixes it with
93
+ // the deployed base.
94
+ const match = /(?:href|src)="(\/[^"]*?\/)assets\//.exec(html);
95
+ if (match && match[1]) return normalizeBase(match[1]);
96
+ // No prefix means root-deployed.
97
+ if (/(?:href|src)="\/assets\//.test(html)) return "/";
98
+ return null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ function normalizeBase(raw) {
105
+ if (!raw) return "/";
106
+ let s = String(raw).trim();
107
+ if (!s || s === "/") return "/";
108
+ if (!s.startsWith("/")) s = "/" + s;
109
+ if (!s.endsWith("/")) s = s + "/";
110
+ return s.replace(/\/+/g, "/");
111
+ }
112
+
113
+ function stripBase(pathname) {
114
+ if (BASE === "/") return pathname.startsWith("/") ? pathname : "/" + pathname;
115
+ if (pathname === BASE || pathname === BASE.slice(0, -1)) return "/";
116
+ if (pathname.startsWith(BASE)) {
117
+ const rest = pathname.slice(BASE.length - 1);
118
+ return rest || "/";
119
+ }
120
+ return pathname;
121
+ }
122
+
58
123
  const MIME = {
59
124
  ".html": "text/html; charset=utf-8",
60
125
  ".js": "text/javascript; charset=utf-8",
@@ -75,23 +140,20 @@ const MIME = {
75
140
  // ---------- Optional release ----------
76
141
 
77
142
  if (!SKIP_BUILD) {
78
- console.log("[preview] PREVIEW_AUTOBUILD=1 — running mado release");
143
+ logger.info("preview", "autobuild", "PREVIEW_AUTOBUILD=1 — running mado release");
79
144
  run("node", ["scripts/cli.mjs", "release"]);
80
145
  }
81
146
 
82
147
  if (!(await exists(OUT))) {
83
- console.error(
84
- `[preview] missing ${OUT}/ — run \`mado release\` first.`,
85
- );
148
+ logger.error("preview", "missing-output", `missing ${OUT}/ — run \`mado release\` first`);
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
- console.error(
92
- `[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.`,
94
- );
156
+ logger.error("preview", "missing-shell", `missing ${spaShell}; mado release did not produce an HTML entry`);
95
157
  process.exit(1);
96
158
  }
97
159
 
@@ -103,7 +165,24 @@ const isImmutable = (filename) =>
103
165
  const server = createServer(async (req, res) => {
104
166
  try {
105
167
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
106
- const pathname = decodeURIComponent(url.pathname);
168
+ const fullPathname = decodeURIComponent(url.pathname);
169
+ // Honour the deployed Vite base: redirect bare `/` to `/${base}/` and
170
+ // strip the prefix before any lookup, so `out/index.html`,
171
+ // `out/assets/...` and `out/docs/index.html` are reachable through
172
+ // the same URL shape the CDN serves.
173
+ if (BASE !== "/") {
174
+ const bareBase = BASE.slice(0, -1);
175
+ if (fullPathname === "/" || fullPathname === bareBase) {
176
+ res.writeHead(302, { location: BASE });
177
+ res.end();
178
+ return;
179
+ }
180
+ if (!fullPathname.startsWith(BASE) && fullPathname !== bareBase) {
181
+ res.writeHead(404).end("not found");
182
+ return;
183
+ }
184
+ }
185
+ const pathname = stripBase(fullPathname);
107
186
  const accepts = (req.headers["accept-encoding"] ?? "").toString();
108
187
 
109
188
  const target = await resolveTarget(pathname);
@@ -134,22 +213,17 @@ const server = createServer(async (req, res) => {
134
213
  res.writeHead(200, headers);
135
214
  res.end(data);
136
215
  } catch (err) {
137
- console.error("[preview] error:", err);
216
+ logger.error("preview", "request", "request failed", err);
138
217
  res.writeHead(500).end(String(err));
139
218
  }
140
219
  });
141
220
 
142
221
  server.on("error", (err) => {
143
222
  if (err.code === "EPERM" || err.code === "EACCES") {
144
- console.error(
145
- `[preview] failed to bind ${HOST}:${PORT}: ${err.message}\n` +
146
- `[preview] tip: this sandbox may disallow binding "${HOST}".\n` +
147
- `[preview] try: mado preview --host 127.0.0.1`,
148
- );
223
+ logger.error("preview", "bind", `failed to bind ${HOST}:${PORT}: ${err.message}`);
224
+ logger.info("preview", "bind-hint", "try: mado preview --host 127.0.0.1");
149
225
  } else {
150
- console.error(
151
- `[preview] failed to listen on ${HOST}:${PORT}: ${err.message}`,
152
- );
226
+ logger.error("preview", "listen", `failed to listen on ${HOST}:${PORT}: ${err.message}`);
153
227
  }
154
228
  process.exit(1);
155
229
  });
@@ -158,7 +232,12 @@ server.listen(PORT, HOST, async () => {
158
232
  const urlHost = HOST === "0.0.0.0" || HOST === "::" ? "localhost" : HOST;
159
233
  console.log("");
160
234
  console.log("Mado preview (production-like)");
161
- console.log(` url: http://${urlHost}:${PORT}/`);
235
+ // Print the URL the server actually serves the app at. When the
236
+ // deployment uses a non-trivial Vite `base`, the bare root just
237
+ // 302-redirects to the base prefix, which surprises users who copy
238
+ // the logged URL into a browser and land on "Not Found".
239
+ console.log(` url: http://${urlHost}:${PORT}${BASE}`);
240
+ console.log(` base: ${BASE}`);
162
241
  console.log(` out: ${OUT}`);
163
242
  console.log(" (Ctrl-C to stop)");
164
243
  console.log("");
@@ -169,7 +248,7 @@ server.listen(PORT, HOST, async () => {
169
248
  function run(cmd, args, { allowFail = false } = {}) {
170
249
  const r = spawnSync(cmd, args, { stdio: "inherit" });
171
250
  if (r.status !== 0 && !allowFail) {
172
- console.error(`[preview] command "${cmd} ${args.join(" ")}" exited with ${r.status}`);
251
+ logger.error("preview", "child-exit", `command "${cmd} ${args.join(" ")}" exited with ${r.status}`);
173
252
  process.exit(r.status ?? 1);
174
253
  }
175
254
  }
@@ -216,7 +295,9 @@ async function resolveTarget(pathname) {
216
295
  // extension) deliberately 404 instead — otherwise a 200 on
217
296
  // /missing.png would mask real bugs.
218
297
  if (!extname(pathname)) {
219
- const spa = join(OUT, "index.html");
298
+ const spa = existsSync(join(OUT, "_mado", "spa.html"))
299
+ ? join(OUT, "_mado", "spa.html")
300
+ : join(OUT, "index.html");
220
301
  if (await exists(spa)) return spa;
221
302
  }
222
303