@madojs/mado 0.9.0 → 0.10.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 (61) hide show
  1. package/AGENTS.md +58 -7
  2. package/CHANGELOG.md +91 -1
  3. package/README.md +20 -4
  4. package/dist/src/component.d.ts +2 -12
  5. package/dist/src/component.js +2 -29
  6. package/dist/src/component.js.map +1 -1
  7. package/dist/src/diagnostics.d.ts +0 -4
  8. package/dist/src/diagnostics.js +1 -0
  9. package/dist/src/diagnostics.js.map +1 -1
  10. package/dist/src/html/bindings.js +3 -0
  11. package/dist/src/html/bindings.js.map +1 -1
  12. package/dist/src/resource.d.ts +3 -6
  13. package/dist/src/resource.js +59 -10
  14. package/dist/src/resource.js.map +1 -1
  15. package/dist/src/router/manifest.d.ts +0 -3
  16. package/dist/src/router/manifest.js +1 -0
  17. package/dist/src/router/manifest.js.map +1 -1
  18. package/dist/src/router.d.ts +1 -1
  19. package/dist/src/router.js +1 -1
  20. package/dist/src/router.js.map +1 -1
  21. package/dist/src/signal.d.ts +0 -4
  22. package/dist/src/signal.js +1 -0
  23. package/dist/src/signal.js.map +1 -1
  24. package/docs/en/03-static-bake.md +1 -2
  25. package/docs/en/06-for-backenders.md +5 -0
  26. package/docs/en/08-llm-zero-history-test.md +5 -0
  27. package/docs/en/18-api-freeze-map.md +63 -0
  28. package/docs/en/19-reactivity-ordering.md +93 -0
  29. package/docs/en/20-v1-stability.md +83 -0
  30. package/docs/en/README.md +3 -0
  31. package/docs/fr/03-static-bake.md +1 -2
  32. package/docs/fr/06-for-backenders.md +6 -0
  33. package/docs/fr/08-llm-zero-history-test.md +5 -0
  34. package/docs/fr/18-api-freeze-map.md +63 -0
  35. package/docs/fr/19-reactivity-ordering.md +97 -0
  36. package/docs/fr/20-v1-stability.md +88 -0
  37. package/docs/fr/README.md +3 -0
  38. package/docs/ru/03-static-bake.md +2 -3
  39. package/docs/ru/06-for-backenders.md +6 -0
  40. package/docs/ru/08-llm-zero-history-test.md +5 -0
  41. package/docs/ru/18-api-freeze-map.md +62 -0
  42. package/docs/ru/19-reactivity-ordering.md +95 -0
  43. package/docs/ru/20-v1-stability.md +82 -0
  44. package/docs/ru/README.md +3 -0
  45. package/docs/uk/06-for-backenders.md +5 -0
  46. package/docs/uk/08-llm-zero-history-test.md +5 -0
  47. package/docs/uk/18-api-freeze-map.md +61 -0
  48. package/docs/uk/19-reactivity-ordering.md +95 -0
  49. package/docs/uk/20-v1-stability.md +83 -0
  50. package/docs/uk/README.md +3 -0
  51. package/llms.txt +59 -5
  52. package/package.json +8 -3
  53. package/scripts/bake.mjs +0 -1
  54. package/scripts/cli.mjs +17 -0
  55. package/scripts/llm-zero-history-smoke.mjs +93 -0
  56. package/scripts/new.mjs +1 -1
  57. package/scripts/package-smoke.mjs +74 -0
  58. package/scripts/size-budget.mjs +88 -0
  59. package/starters/admin/package.json +2 -2
  60. package/starters/crud/package.json +2 -2
  61. package/starters/minimal/package.json +2 -2
