@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.
- package/AGENTS.md +58 -7
- package/CHANGELOG.md +91 -1
- package/README.md +20 -4
- package/dist/src/component.d.ts +2 -12
- package/dist/src/component.js +2 -29
- package/dist/src/component.js.map +1 -1
- package/dist/src/diagnostics.d.ts +0 -4
- package/dist/src/diagnostics.js +1 -0
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/html/bindings.js +3 -0
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/resource.d.ts +3 -6
- package/dist/src/resource.js +59 -10
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/manifest.d.ts +0 -3
- package/dist/src/router/manifest.js +1 -0
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router.d.ts +1 -1
- package/dist/src/router.js +1 -1
- package/dist/src/router.js.map +1 -1
- package/dist/src/signal.d.ts +0 -4
- package/dist/src/signal.js +1 -0
- package/dist/src/signal.js.map +1 -1
- package/docs/en/03-static-bake.md +1 -2
- package/docs/en/06-for-backenders.md +5 -0
- package/docs/en/08-llm-zero-history-test.md +5 -0
- package/docs/en/18-api-freeze-map.md +63 -0
- package/docs/en/19-reactivity-ordering.md +93 -0
- package/docs/en/20-v1-stability.md +83 -0
- package/docs/en/README.md +3 -0
- package/docs/fr/03-static-bake.md +1 -2
- package/docs/fr/06-for-backenders.md +6 -0
- package/docs/fr/08-llm-zero-history-test.md +5 -0
- package/docs/fr/18-api-freeze-map.md +63 -0
- package/docs/fr/19-reactivity-ordering.md +97 -0
- package/docs/fr/20-v1-stability.md +88 -0
- package/docs/fr/README.md +3 -0
- package/docs/ru/03-static-bake.md +2 -3
- package/docs/ru/06-for-backenders.md +6 -0
- package/docs/ru/08-llm-zero-history-test.md +5 -0
- package/docs/ru/18-api-freeze-map.md +62 -0
- package/docs/ru/19-reactivity-ordering.md +95 -0
- package/docs/ru/20-v1-stability.md +82 -0
- package/docs/ru/README.md +3 -0
- package/docs/uk/06-for-backenders.md +5 -0
- package/docs/uk/08-llm-zero-history-test.md +5 -0
- package/docs/uk/18-api-freeze-map.md +61 -0
- package/docs/uk/19-reactivity-ordering.md +95 -0
- package/docs/uk/20-v1-stability.md +83 -0
- package/docs/uk/README.md +3 -0
- package/llms.txt +59 -5
- package/package.json +8 -3
- package/scripts/bake.mjs +0 -1
- package/scripts/cli.mjs +17 -0
- package/scripts/llm-zero-history-smoke.mjs +93 -0
- package/scripts/new.mjs +1 -1
- package/scripts/package-smoke.mjs +74 -0
- package/scripts/size-budget.mjs +88 -0
- package/starters/admin/package.json +2 -2
- package/starters/crud/package.json +2 -2
- 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.
|
|
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
|
|
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`
|
|
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.
|
|
225
|
-
|
|
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.
|
|
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":
|
|
40
|
-
|
|
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
|
+
}
|