@madojs/mado 0.12.0 → 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 (154) hide show
  1. package/AGENTS.md +15 -8
  2. package/CHANGELOG.md +58 -0
  3. package/README.md +25 -3
  4. package/dist/src/component.d.ts +3 -1
  5. package/dist/src/component.d.ts.map +1 -0
  6. package/dist/src/component.js +65 -30
  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.map +1 -1
  15. package/dist/src/devtools-hook.d.ts +14 -0
  16. package/dist/src/devtools-hook.d.ts.map +1 -0
  17. package/dist/src/devtools-hook.js +27 -0
  18. package/dist/src/devtools-hook.js.map +1 -0
  19. package/dist/src/devtools.d.ts +24 -22
  20. package/dist/src/devtools.d.ts.map +1 -0
  21. package/dist/src/devtools.js +230 -58
  22. package/dist/src/devtools.js.map +1 -1
  23. package/dist/src/diagnostics.d.ts +12 -0
  24. package/dist/src/diagnostics.d.ts.map +1 -0
  25. package/dist/src/diagnostics.js +68 -8
  26. package/dist/src/diagnostics.js.map +1 -1
  27. package/dist/src/each.d.ts +1 -5
  28. package/dist/src/each.d.ts.map +1 -0
  29. package/dist/src/each.js +0 -8
  30. package/dist/src/each.js.map +1 -1
  31. package/dist/src/forms.d.ts +27 -96
  32. package/dist/src/forms.d.ts.map +1 -0
  33. package/dist/src/forms.js +184 -394
  34. package/dist/src/forms.js.map +1 -1
  35. package/dist/src/head.d.ts +1 -0
  36. package/dist/src/head.d.ts.map +1 -0
  37. package/dist/src/head.js +2 -1
  38. package/dist/src/head.js.map +1 -1
  39. package/dist/src/html/bindings.d.ts +1 -0
  40. package/dist/src/html/bindings.d.ts.map +1 -0
  41. package/dist/src/html/bindings.js.map +1 -1
  42. package/dist/src/html/parser.d.ts +3 -4
  43. package/dist/src/html/parser.d.ts.map +1 -0
  44. package/dist/src/html/parser.js +63 -14
  45. package/dist/src/html/parser.js.map +1 -1
  46. package/dist/src/html/template-types.d.ts +1 -0
  47. package/dist/src/html/template-types.d.ts.map +1 -0
  48. package/dist/src/html/template-types.js.map +1 -1
  49. package/dist/src/html/template.d.ts +5 -1
  50. package/dist/src/html/template.d.ts.map +1 -0
  51. package/dist/src/html/template.js +78 -3
  52. package/dist/src/html/template.js.map +1 -1
  53. package/dist/src/index.d.ts +7 -8
  54. package/dist/src/index.d.ts.map +1 -0
  55. package/dist/src/index.js +5 -7
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/json.d.ts +3 -0
  58. package/dist/src/json.d.ts.map +1 -0
  59. package/dist/src/json.js +14 -0
  60. package/dist/src/json.js.map +1 -0
  61. package/dist/src/lifecycle.d.ts +1 -0
  62. package/dist/src/lifecycle.d.ts.map +1 -0
  63. package/dist/src/lifecycle.js +3 -2
  64. package/dist/src/lifecycle.js.map +1 -1
  65. package/dist/src/page.d.ts +1 -0
  66. package/dist/src/page.d.ts.map +1 -0
  67. package/dist/src/page.js.map +1 -1
  68. package/dist/src/persisted.d.ts +6 -1
  69. package/dist/src/persisted.d.ts.map +1 -0
  70. package/dist/src/persisted.js +17 -11
  71. package/dist/src/persisted.js.map +1 -1
  72. package/dist/src/resource.d.ts +2 -1
  73. package/dist/src/resource.d.ts.map +1 -0
  74. package/dist/src/resource.js +126 -49
  75. package/dist/src/resource.js.map +1 -1
  76. package/dist/src/router/base.d.ts +1 -0
  77. package/dist/src/router/base.d.ts.map +1 -0
  78. package/dist/src/router/base.js.map +1 -1
  79. package/dist/src/router/manifest.d.ts +1 -0
  80. package/dist/src/router/manifest.d.ts.map +1 -0
  81. package/dist/src/router/manifest.js +89 -148
  82. package/dist/src/router/manifest.js.map +1 -1
  83. package/dist/src/router/match.d.ts +1 -0
  84. package/dist/src/router/match.d.ts.map +1 -0
  85. package/dist/src/router/match.js +19 -5
  86. package/dist/src/router/match.js.map +1 -1
  87. package/dist/src/router/navigation.d.ts +1 -0
  88. package/dist/src/router/navigation.d.ts.map +1 -0
  89. package/dist/src/router/navigation.js.map +1 -1
  90. package/dist/src/signal.d.ts +1 -0
  91. package/dist/src/signal.d.ts.map +1 -0
  92. package/dist/src/signal.js +46 -17
  93. package/dist/src/signal.js.map +1 -1
  94. package/dist/src/static-runtime.d.ts +1 -0
  95. package/dist/src/static-runtime.d.ts.map +1 -0
  96. package/dist/src/static-runtime.js.map +1 -1
  97. package/dist/src/vite/index.d.ts +1 -0
  98. package/dist/src/vite/index.d.ts.map +1 -0
  99. package/dist/src/vite/index.js +4 -3
  100. package/dist/src/vite/index.js.map +1 -1
  101. package/docs/architecture/v1-roadmap.md +19 -0
  102. package/docs/en/10-pages-and-components.md +9 -1
  103. package/docs/en/13-data.md +8 -6
  104. package/docs/en/14-forms.md +66 -219
  105. package/docs/en/15-static-snapshots.md +4 -4
  106. package/docs/en/16-app-architecture.md +3 -2
  107. package/docs/en/20-deployment.md +21 -8
  108. package/docs/en/21-error-handling.md +9 -6
  109. package/docs/en/24-devtools-and-diagnostics.md +45 -0
  110. package/docs/en/30-api-freeze-map.md +8 -9
  111. package/docs/en/32-v1-stability.md +2 -2
  112. package/docs/en/33-migration-0.12-0.13.md +56 -0
  113. package/docs/en/40-llm-guide.md +7 -9
  114. package/docs/en/41-for-backenders.md +13 -9
  115. package/docs/en/42-why-mado.md +2 -2
  116. package/docs/en/README.md +2 -0
  117. package/docs/recipes/nginx/nginx.conf +2 -2
  118. package/llms.txt +25 -7
  119. package/package.json +37 -16
  120. package/scripts/clean.mjs +8 -0
  121. package/scripts/cli/generate.mjs +11 -14
  122. package/scripts/cli/help.mjs +2 -0
  123. package/scripts/cli/index.mjs +14 -6
  124. package/scripts/cli/init.mjs +13 -18
  125. package/scripts/cli/release.mjs +22 -19
  126. package/scripts/cli/run.mjs +5 -3
  127. package/scripts/logger.mjs +117 -0
  128. package/scripts/output-guard.mjs +128 -0
  129. package/scripts/preview.mjs +11 -19
  130. package/scripts/static/browser.mjs +49 -8
  131. package/scripts/static/discover.mjs +4 -1
  132. package/scripts/static.mjs +21 -7
  133. package/starters/default/package.json +4 -4
  134. package/starters/default/tsconfig.node.json +13 -0
  135. package/starters/default/vite.config.ts +0 -3
  136. package/starters/modular/README.md +4 -4
  137. package/starters/modular/package.json +4 -4
  138. package/starters/modular/src/layouts/app-shell.layout.ts +1 -2
  139. package/starters/modular/src/modules/auth/login.page.ts +5 -12
  140. package/starters/modular/src/modules/billing/pages/invoice-detail.page.ts +3 -4
  141. package/starters/modular/src/modules/billing/pages/invoices-list.page.ts +1 -1
  142. package/starters/modular/src/shared/styles/reset.css +21 -1
  143. package/starters/modular/tsconfig.json +10 -1
  144. package/starters/modular/tsconfig.node.json +13 -0
  145. package/starters/modular/vite.config.ts +6 -5
  146. package/dist/src/lazy.d.ts +0 -38
  147. package/dist/src/lazy.js +0 -73
  148. package/dist/src/lazy.js.map +0 -1
  149. package/scripts/bake.mjs +0 -5
  150. package/scripts/docs-lint.mjs +0 -170
  151. package/scripts/package-smoke.mjs +0 -135
  152. package/scripts/release-notes.mjs +0 -66
  153. package/scripts/size-budget.mjs +0 -95
  154. package/starters/modular/src/shared/ui/x-button.component.ts +0 -49