@@ -0,0 +1,61 @@
1
+ # Карта замороження API
2
+
3
+ > Що є публічним, що внутрішнім, і що SemVer захищатиме у v1.
4
+
5
+ Контракт Mado v1 навмисно невеликий. Код застосунку імпортує API з кореня
6
+ пакета:
7
+
8
+ ```ts
9
+ import { component, html, resource, routes, signal } from "@madojs/mado";
10
+ ```
11
+
12
+ Єдиний публічний subpath — side-effect модуль devtools:
13
+
14
+ ```ts
15
+ import "@madojs/mado/devtools.js";
16
+ ```
17
+
18
+ Усе інше під `dist/src/` — деталь реалізації, навіть якщо файл видно в
19
+ репозиторії.
20
+
21
+ ## Стабільний публічний API
22
+
23
+ Ці імена публічні й захищаються SemVer після v1:
24
+
25
+ - Reactivity: `signal`, `computed`, `effect`, `untracked`, `batch`,
26
+ `flushSync`.
27
+ - Templates і directives: `html`, `render`, `each`, `list`, `unsafeHTML`,
28
+ `ref`, `classMap`, `styleMap`.
29
+ - Components і CSS: `component`, `css`, `cssVars`.
30
+ - Routing і pages: `routes`, `router`, `page`, `layout`, `nested`,
31
+ `navigate`, `queryParam`, `prefetchPath`.
32
+ - Data: `resource`, `mutation`, `invalidate`, `jsonFetcher`, `HttpError`.
33
+ - Forms: `useForm`.
34
+ - Head і persistence: `applyHead`, `persisted`.
35
+ - Context: `createContext`, `provide`, `inject`.
36
+ - Advanced lifecycle helpers: `createLifecycle`, `runInLifecycle`,
37
+ `getCurrentLifecycle`.
38
+ - Публічні TypeScript-типи, експортовані з `@madojs/mado`.
39
+
40
+ ## Внутрішнє або нестабільне
41
+
42
+ Це не публічний API:
43
+
44
+ - Package subpaths крім `@madojs/mado` і `@madojs/mado/devtools.js`.
45
+ - Internals parser/binding: `html/parser.js`, `html/bindings.js`,
46
+ `ChildState`, `EachEntry`.
47
+ - Internals router: `router/match.js`, `router/navigation.js`,
48
+ `router/manifest.js`.
49
+ - Diagnostics internals і всі `_testHooks`.
50
+ - Точний текст bundle, назви chunks і внутрішня структура файлів.
51
+
52
+ Тести репозиторію можуть імпортувати internal files через відносні шляхи
53
+ `dist/`. Код застосунків не повинен цього робити.
54
+
55
+ ## Що може змінюватися
56
+
57
+ Patch і minor releases можуть додавати root exports, options, diagnostics, docs
58
+ або starter files. Вони також можуть змінювати internals, форму bundle і деталі
59
+ реалізації, якщо stable API та задокументована поведінка лишаються сумісними.
60
+
61
+ Breaking changes стабільного API потребують major version.
@@ -0,0 +1,95 @@
1
+ # Порядок reactivity
2
+
3
+ > Малий набір ordering-гарантій, які Mado вважає публічною поведінкою.
4
+
5
+ Reactivity у Mado синхронна для читання і планована для side effects. Мета —
6
+ передбачувані UI updates без великої scheduling-моделі.
7
+
8
+ ## Signals
9
+
10
+ `signal(value)` повертає getter-функцію. `set(next)` змінює значення одразу,
11
+ якщо `Object.is(previous, next)` не дорівнює `true`.
12
+
13
+ ```ts
14
+ const count = signal(0);
15
+ count.set(1);
16
+ count(); // 1, одразу
17
+ ```
18
+
19
+ Computed values позначаються до запуску effects, тому effect, який читає
20
+ computed, бачить актуальні dependencies, а не застарілий cache.
21
+
22
+ ## Effects
23
+
24
+ `effect(fn)` запускається один раз одразу. Подальші зміни dependencies планують
25
+ один запуск effect у microtask. Тести можуть викликати `flushSync()`, щоб
26
+ синхронно очистити чергу.
27
+
28
+ Якщо effect повертає cleanup-функцію, Mado запускає її перед наступним запуском
29
+ effect і ще раз при виклику disposer.
30
+
31
+ ```ts
32
+ const stop = effect(() => {
33
+ const id = setInterval(tick, 1000);
34
+ return () => clearInterval(id);
35
+ });
36
+
37
+ stop();
38
+ ```
39
+
40
+ У компонентах і pages для unmount cleanup надавайте перевагу
41
+ `ctx.onDispose()` / page `onDispose()`. Cleanup effect — це cleanup між runs.
42
+
43
+ ## Batch
44
+
45
+ `batch(fn)` групує записи signals в один subscriber pass. Effects не
46
+ запускаються до виходу з найзовнішнього batch, включно з вкладеними batches.
47
+
48
+ ```ts
49
+ batch(() => {
50
+ first.set("Ada");
51
+ batch(() => last.set("Lovelace"));
52
+ });
53
+ // effects бачать тільки фінальну пару
54
+ ```
55
+
56
+ Спостережувані `computed({ equals })` також зберігають batch atomicity: вони
57
+ перераховуються один раз після зовнішнього batch на повністю застосованому
58
+ state. Вони не повинні бачити напівзастосований batch на кшталт
59
+ `(new x, old y)`.
60
+
61
+ ## DOM updates
62
+
63
+ `render(result, container)` перевикористовує наявний template instance, коли
64
+ наступний render має ті самі template strings. Для child bindings, що
65
+ повертають вкладений `html```, діє те саме правило: ті самі strings оновлюються
66
+ на місці, інші strings перебудовують гілку.
67
+
68
+ Це означає, що непов'язані зміни signals не пересоздають `<input>` у
69
+ стабільному вкладеному template, тому focus, DOM state і listeners
70
+ зберігаються.
71
+
72
+ Списки повинні використовувати `each(items, key, renderItem)`. Keys задають DOM
73
+ identity. Duplicate keys попереджають у development і отримують positional
74
+ suffix, щоб кожен item все одно рендерився, але duplicate keys — це data bug.
75
+
76
+ ## Teardown компонентів
77
+
78
+ Custom elements можуть отримати `disconnectedCallback()`, а потім
79
+ `connectedCallback()` під час same-tick move. Mado відкладає teardown компонента
80
+ до microtask і скасовує його при reconnect, тому keyed reorders зберігають
81
+ state компонента. Справжнє видалення все одно запускає lifecycle cleanup на
82
+ наступній microtask.
83
+
84
+ ## Не гарантується
85
+
86
+ Mado не гарантує точну кількість internal scheduler microtasks, порядок
87
+ незалежних effects без спільних dependencies, форму generated bundle або
88
+ внутрішній module layout. Це implementation details.
89
+
90
+ Invariant tests для цього контракту:
91
+
92
+ - `test/reactivity-ordering.test.mjs`
93
+ - `test/signal-batch-equals.test.mjs`
94
+ - `test/update-nested-reuse.test.mjs`
95
+ - `test/each-component-state.test.mjs`
@@ -0,0 +1,83 @@
1
+ # Стабільність v1
2
+
3
+ > Що Mado обіцяє після v1, і що лишається вільним для розвитку.
4
+
5
+ Mado v1 означає, що публічний app-facing contract достатньо стабільний для
6
+ реальних business apps. Це не означає, що кожен internal file, generated byte,
7
+ starter copy або diagnostic string заморожені назавжди.
8
+
9
+ Читайте разом із:
10
+
11
+ - [Карта замороження API](./18-api-freeze-map.md)
12
+ - [Порядок reactivity](./19-reactivity-ordering.md)
13
+
14
+ ## Стабільно під SemVer
15
+
16
+ Після v1 Mado вважає SemVer-protected:
17
+
18
+ - Public exports з `@madojs/mado`.
19
+ - Public TypeScript types з `@madojs/mado`.
20
+ - Side-effect subpath `@madojs/mado/devtools.js`.
21
+ - Template binding syntax: child `${}`, `@event`, `.prop`, `?boolean`,
22
+ attribute bindings, directives і `each()`.
23
+ - Signal semantics, описані в reactivity ordering guide.
24
+ - Component lifecycle semantics: setup один раз за connection lifetime,
25
+ deferred teardown для same-tick moves, cleanup через `ctx.onDispose`.
26
+ - Router/page/resource/form contracts, описані в English docs.
27
+ - Імена CLI commands і широкий сенс команд (`build`, `dev`, `release`, `bake`,
28
+ `bundle`, `preview`, `init`, `new`).
29
+
30
+ Ламати це можна тільки в major version.
31
+
32
+ ## Дозволено в minor releases
33
+
34
+ Minor releases можуть додавати:
35
+
36
+ - New root exports.
37
+ - New options на наявних API.
38
+ - New diagnostics і warnings.
39
+ - New starters, examples, docs і CLI flags.
40
+ - Performance improvements і internal implementation rewrites.
41
+
42
+ Minor release не має вимагати змін у вже коректних apps.
43
+
44
+ ## Дозволено в patch releases
45
+
46
+ Patch releases можуть виправляти bugs, посилювати diagnostics, покращувати docs
47
+ і робити сумісні implementation changes. Patch може змінити timing тільки якщо
48
+ старий timing був незадокументованим bug і зміна зберігає reactivity ordering
49
+ contract.
50
+
51
+ ## Нестабільно
52
+
53
+ Це навмисно не захищено SemVer:
54
+
55
+ - Internal package subpaths крім `@madojs/mado/devtools.js`.
56
+ - Файли під `src/`, `dist/src/` і implementation module boundaries.
57
+ - `_testHooks`, diagnostics internals і warning codes.
58
+ - Точний generated JavaScript text, chunk names, sourcemap content і bundle
59
+ byte layout.
60
+ - Internal parser, binding, router і resource cache data structures.
61
+ - Starter app visual copy і demo data.
62
+
63
+ Apps не повинні імпортувати internal files або перевіряти точний bundle output.
64
+
65
+ ## Bundle і release output
66
+
67
+ Mado триматиме size budget і deterministic release tests, але v1 stability не
68
+ заморожує byte-for-byte bundler output. Hashes, chunk boundaries і generated
69
+ asset names можуть змінюватися, якщо задокументований deployment contract
70
+ продовжує працювати.
71
+
72
+ ## Якщо release вас зламав
73
+
74
+ Якщо update ламає код, який використовує тільки public exports і
75
+ задокументовану поведінку, вважайте це bug. Відкрийте issue з:
76
+
77
+ - версією Mado до і після;
78
+ - задіяним public API;
79
+ - мінімальною репродукцією;
80
+ - чи це runtime behaviour, TypeScript types, CLI output або docs.
81
+
82
+ Якщо поломка залежить від internal subpath або точного generated output, про це
83
+ все одно варто повідомити, але це не вважається SemVer break.
package/docs/uk/README.md CHANGED
@@ -22,3 +22,6 @@
22
22
  | Обробка помилок | [15-error-handling.md](./15-error-handling.md) |
23
23
  | Рецепти bake | [16-bake-cookbook.md](./16-bake-cookbook.md) |
24
24
  | Shadow DOM + форми | [17-shadow-dom-forms.md](./17-shadow-dom-forms.md) |
25
+ | Карта замороження API | [18-api-freeze-map.md](./18-api-freeze-map.md) |
26
+ | Порядок reactivity | [19-reactivity-ordering.md](./19-reactivity-ordering.md) |
27
+ | Стабільність v1 | [20-v1-stability.md](./20-v1-stability.md) |
package/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # Mado
2
2
 
3
3
  > A calm browser-native SPA framework for internal tools, admin panels and business apps.
4
- > Routing, forms, state, data fetching and prerendering. TypeScript-only build (`tsc`), zero runtime dependencies.
4
+ > Routing, forms, state, data fetching and prerendering. Zero runtime dependencies; generated apps use `typescript`, `esbuild` and `linkedom` as dev tooling.
5
5
 
6
6
  Mado is a focused frontend framework for admin panels, internal tools and CRUD-heavy SPA. It deliberately avoids React patterns (no JSX, no hooks, no VDOM, no Vite). Target audience: backend developers and small teams who want a complete app stack without frontend infrastructure overhead.
7
7
 
@@ -15,7 +15,10 @@ Mado is a focused frontend framework for admin panels, internal tools and CRUD-h
15
15
  - **Cleanup via `ctx.onDispose(fn)`** in setup, not via return from effect.