@@ -279,16 +279,15 @@ This means:
279
279
  // ❌ No such API
280
280
  const f = useForm({ resolver: zodResolver(schema) });
281
281
 
282
- // ✅ HTML-style schema validation
282
+ // ✅ Native constraints in markup, reactive state in Mado
283
283
  const f = useForm({
284
- email: { required: true, type: "email" },
285
- age: { required: true, type: "number", min: 18 },
284
+ initial: { email: "", age: "" as number | "" },
286
285
  });
286
+ html`<input name="email" type="email" required @input=${f.onInput} />`;
287
287
 
288
288
  // ✅ Or a custom function when HTML5 isn't enough
289
- const f = useForm(
290
- { name: { required: true } },
291
- {
289
+ const f = useForm({
290
+ initial: { name: "" },
292
291
  validate: (values) => {
293
292
  const errors: Record<string, string> = {};
294
293
  if (values.name && /\d/.test(values.name as string)) {
@@ -296,8 +295,7 @@ const f = useForm(
296
295
  }
297
296
  return Object.keys(errors).length ? errors : null;
298
297
  },
299
- },
300
- );
298
+ });
301
299
  ```
302
300
 
303
301
  ---
@@ -705,7 +703,7 @@ automatically on navigation. Only raw browser APIs need explicit
705
703
  | `useRouter().query.q` | `queryParam('q')` |
706
704
  | `<input value={v} onChange={...}>` | `<input .value=${v} @input=${...}>` |
707
705
  | `{items.map(x => ...)}` | `${() => each(items, x => x.id, x => ...)}` |
708
- | `useForm({ resolver: zodResolver })` | `useForm({...}, { validate: (v) => ... })` |
706
+ | `useForm({ resolver: zodResolver })` | `useForm({ initial, validate })` + native constraints |
709
707
  | `class extends HTMLElement` | `component('x-name', setup)` |
710
708
  | `@customElement('x')` | `component('x-name', setup)` |
711
709
  | `host.getAttribute('x')` in render | `ctx.attr('x', default)` (reactive) |
@@ -195,14 +195,13 @@ We register the `<x-counter>` tag in the browser — it becomes a "function" tha
195
195
 
196
196
  ## Forms — like `form.Validate()` on the backend
197
197
 
198
- Mado uses **schema-based validation close to native HTML constraints**, plus adds state tracking.
198
+ Mado uses the browser's **constraint validation** and adds reactive state tracking.
199
199
 
200
200
  ```ts
201
201
  import { useForm } from "@madojs/mado";
202
202
 
203
203
  const f = useForm({
204
- email: { required: true, type: "email" },
205
- age: { required: true, type: "number", min: 18 },
204
+ initial: { email: "", age: "" as number | "" },
206
205
  });
207
206
 
208
207
  // in the template:
@@ -215,6 +214,8 @@ html`
215
214
  >
216
215
  <input
217
216
  name="email"
217
+ type="email"
218
+ required
218
219
  .value=${() => f.values().email ?? ""}
219
220
  @input=${f.onInput}
220
221
  @blur=${f.onBlur}
@@ -225,12 +226,13 @@ html`
225
226
  ? html`<small>${f.errors().email}</small>`
226
227
  : null}