16
16
  - **Page cleanup via `onDispose`** in view: `view: ({ onDispose }) => { ... onDispose(() => cleanup()); }`. Only needed for raw APIs (setInterval, WebSocket). `resource()`/`effect()` auto-cleanup.
17
17
  - **`untracked()` in page view async** — functions called synchronously in `view()` that read signals must wrap reads in `untracked()` to avoid effect cycles with the router.
18
- - **Reactive attributes via `ctx.attr(name, default?)`** — returns a Signal<string> that auto-updates when the attribute changes. Uses `observedAttributes` when available, falls back to a per-instance `MutationObserver` for attrs registered during setup. No boilerplate needed.
18
+ - **Reactive attributes via `ctx.attr(name, default?)`** — returns a Signal<string> that auto-updates when the attribute changes via a per-instance `MutationObserver`. No `observedAttributes` option, no boilerplate.
19
+ - **Public imports only.** App code imports from `@madojs/mado` and optionally side-effect `@madojs/mado/devtools.js`. Other package subpaths and `dist/src/*` are internal.
20
+ - **Layouts are stateless wrappers.** Use route-manifest `layout()` and render `${child}` inside shared chrome. Put per-page state in pages/components/resources, not in layout view locals keyed by route identity.
21
+ - **Bake is a static meta-shell/prerender pass.** It is not SSR with hydration and not a Next-style SSG runtime.
19
22
 
20
23
  ## Critical template rules
21
24
 
@@ -47,6 +50,24 @@ Mado is a focused frontend framework for admin panels, internal tools and CRUD-h
47
50
  html`<ul>${() => each(users(), u => u.id, u => html`<li>${u.name}</li>`)}</ul>`
48
51
  ```
49
52
 
53
+ 4. **Parser hard errors:**
54
+ - no dynamic `${...}` child slots inside `<script>`, `<style>`, `<textarea>`, or `<title>`;
55
+ - no nested SVG-only `html` templates inside an outer `<svg>`.
56
+
57
+ ```ts
58
+ // ❌ throws: RAW_TEXT elements cannot host child slots
59
+ html`<textarea>${draft}</textarea>`;
60
+
61
+ // ✅ bind the DOM property instead
62
+ html`<textarea .value=${draft}></textarea>`;
63
+
64
+ // ❌ throws: nested SVG template loses namespace context
65
+ html`<svg>${html`<circle r="5"></circle>`}</svg>`;
66
+
67
+ // ✅ keep SVG internals in one template
68
+ html`<svg viewBox="0 0 10 10"><circle r="5"></circle></svg>`;
69
+ ```
70
+
50
71
  ## Canonical imports
51
72
 
52
73
  ```ts
@@ -114,6 +135,35 @@ const stats = resource(() => "/api/admin/stats", apiFetcher<Stats>());
114
135
  Unlike `jsonFetcher()`, `apiFetcher()` attaches the Bearer token from memory.
115
136
  Use `jsonFetcher()` for public endpoints, `apiFetcher()` for anything behind auth.
116
137
 
138
+ ## Resource and mutation semantics
139
+
140
+ - A `resource()` key is the cache identity. Same key means shared cache and
141
+ deduped in-flight request; use distinct keys for distinct data or auth scope.
142
+ - `mutation().run()` is concurrent by default. `loading()` stays true while any
143
+ run is in flight. Use `{ abortPrevious: true }` only for search-as-you-type or
144
+ "latest request wins" flows.
145
+ - `invalidates` runs after a successful mutation and is best-effort: errors are
146
+ logged, but the mutation result stays successful.
147
+
148
+ ## Layouts and bake
149
+
150
+ Use `layout()` in `routes.ts` for shared shells. A layout view should be a pure
151
+ wrapper around `${child}` and shared chrome:
152
+
153
+ ```ts
154
+ export default page({
155
+ view: ({ child }) => html`<x-app-shell>${child}</x-app-shell>`,
156
+ });
157
+ ```
158
+
159
+ Do not create route-specific state in layout view locals. Put it in pages,
160
+ components, resources, or app-level contexts.
161
+
162
+ `mado bake` renders selected routes to static HTML for SEO and first paint. It
163
+ does not hydrate server-rendered code. Baked views must be deterministic from
164
+ `params`, `bake.data`, and plain values. Avoid browser-only effects, timers,
165
+ relative `fetch`, and runtime directives like keyed `each()` during bake.
166
+
117
167
  ## Canonical "Hello world"