227
228
 
228
- <button ?disabled=${() => !f.isValid() || f.submitting()}>Save</button>
229
+ <input name="age" type="number" min="18" @input=${f.onInput} />
230
+ <button type="submit" ?disabled=${() => !f.isValid() || f.submitting()}>Save</button>
229
231
  </form>
230
232
  `;
231
233
  ```
232
234
 
233
- Custom validation — `validate: (values) => errors | null`. No Yup schemas or dependencies.
235
+ Custom validation — `validate: (values, { signal }) => errors | null`. No Yup schemas or dependencies.
234
236
 
235
237
  ---
236
238
 
@@ -258,6 +260,8 @@ component("x-page", ({ host }) => {
258
260
  ```
259
261
 
260
262
  This is like `context.WithValue` / `ctx.Value` in Go, but reactive.
263
+ The transport is the interoperable Web Components `context-request` event, so
264
+ providers and consumers can cross library boundaries.
261
265
 
262
266
  ---
263
267
 
@@ -335,7 +339,7 @@ const createUser = mutation<NewUser, User>(
335
339
  );
336
340
 
337
341
  // in page.view:
338
- const f = useForm({ name: { required: true } });
342
+ const f = useForm({ initial: { name: "" } });
339
343
 
340
344
  html`
341
345
  <form
@@ -344,8 +348,8 @@ html`
344
348
  navigate("/users");
345
349
  })}
346
350
  >
347
- <input name="name" @input=${f.onInput} />
348
- <button>Create</button>
351
+ <input name="name" required @input=${f.onInput} />
352
+ <button type="submit">Create</button>
349
353
  </form>
350
354
  `;
351
355
  ```
@@ -426,7 +430,7 @@ Everything else — standard browser + TypeScript.
426
430
 
427
431
  ## What's missing (honestly)
428
432
 
429
- - No browser extension dev-tools. Use `localStorage.madoDebug = '1'` + console.
433
+ - The built-in overlay covers runtime inspection; a browser extension remains post-v1 work.
430
434
  - No StackBlitz starters (yet).
431
435
  - No AI assistant that knows Mado as well as React. When in doubt — read `src/`, it's not scary.
432
436
 
@@ -138,7 +138,7 @@ I won't dwell on this for long, because React is in a **different weight class**
138
138
  **Mado wins:**
139
139
 
140
140
  - bundle size dozens of times smaller;
141
- - zero infrastructure (no Vite, no Babel, no 200 packages);
141
+ - one conventional Vite transport without a framework compiler or Babel layer;
142
142
  - readable in an evening — if something breaks, open `src/`;
143
143
  - signals instead of hooks (no "can't use in an if" rules, no stale-closure traps);
144
144
  - no need to migrate between major versions.
@@ -194,7 +194,7 @@ This is sufficient for most applications. If you're building Excel in the browse
194
194
  Mado is a **narrow** tool with honest positioning. It is strongest where:
195
195
 
196
196
  1. You want to **own** the code and read it in its entirety.
197
- 2. **Infrastructure simplicity** is critical (no Vite/Webpack/Babel).
197
+ 2. **Infrastructure simplicity** is critical (one Vite transport, no custom compiler).
198
198
  3. You need **batteries in one box** (router + data + forms + SEO).
199
199
  4. You are not a junior and are not afraid of Web Components.
200
200
 
package/docs/en/README.md CHANGED
@@ -47,6 +47,7 @@ section you need from the matrix below.
47
47
  | Error handling | [21-error-handling.md](./21-error-handling.md) |
48
48
  | Testing | [22-testing.md](./22-testing.md) |
49
49
  | Cookbook | [23-cookbook.md](./23-cookbook.md) |
50
+ | Devtools and diagnostics | [24-devtools-and-diagnostics.md](./24-devtools-and-diagnostics.md) |
50
51
 
51
52
  ### Reference
52
53
 
@@ -55,6 +56,7 @@ section you need from the matrix below.
55
56
  | API freeze map | [30-api-freeze-map.md](./30-api-freeze-map.md) |
56
57
  | Reactivity ordering | [31-reactivity-ordering.md](./31-reactivity-ordering.md) |
57
58
  | v1 stability contract | [32-v1-stability.md](./32-v1-stability.md) |
59
+ | Migration 0.12 → 0.13 | [33-migration-0.12-0.13.md](./33-migration-0.12-0.13.md) |
58
60
 
59
61
  ### Meta
60
62
 
@@ -4,7 +4,7 @@
4
4
  # - Vite hashed assets from /assets/ with immutable caching;
5
5
  # - precompressed .gz files written by `mado release`;
6
6
  # - baked route HTML when present;
7
- # - index.html as SPA fallback for client-only routes.
7
+ # - _mado/spa.html as the noindex SPA fallback for client-only routes.
8
8
 
9
9
  server {
10
10
  listen 80;
@@ -37,6 +37,6 @@ server {
37
37
 
38
38
  location / {
39
39
  add_header Cache-Control "no-cache, must-revalidate";
40
- try_files $uri $uri/ /index.html;
40
+ try_files $uri $uri/ /_mado/spa.html;
41
41
  }
42
42
  }
package/llms.txt CHANGED
@@ -84,7 +84,7 @@ import {
84
84
  // reactivity
85
85
  signal, computed, effect, untracked, batch, flushSync,
86
86
  // rendering + templates
87
- html, render, each, list, unsafeHTML, ref, classMap, styleMap,
87
+ html, render, unmount, each, unsafeHTML, ref, classMap, styleMap,
88
88
  // components
89
89
  component, css, cssVars,
90
90
  // routing
@@ -94,14 +94,14 @@ import {
94
94
  page, layout, applyHead,
95
95
  // data
96
96
  resource, mutation, invalidate, jsonFetcher, HttpError,
97
- // forms / context / persistence / lazy
97
+ // forms / context / persistence
98
98
  useForm,
99
99
  createContext, provide, inject,
100
- persisted, lazy,
100
+ persisted,
101
101
  } from "@madojs/mado";
102
102
 
103
- // Side-effect-only:
104
- import "@madojs/mado/devtools.js";
103
+ // Development overlay/controller:
104
+ import { devtools } from "@madojs/mado/devtools.js";
105
105
 
106
106
  // Vite plugin:
107
107
  import { mado } from "@madojs/mado/vite";
@@ -110,6 +110,10 @@ import { mado } from "@madojs/mado/vite";
110
110
  Everything else under the package is internal and subject to change
111
111
  without notice (see `docs/en/30-api-freeze-map.md`).
112
112
 
113
+ `page.load` is synchronous: return a plain value or `Resource`, never a
114
+ Promise. Async work belongs in `resource()` so navigation can commit or roll
115
+ back lifecycle ownership transactionally.
116
+
113
117
  ## Pages and the static snapshot pipeline
114
118
 
115
119
  A page declares its participation in static capture through the
@@ -219,11 +223,25 @@ const create = mutation((u) => api.create(u), {
219
223
  });
220
224
  ```
221
225
 
222
- The key is cache identity: same key shared cache and de-duped
223
- in-flight request. `mutation().run()` is concurrent by default;
226
+ Cache identity is the key plus fetcher. `staleTime: 0` only de-dupes an
227
+ in-flight request; positive values expire automatically. `mutation().run()` is concurrent by default;
224
228
  `{ abortPrevious: true }` keeps only the latest in flight.
225
229
  `invalidates` runs after a successful mutation and is best-effort.
226
230
 
231
+ ## Forms
232
+
233
+ ```ts
234
+ const form = useForm({ initial: { email: "" } });
235
+ html`<form @submit=${form.onSubmit(save)}>
236
+ <input name="email" type="email" required @input=${form.onInput} />
237
+ <button type="submit">Save</button>
238
+ </form>`;
239
+ ```
240
+
241
+ Native constraints and `ValidityState` are authoritative. Custom async
242
+ validation is `validate(values, { signal, form })`; reset aborts it. Use native
243
+ controls for checkbox/radio groups, multiple select, files and keyboard submit.
244
+
227
245
  ## Static snapshot constraints
228
246
 
229
247
  - `paths()` and `initialData()` run during discovery AND ship in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madojs/mado",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "A calm native-first web framework for sites and apps. Web Components, signals, routing, data, forms and browser-rendered static snapshots. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,7 +29,6 @@
29
29
  "mado": "./scripts/cli.mjs"
30
30
  },
31
31
  "main": "./dist/src/index.js",
32
- "module": "./dist/src/index.js",
33
32
  "types": "./dist/src/index.d.ts",
34
33
  "exports": {
35
34
  ".": {
@@ -47,8 +46,16 @@
47
46
  },
48
47
  "files": [
49
48
  "dist/src",
50
- "scripts",
51
- "templates",
49
+ "scripts/cli.mjs",
50
+ "scripts/_config.mjs",
51
+ "scripts/clean.mjs",
52
+ "scripts/logger.mjs",
53
+ "scripts/output-guard.mjs",
54
+ "scripts/preview.mjs",
55
+ "scripts/static.mjs",
56
+ "scripts/vite.default.mjs",
57
+ "scripts/cli",
58
+ "scripts/static",
52
59
  "starters",
53
60
  "README.md",
54
61
  "LICENSE",
@@ -60,6 +67,14 @@
60
67
  "docs/architecture/**/*.md",
61
68
  "docs/recipes/**/*"
62
69
  ],
70
+ "sideEffects": [
71
+ "./dist/src/devtools.js"
72
+ ],
73
+ "publishConfig": {
74
+ "access": "public",
75
+ "provenance": true
76
+ },
77
+ "packageManager": "npm@11.16.0",
63
78
  "scripts": {
64
79
  "build": "node scripts/cli.mjs build",
65
80
  "watch": "node scripts/cli.mjs watch",
@@ -69,23 +84,26 @@
69
84
  "new": "node scripts/cli.mjs new",
70
85
  "size": "node scripts/size-budget.mjs",
71
86
  "package:smoke": "node scripts/package-smoke.mjs",
72
- "docs:lint": "node scripts/docs-lint.mjs",
87
+ "docs:lint": "node scripts/docs-lint.mjs && node node_modules/typescript/bin/tsc --noEmit -p tsconfig.docs.json",
88
+ "api:check": "node scripts/api-check.mjs",
89
+ "benchmark:static": "node scripts/benchmark-static.mjs",
73
90
  "test": "node scripts/cli.mjs test",
74
91
  "typecheck": "node scripts/cli.mjs typecheck",
75
- "clean": "rm -rf dist out",
76
- "prepublishOnly": "npm run clean && npm run build && npm test"
92
+ "clean": "node scripts/clean.mjs",
93
+ "verify:release": "npm run clean && npm test && npm run docs:lint && npm run api:check && npm run size && npm run package:smoke && npm audit --audit-level=high",
94
+ "prepack": "node scripts/clean.mjs --dist-only && npm run build",
95
+ "prepublishOnly": "npm run verify:release"
77
96
  },
78
97
  "devDependencies": {
79
- "@types/node": "^20.19.0",
80
- "esbuild": "^0.28.0",
81
- "lightningcss": "^1.32.0",
82
- "linkedom": "^0.18.12",
83
- "playwright-core": "^1.47.0",
84
- "typescript": "^6.0.3",
85
- "vite": "^8.0.16"
98
+ "@types/node": "^22.12.0",
99
+ "esbuild": "^0.28.1",
100
+ "linkedom": "^0.18.13",
101
+ "playwright-core": "^1.61.1",
102
+ "typescript": "^7.0.2",
103
+ "vite": "^8.1.4"
86
104
  },
87
105
  "peerDependencies": {
88
- "playwright-core": "^1.47.0",
106
+ "playwright-core": "^1.61.1",
89
107
  "vite": "^8.0.0"
90
108
  },
91
109
  "peerDependenciesMeta": {
@@ -97,6 +115,9 @@
97
115
  }
98
116
  },
99
117
  "engines": {
100
- "node": ">=20"
118
+ "node": ">=22.12"
119
+ },
120
+ "allowScripts": {
121
+ "esbuild@0.28.1": true
101
122
  }
102
123
  }