118
168
 
119
169
  ```ts
@@ -201,6 +251,9 @@ export default page({
201
251
  - docs/en/15-error-handling.md — route/data/action error boundaries
202
252
  - docs/en/16-bake-cookbook.md — static bake recipes and failure modes
203
253
  - docs/en/17-shadow-dom-forms.md — Shadow DOM + useForm() patterns (proxy properties, form submit bridge)
254
+ - docs/en/18-api-freeze-map.md — stable public API vs internal implementation details
255
+ - docs/en/19-reactivity-ordering.md — signal ordering, batching and teardown guarantees
256
+ - docs/en/20-v1-stability.md — v1 SemVer contract and what remains internal
204
257
  - examples/basic/ — minimal API tour
205
258
  - examples/tickets/ — LLM zero-history CRUD validation
206
259
  - examples/showcase/ — flagship CRM pressure app (auth, nested routes, forms, mutations)
@@ -210,7 +263,7 @@ export default page({
210
263
 
211
264
  - ❌ JSX → tagged templates instead
212
265
  - ❌ Virtual DOM → fine-grained signal updates
213
- - ❌ SSR with hydration → `bake` (static build) or edge-prerender for SEO
266
+ - ❌ SSR with hydration → `bake` static meta-shell or edge-prerender for SEO
214
267
  - ❌ Hooks and rules of hooks → signals
215
268
  - ❌ Mandatory Webpack/Vite → only `tsc`
216
269
  - ❌ React-Router / TanStack → built-in 500-line `routes()`
@@ -221,8 +274,9 @@ export default page({
221
274
 
222
275
  ## Version
223
276
 
224
- `0.8.0` — pre-1.0 product-surface release. API may still change before 1.0.
225
- Semver is not guaranteed on minor versions before 1.0.
277
+ `0.10.0` — pre-1.0 API-lock release. Phase B closed the public surface,
278
+ package exports, docs and CI gates. SemVer is not guaranteed on minor versions
279
+ before 1.0.
226
280
 
227
281
  ## License
228
282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madojs/mado",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Mado — a calm browser-native SPA framework for internal tools, admin panels and business apps. Routing, forms, state and data fetching without frontend infrastructure overhead.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,8 +36,10 @@
36
36
  "types": "./dist/src/index.d.ts",
37
37
  "import": "./dist/src/index.js"
38
38
  },
39
- "./devtools.js": "./dist/src/devtools.js",
40
- "./*": "./dist/src/*"
39
+ "./devtools.js": {
40
+ "types": "./dist/src/devtools.d.ts",
41
+ "import": "./dist/src/devtools.js"
42
+ }
41
43
  },
42
44
  "files": [
43
45
  "dist/src",
@@ -66,6 +68,9 @@
66
68
  "test:browser": "node scripts/cli.mjs test browser",
67
69
  "new": "node scripts/cli.mjs new",
68
70
  "examples": "node scripts/cli.mjs examples",
71
+ "size": "node scripts/size-budget.mjs",
72
+ "package:smoke": "node scripts/package-smoke.mjs",
73
+ "llm:smoke": "node scripts/llm-zero-history-smoke.mjs",
69
74
  "test": "node scripts/cli.mjs test",
70
75
  "typecheck": "node scripts/cli.mjs typecheck",
71
76
  "clean": "rm -rf dist out",
package/scripts/bake.mjs CHANGED
@@ -522,7 +522,6 @@ function buildHtml({ template, bodyHtml, head, bakedData, revalidate, canonical
522
522
 
523
523
  if (revalidate) {
524
524
  setMeta(document, { name: "bake-revalidate", content: String(revalidate) });
525
- setMeta(document, { name: "bake-stamp", content: String(Date.now()) });
526
525
  }
527
526
 
528
527
  if (bakedData !== undefined) {
package/scripts/cli.mjs CHANGED
@@ -144,6 +144,7 @@ async function runInit(rawArgs) {
144
144
  await cp(source, target, { recursive: true, force: true });
145
145
  await copyCanonicalAgentFiles(target);
146
146
  await ensureStarterGitignore(target);
147
+ await ensureStarterPackageJson(target);
147
148
 
148
149
  const packageName = packageNameFromDir(target);
149
150
  if (!isValidPackageName(packageName)) {
@@ -331,6 +332,22 @@ async function ensureStarterGitignore(target) {
331
332
  await writeFile(file, "node_modules\ndist\nout\n.DS_Store\n*.log\n");
332
333
  }
333
334
 
335
+ async function ensureStarterPackageJson(target) {
336
+ const file = join(target, "package.json");
337
+ if (!existsSync(file)) return;
338
+
339
+ const pkg = JSON.parse(await readFile(file, "utf8"));
340
+ const rootDev = PACKAGE_JSON.devDependencies ?? {};
341
+ pkg.devDependencies = {
342
+ ...(pkg.devDependencies ?? {}),
343
+ typescript: rootDev.typescript ?? "^6.0.3",
344
+ esbuild: rootDev.esbuild ?? "^0.28.0",
345
+ linkedom: rootDev.linkedom ?? "^0.18.12",
346
+ };
347
+
348
+ await writeFile(file, `${JSON.stringify(pkg, null, 2)}\n`);
349
+ }
350
+
334
351
  async function runNodeBin(bin, args) {
335
352
  await run(process.execPath, [resolveBin(bin), ...args]);
336
353
  }
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from "node:child_process";
4
+ import { readdir, readFile, stat } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { promisify } from "node:util";
7
+
8
+ const exec = promisify(execFile);
9
+ const root = process.cwd();
10
+ const ticketsDir = join(root, "examples", "tickets");
11
+
12
+ const llms = await read("llms.txt");
13
+ assertIncludes(llms, "This is NOT React", "llms.txt must keep the React warning");
14
+ assertIncludes(llms, "Canonical CRUD pattern", "llms.txt must keep the CRUD recipe");
15
+ assertIncludes(llms, "resource()", "llms.txt must document resource()");
16
+ assertIncludes(llms, "mutation", "llms.txt must document mutation()");
17
+ assertIncludes(llms, "useForm", "llms.txt must document useForm()");
18
+
19
+ const files = await collectTs(ticketsDir);
20
+ const code = (await Promise.all(files.map((file) => read(file)))).join("\n");
21
+ const routes = await read(join(ticketsDir, "routes.ts"));
22
+
23
+ for (const route of ['"/"', '"/tickets"', '"/tickets/new"', '"/tickets/:id"', '"*"']) {
24
+ assertIncludes(routes, route, `tickets routes must include ${route}`);
25
+ }
26
+
27
+ for (const api of [
28
+ "component(",
29
+ "html`",
30
+ "signal(",
31
+ "computed(",
32
+ "resource(",
33
+ "mutation(",
34
+ "invalidates",
35
+ "queryParam(",
36
+ "each(",
37
+ "useForm(",
38
+ ]) {
39
+ assertIncludes(code, api, `tickets example must exercise ${api}`);
40
+ }
41
+
42
+ const forbidden = [
43
+ /\buseState\s*\(/,
44
+ /\buseEffect\s*\(/,
45
+ /\$state\b/,
46
+ /\bref\s*\(/,
47
+ /from\s+["']react["']/,
48
+ /class\s+\w+\s+extends\s+HTMLElement/,
49
+ /<>\s*$/,
50
+ /(^|[^?.\w-])disabled=\$\{/,
51
+ /(^|[^?.\w-])checked=\$\{/,
52
+ ];
53
+
54
+ for (const pattern of forbidden) {
55
+ if (pattern.test(code)) {
56
+ throw new Error(`[llm-smoke] forbidden generated pattern: ${pattern}`);
57
+ }
58
+ }
59
+
60
+ await run(process.execPath, ["scripts/cli.mjs", "build"]);
61
+ await run(process.execPath, ["--test", "test/tickets-smoke.test.mjs"]);
62
+
63
+ console.log("[llm-smoke] ok examples/tickets follows llms.txt and passes smoke");
64
+
65
+ async function collectTs(dir) {
66
+ const out = [];
67
+ for (const entry of await readdir(dir)) {
68
+ const file = join(dir, entry);
69
+ const s = await stat(file);
70
+ if (s.isDirectory()) out.push(...await collectTs(file));
71
+ else if (file.endsWith(".ts")) out.push(file);
72
+ }
73
+ return out.sort();
74
+ }
75
+
76
+ async function read(file) {
77
+ return readFile(file, "utf8");
78
+ }
79
+
80
+ function assertIncludes(text, needle, message) {
81
+ if (!text.includes(needle)) throw new Error(`[llm-smoke] ${message}`);
82
+ }
83
+
84
+ async function run(cmd, args) {
85
+ console.log(`[llm-smoke] ${cmd} ${args.join(" ")}`);
86
+ try {
87
+ await exec(cmd, args, { cwd: root, maxBuffer: 20 * 1024 * 1024 });
88
+ } catch (err) {
89
+ if (err.stdout) process.stdout.write(err.stdout);
90
+ if (err.stderr) process.stderr.write(err.stderr);
91
+ throw err;
92
+ }
93
+ }
package/scripts/new.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  // Result: examples/pages/<name>.ts (or src/pages/, when present)
8
8
  // with __name__ / __Name__ placeholders replaced.
9
9
  //
10
- // Zero dependencies.
10
+ // Zero runtime dependencies; generated apps use dev tooling only.
11
11
 
12
12
  import { readFile, writeFile, mkdir, access } from "node:fs/promises";
13
13
  import { dirname, join, resolve } from "node:path";
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from "node:child_process";
4
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { basename, join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { promisify } from "node:util";
9
+
10
+ const exec = promisify(execFile);
11
+ const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
12
+ const tempRoot = await mkdtemp(join(tmpdir(), "mado-package-smoke-"));
13
+ let tarball = "";
14
+
15
+ try {
16
+ const packed = await exec("npm", ["pack", "--silent"], { cwd: repoRoot });
17
+ tarball = resolve(repoRoot, packed.stdout.trim().split(/\s+/).at(-1) ?? "");
18
+ if (!tarball) throw new Error("[package-smoke] npm pack did not return a tarball");
19
+
20
+ const installRoot = join(tempRoot, "installed");
21
+ await mkdir(installRoot, { recursive: true });
22
+ await run("npm", ["install", tarball], { cwd: installRoot });
23
+
24
+ await run(
25
+ process.execPath,
26
+ [
27
+ "--input-type=module",
28
+ "--eval",
29
+ `
30
+ import { html, signal } from "@madojs/mado";
31
+ import "@madojs/mado/devtools.js";
32
+ if (typeof html !== "function" || typeof signal !== "function") {
33
+ throw new Error("public root import failed");
34
+ }
35
+ try {
36
+ await import("@madojs/mado/lifecycle.js");
37
+ throw new Error("internal lifecycle subpath unexpectedly resolved");
38
+ } catch (err) {
39
+ if (err?.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") throw err;
40
+ }
41
+ `,
42
+ ],
43
+ { cwd: installRoot },
44
+ );
45
+
46
+ await run("npx", ["mado", "init", "smoke-app", "--starter", "minimal"], {
47
+ cwd: installRoot,
48
+ env: { ...process.env, MADO_PACKAGE_SPEC: tarball },
49
+ });
50
+
51
+ const appRoot = join(installRoot, "smoke-app");
52
+ await run("npm", ["install"], { cwd: appRoot });
53
+ await run("npm", ["run", "release"], { cwd: appRoot });
54
+
55
+ console.log(`[package-smoke] ok ${basename(tarball)}`);
56
+ } finally {
57
+ await rm(tempRoot, { recursive: true, force: true });
58
+ if (tarball) await rm(tarball, { force: true });
59
+ }
60
+
61
+ async function run(cmd, args, options) {
62
+ console.log(`[package-smoke] ${cmd} ${args.join(" ")}`);
63
+ try {
64
+ await exec(cmd, args, {
65
+ cwd: options.cwd,
66
+ env: options.env ?? process.env,
67
+ maxBuffer: 20 * 1024 * 1024,
68
+ });
69
+ } catch (err) {
70
+ if (err.stdout) process.stdout.write(err.stdout);
71
+ if (err.stderr) process.stderr.write(err.stderr);
72
+ throw err;
73
+ }
74
+ }