@@ -0,0 +1,8 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+
4
+ const root = resolve(import.meta.dirname, "..");
5
+ const targets = process.argv.includes("--dist-only") ? ["dist"] : ["dist", "out"];
6
+ for (const target of targets) {
7
+ await rm(resolve(root, target), { recursive: true, force: true });
8
+ }
@@ -2,6 +2,8 @@ import { existsSync } from "node:fs";
2
2
  import { access, mkdir, writeFile } from "node:fs/promises";
3
3
  import { dirname, join, relative } from "node:path";
4
4
 
5
+ import { logger } from "../logger.mjs";
6
+
5
7
  const KINDS = [
6
8
  "module",
7
9
  "page",
@@ -52,15 +54,8 @@ export async function runNew(ctx, args) {
52
54
 
53
55
  const shape = detectStarterShape(ctx);
54
56
  if (shape === "universal" && MODULAR_ONLY.has(kind)) {
55
- console.error(
56
- `[mado] '${kind}' is a modular-starter generator and is not ` +
57
- "available in the universal starter.",
58
- );
59
- console.error(
60
- "[mado] Either move this project to the modular layout " +
61
- "(create src/modules/<name>/) or scaffold a fresh modular " +
62
- "project with: mado init my-app --starter modular",
63
- );
57
+ logger.error("mado", "generator-shape", `'${kind}' is not available in the universal starter`);
58
+ logger.info("mado", "generator-hint", "Create src/modules/<name>/ or run: mado init my-app --starter modular");
64
59
  process.exit(1);
65
60
  }
66
61
 
@@ -78,8 +73,8 @@ export async function runNew(ctx, args) {
78
73
 
79
74
  const fn = generators[kind];
80
75
  if (!fn) {
81
- console.error(`[mado] unknown generator: ${kind}`);
82
- console.error(`[mado] available generators: ${KINDS.join(", ")}`);
76
+ logger.error("mado", "unknown-generator", `unknown generator: ${kind}`);
77
+ logger.info("mado", "available-generators", `available generators: ${KINDS.join(", ")}`);
83
78
  process.exit(1);
84
79
  }
85
80
 
@@ -266,7 +261,9 @@ async function scaffoldForm(ctx, target) {
266
261
  // Call inside a page view: const form = ${useName}();
267
262
  export const ${useName} = () =>
268
263
  useForm({
269
- // name: { required: true },
264
+ initial: {
265
+ // name: "",
266
+ },
270
267
  });
271
268
  `,
272
269
  );
@@ -350,7 +347,7 @@ function moduleFile(ctx, target, suffix) {
350
347
 
351
348
  async function writeOnce(path, content) {
352
349
  if (await exists(path)) {
353
- console.error(`[mado] file already exists: ${path}`);
350
+ logger.error("mado", "file-exists", `file already exists: ${path}`);
354
351
  process.exit(2);
355
352
  }
356
353
  await mkdir(dirname(path), { recursive: true });
@@ -383,7 +380,7 @@ function leafName(target) {
383
380
 
384
381
  function assertSingleSegment(label, name) {
385
382
  if (name.includes("/")) {
386
- console.error(`[mado] ${label.toLowerCase()} name must be a single path segment`);
383
+ logger.error("mado", "invalid-name", `${label.toLowerCase()} name must be a single path segment`);
387
384
  process.exit(1);
388
385
  }
389
386
  }
@@ -28,6 +28,8 @@ ${buildLine}
28
28
  Configuration:
29
29
  Put app/dev/build settings in vite.config.ts.
30
30
  Mado CLI flags are explicit per command.
31
+ --log-level debug|info|warn|error|silent
32
+ --log-format pretty|plain|json --no-color
31
33
 
32
34
  Docs: README.md and docs/en/README.md.`);
33
35
  }
@@ -1,8 +1,9 @@
1
- import { readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
5
  import { detectContext } from "../_config.mjs";
6
+ import { configureLogger, logger } from "../logger.mjs";
6
7
  import { runInit } from "./init.mjs";
7
8
  import { runNew } from "./generate.mjs";
8
9
  import { printHelp } from "./help.mjs";
@@ -12,6 +13,7 @@ import { hasFlag, listTestFiles, run, runNodeBin, runNodeScript, runVite } from
12
13
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
13
14
 
14
15
  export async function main(argv) {
16
+ argv = configureLogger(argv);
15
17
  const projectRoot = resolve(process.cwd());
16
18
  const context = detectContext(projectRoot);
17
19
  const packageJson = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
@@ -37,7 +39,8 @@ export async function main(argv) {
37
39
  if (ctx.isRepo) {
38
40
  await runNodeBin(ctx, "typescript/bin/tsc", args);
39
41
  } else {
40
- await runVite(ctx, ["build", ...args], { defaultConfig: true });
42
+ const outputArgs = hasFlag(args, "--outDir") ? args : ["--outDir", "out", ...args];
43
+ await runVite(ctx, ["build", ...outputArgs], { defaultConfig: true });
41
44
  }
42
45
  break;
43
46
  case "watch":
@@ -47,9 +50,14 @@ export async function main(argv) {
47
50
  await runVite(ctx, ["build", "--watch", ...args], { defaultConfig: true });
48
51
  }
49
52
  break;
50
- case "typecheck":
53
+ case "typecheck": {
51
54
  await runNodeBin(ctx, "typescript/bin/tsc", ["--noEmit", ...args]);
55
+ const nodeConfig = join(ctx.projectRoot, "tsconfig.node.json");
56
+ if (existsSync(nodeConfig) && !hasFlag(args, "--project", "-p")) {
57
+ await runNodeBin(ctx, "typescript/bin/tsc", ["--noEmit", "-p", nodeConfig]);
58
+ }
52
59
  break;
60
+ }
53
61
  case "test": {
54
62
  await runNodeBin(ctx, "typescript/bin/tsc", []);
55
63
  const files = await listTestFiles(projectRoot);
@@ -66,8 +74,8 @@ export async function main(argv) {
66
74
  );
67
75
  break;
68
76
  case "bake":
69
- console.error("[mado] `mado bake` was removed.");
70
- console.error("Use `mado static`, or run the complete pipeline with `mado release`.");
77
+ logger.error("mado", "command-removed", "`mado bake` was removed.");
78
+ logger.info("mado", "replacement", "Use `mado static`, or run the complete pipeline with `mado release`.");
71
79
  process.exit(1);
72
80
  break;
73
81
  case "static":
@@ -88,7 +96,7 @@ export async function main(argv) {
88
96
  printHelp(ctx);
89
97
  break;
90
98
  default:
91
- console.error(`[mado] unknown command: ${command}`);
99
+ logger.error("mado", "unknown-command", `unknown command: ${command}`);
92
100
  printHelp(ctx);
93
101
  process.exit(1);
94
102
  }
@@ -3,6 +3,7 @@ import { copyFile, cp, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { join, resolve } from "node:path";
4
4
 
5
5
  import { parseFlags } from "../_config.mjs";
6
+ import { logger } from "../logger.mjs";
6
7
 
7
8
  // `default` is the universal starter: minimal, runnable, zero backend.
8
9
  // `modular` is the long-lived business-app reference architecture:
@@ -15,30 +16,30 @@ export async function runInit(ctx, rawArgs) {
15
16
  const { flags, positional } = parseFlags(rawArgs);
16
17
  const targetArg = positional[0];
17
18
  if (!targetArg) {
18
- console.error("[mado] usage: mado init <name> [--starter default] [--force]");
19
+ logger.error("mado", "usage", "usage: mado init <name> [--starter default] [--force]");
19
20
  process.exit(1);
20
21
  }
21
22
 
22
23
  const starter = String(flags.starter ?? "default");
23
24
  if (!STARTERS.includes(starter)) {
24
- console.error(`[mado] unknown starter: ${starter}`);
25
- console.error(`[mado] available starters: ${STARTERS.join(", ")}`);
25
+ logger.error("mado", "unknown-starter", `unknown starter: ${starter}`);
26
+ logger.info("mado", "available-starters", `available starters: ${STARTERS.join(", ")}`);
26
27
  process.exit(1);
27
28
  }
28
29
 
29
30
  const target = resolve(ctx.projectRoot, targetArg);
30
31
  const source = join(ctx.packageRoot, "starters", starter);
31
32
  if (!existsSync(source)) {
32
- console.error(`[mado] missing starter template: ${starter}`);
33
+ logger.error("mado", "missing-starter", `missing starter template: ${starter}`);
33
34
  process.exit(1);
34
35
  }
35
36
  if (existsSync(target) && statSync(target).isFile()) {
36
- console.error(`[mado] target exists and is a file: ${target}`);
37
+ logger.error("mado", "target-is-file", `target exists and is a file: ${target}`);
37
38
  process.exit(1);
38
39
  }
39
40
  if (existsSync(target) && readdirSync(target).length > 0 && !flags.force) {
40
- console.error(`[mado] target directory is not empty: ${target}`);
41
- console.error("[mado] use --force to write into it");
41
+ logger.error("mado", "target-not-empty", `target directory is not empty: ${target}`);
42
+ logger.info("mado", "force-hint", "use --force to write into it");
42
43
  process.exit(1);
43
44
  }
44
45
 
@@ -50,7 +51,7 @@ export async function runInit(ctx, rawArgs) {
50
51
 
51
52
  const packageName = packageNameFromDir(target);
52
53
  if (!isValidPackageName(packageName)) {
53
- console.error(`[mado] invalid package name derived from target: ${packageName}`);
54
+ logger.error("mado", "invalid-package-name", `invalid package name derived from target: ${packageName}`);
54
55
  process.exit(1);
55
56
  }
56
57
 
@@ -95,7 +96,10 @@ async function copyCanonicalLLMFiles(ctx, target) {
95
96
  async function ensureStarterGitignore(target) {
96
97
  const file = join(target, ".gitignore");
97
98
  if (existsSync(file)) return;
98
- await writeFile(file, "node_modules\nout\n.DS_Store\n*.log\n");
99
+ await writeFile(
100
+ file,
101
+ "node_modules\ndist\nout\n.cache\n.env\n.env.*\n!.env.example\n.DS_Store\n*.log\n",
102
+ );
99
103
  }
100
104
 
101
105
  async function ensureStarterPackageJson(ctx, target) {
@@ -110,15 +114,6 @@ async function ensureStarterPackageJson(ctx, target) {
110
114
  process.env.MADOJS_PACKAGE_SPEC ||
111
115
  `^${ctx.packageJson.version}`,
112
116
  };
113
- const rootDev = ctx.packageJson.devDependencies ?? {};
114
- pkg.devDependencies = {
115
- ...(pkg.devDependencies ?? {}),
116
- typescript: rootDev.typescript ?? "^6.0.3",
117
- lightningcss: rootDev.lightningcss ?? "^1.32.0",
118
- "playwright-core": rootDev["playwright-core"] ?? "^1.47.0",
119
- vite: rootDev.vite ?? "^8.0.16",
120
- };
121
-
122
117
  await writeFile(file, `${JSON.stringify(pkg, null, 2)}\n`);
123
118
  }
124
119
 
@@ -1,9 +1,11 @@
1
1
  import { existsSync } from "node:fs";
2
- import { readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { readdir, readFile, writeFile } from "node:fs/promises";
3
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";
7
+ import { logger } from "../logger.mjs";
8
+ import { prepareOutputDirectory } from "../output-guard.mjs";
7
9
  import { runNodeBin, runNodeScript, runVite, writeIfMissing } from "./run.mjs";
8
10
 
9
11
  export async function runRelease(ctx, rawArgs) {
@@ -13,33 +15,35 @@ export async function runRelease(ctx, rawArgs) {
13
15
  typeof releaseFlags.out === "string" ? releaseFlags.out : "out",
14
16
  );
15
17
 
16
- console.log(`[release] context: ${ctx.context}`);
17
- console.log(`[release] artifact: ${outDir}`);
18
- console.log("");
18
+ logger.info("release", "context", `context: ${ctx.context}`);
19
+ logger.info("release", "artifact", `artifact: ${outDir}`);
19
20
 
20
- if (!releaseFlags["no-clean"]) {
21
- if (existsSync(outDir)) {
22
- await rm(outDir, { recursive: true, force: true });
23
- console.log(`[release] cleaned ${outDir}`);
24
- }
21
+ await prepareOutputDirectory({
22
+ projectRoot: ctx.projectRoot,
23
+ outDir,
24
+ clean: !releaseFlags["no-clean"],
25
+ force: releaseFlags["force-output"] === true,
26
+ });
27
+ if (releaseFlags["no-clean"]) {
28
+ logger.info("release", "clean-skip", "--no-clean: keeping existing out/");
25
29
  } else {
26
- console.log("[release] --no-clean: keeping existing out/");
30
+ logger.info("release", "clean", `prepared ${outDir}`);
27
31
  }
28
32
 
29
- console.log("[release] step 1/5 typecheck");
33
+ logger.info("release", "step", "step 1/5 typecheck");
30
34
  await runNodeBin(ctx, "typescript/bin/tsc", ["--noEmit"]);
31
35
 
32
- console.log("[release] step 2/5 vite build");
36
+ logger.info("release", "step", "step 2/5 vite build");
33
37
  await runVite(ctx, ["build", "--outDir", outDir], { defaultConfig: true });
34
38
 
35
- console.log("[release] step 3/5 static snapshots");
39
+ logger.info("release", "step", "step 3/5 static snapshots");
36
40
  await runNodeScript(ctx, "scripts/static.mjs", [
37
41
  ...rawArgs.filter((a) => a !== "--no-clean"),
38
42
  "--out",
39
43
  outDir,
40
44
  ]);
41
45
 
42
- console.log("[release] step 4/5 deployment files");
46
+ logger.info("release", "step", "step 4/5 deployment files");
43
47
  // GitHub Pages / Netlify / Cloudflare Pages fallback. SPA fallback shell
44
48
  // is written by `mado static`; here we only register the deployment
45
49
  // bindings, and respect user-supplied files (writeIfMissing).
@@ -61,12 +65,11 @@ export async function runRelease(ctx, rawArgs) {
61
65
  "[release] ",
62
66
  );
63
67
 
64
- console.log("[release] step 5/5 precompress assets");
68
+ logger.info("release", "step", "step 5/5 precompress assets");
65
69
  await precompressOut(outDir);
66
70
 
67
- console.log("");
68
- console.log(`[release] done. Deploy artifact: ${outDir}`);
69
- console.log("[release] try: mado preview");
71
+ logger.info("release", "done", `done. Deploy artifact: ${outDir}`);
72
+ logger.info("release", "next", "try: mado preview");
70
73
  }
71
74
 
72
75
  async function precompressOut(outDir) {
@@ -84,7 +87,7 @@ async function precompressOut(outDir) {
84
87
  );
85
88
  count++;
86
89
  }
87
- console.log(`[release] compressed ${count} file(s)`);
90
+ logger.info("release", "compress", `compressed ${count} file(s)`);
88
91
  }
89
92
 
90
93
  async function listCompressibleFiles(dir) {