@madojs/mado 0.8.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 +81 -4
- package/CHANGELOG.md +202 -1
- package/README.md +184 -242
- package/ROADMAP.md +174 -79
- package/TODO.md +8 -5
- package/dist/src/component.d.ts +2 -12
- package/dist/src/component.js +30 -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/forms.js +17 -0
- package/dist/src/forms.js.map +1 -1
- package/dist/src/html/bindings.js +35 -3
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/html/parser.js +60 -3
- package/dist/src/html/parser.js.map +1 -1
- package/dist/src/lifecycle.js +18 -0
- package/dist/src/lifecycle.js.map +1 -1
- package/dist/src/persisted.js +43 -9
- package/dist/src/persisted.js.map +1 -1
- package/dist/src/resource.d.ts +13 -6
- package/dist/src/resource.js +83 -16
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/manifest.d.ts +0 -3
- package/dist/src/router/manifest.js +23 -2
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/navigation.js +56 -2
- package/dist/src/router/navigation.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 +56 -7
- package/dist/src/signal.js.map +1 -1
- package/docs/en/00-the-mado-way.md +23 -12
- package/docs/en/03-static-bake.md +1 -2
- package/docs/en/05-why-mado.md +78 -68
- package/docs/en/06-for-backenders.md +80 -55
- package/docs/en/07-llm-pitfalls.md +101 -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/00-the-mado-way.md +25 -13
- package/docs/fr/03-static-bake.md +1 -2
- package/docs/fr/06-for-backenders.md +6 -0
- package/docs/fr/07-llm-pitfalls.md +2 -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/00-the-mado-way.md +24 -11
- package/docs/ru/03-static-bake.md +2 -3
- package/docs/ru/06-for-backenders.md +6 -0
- package/docs/ru/07-llm-pitfalls.md +2 -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/00-the-mado-way.md +3 -1
- package/docs/uk/06-for-backenders.md +5 -0
- package/docs/uk/07-llm-pitfalls.md +2 -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 +63 -7
- package/package.json +10 -5
- package/scripts/bake.mjs +0 -1
- package/scripts/bundle.mjs +6 -6
- 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
|
@@ -598,6 +598,105 @@ See [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md) for the full pattern.
|
|
|
598
598
|
|
|
599
599
|
---
|
|
600
600
|
|
|
601
|
+
## Pitfall #23: signal reads in async functions called from `view()` create effect cycles
|
|
602
|
+
|
|
603
|
+
**Symptom:** `[mado] effect cycle detected: subscriber re-ran more than 100 times in one flush.`
|
|
604
|
+
|
|
605
|
+
The router calls `page.view()` inside a reactive effect. Any signal read
|
|
606
|
+
**synchronously** during `view()` subscribes the router's render effect. If that
|
|
607
|
+
signal is then written (e.g. `loading.set(true)`) — the router re-runs `view()`,
|
|
608
|
+
which reads the signal again → infinite loop.
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
// ❌ INFINITE LOOP — loadMore reads signals inside the router's effect
|
|
612
|
+
export default page({
|
|
613
|
+
view: () => {
|
|
614
|
+
const cursor = signal<string | null>("start");
|
|
615
|
+
const loading = signal(false);
|
|
616
|
+
|
|
617
|
+
const loadMore = async () => {
|
|
618
|
+
if (cursor() === null || loading()) return; // ← subscribes render effect!
|
|
619
|
+
loading.set(true); // ← re-triggers render → loadMore() → ∞
|
|
620
|
+
const res = await fetch(`/api/items?cursor=${cursor()}`);
|
|
621
|
+
// ...
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
loadMore(); // called synchronously during view()
|
|
625
|
+
return html`...`;
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// ✅ CORRECT — wrap signal reads in untracked()
|
|
630
|
+
import { untracked } from "@madojs/mado";
|
|
631
|
+
|
|
632
|
+
export default page({
|
|
633
|
+
view: () => {
|
|
634
|
+
const cursor = signal<string | null>("start");
|
|
635
|
+
const loading = signal(false);
|
|
636
|
+
|
|
637
|
+
const loadMore = async () => {
|
|
638
|
+
const c = untracked(() => cursor());
|
|
639
|
+
if (c === null || untracked(() => loading())) return;
|
|
640
|
+
loading.set(true);
|
|
641
|
+
const res = await fetch(`/api/items?cursor=${c}`);
|
|
642
|
+
// ...
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
loadMore();
|
|
646
|
+
return html`...`;
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
**Rule:** Any function that reads signals AND is called synchronously during
|
|
652
|
+
`view()` initialization must use `untracked()` for those reads. This includes:
|
|
653
|
+
|
|
654
|
+
- Data fetching / loadMore functions
|
|
655
|
+
- IntersectionObserver callbacks set up during init
|
|
656
|
+
- Timer/polling setup functions that check state
|
|
657
|
+
|
|
658
|
+
Signals read inside the **returned template** (`html\`...\``) are fine — they are
|
|
659
|
+
wrapped in a child-binding function `${() => ...}` which creates its own effect.
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## Pitfall #24: `setInterval` / manual subscriptions in `page()` view without cleanup
|
|
664
|
+
|
|
665
|
+
**Symptom:** After navigating away, timers/subscriptions keep running (zombie intervals,
|
|
666
|
+
server logs show polling requests from pages the user already left).
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
// ❌ ZOMBIE — interval survives navigation
|
|
670
|
+
export default page({
|
|
671
|
+
view: () => {
|
|
672
|
+
const tick = signal(0);
|
|
673
|
+
setInterval(() => tick.update((n) => n + 1), 3000); // never cleaned up!
|
|
674
|
+
return html`<div>${tick}</div>`;
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// ✅ CORRECT — use onDispose for cleanup
|
|
679
|
+
export default page({
|
|
680
|
+
view: ({ onDispose }) => {
|
|
681
|
+
const tick = signal(0);
|
|
682
|
+
const id = setInterval(() => tick.update((n) => n + 1), 3000);
|
|
683
|
+
onDispose(() => clearInterval(id));
|
|
684
|
+
return html`<div>${tick}</div>`;
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
**Note:** `resource()` and `effect()` created inside `view()` are automatically
|
|
690
|
+
cleaned up on navigation (they register with the page lifecycle). Only raw
|
|
691
|
+
browser APIs need explicit `onDispose()`:
|
|
692
|
+
|
|
693
|
+
- `setInterval` / `setTimeout`
|
|
694
|
+
- `addEventListener` (on window/document)
|
|
695
|
+
- `WebSocket` / `EventSource`
|
|
696
|
+
- `IntersectionObserver` / `ResizeObserver`
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
601
700
|
## Cheat-sheet for AI
|
|
602
701
|
|
|
603
702
|
| If you want to do… | Correct in Mado |
|
|
@@ -619,5 +718,7 @@ See [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md) for the full pattern.
|
|
|
619
718
|
| `@customElement('x')` | `component('x-name', setup)` |
|
|
620
719
|
| `host.getAttribute('x')` in render | `ctx.attr('x', default)` (reactive) |
|
|
621
720
|
| `jsonFetcher()` with auth | `apiFetcher()` (attaches Bearer token) |
|
|
721
|
+
| `setInterval` in page view | `onDispose(() => clearInterval(id))` |
|
|
722
|
+
| signal read in view() async init | `untracked(() => cursor())` |
|
|
622
723
|
|
|
623
724
|
If something doesn't fit this list — open `src/` and **read 500 lines**. Seriously. Mado is intentionally small to be readable.
|
|
@@ -50,6 +50,11 @@ Look for these after implementation:
|
|
|
50
50
|
The current `examples/tickets` implementation did not require new public APIs or
|
|
51
51
|
runtime dependencies.
|
|
52
52
|
|
|
53
|
+
CI runs `npm run llm:smoke` as a deterministic proxy for this task: it verifies
|
|
54
|
+
that `llms.txt` still contains the key guidance, checks the committed
|
|
55
|
+
`examples/tickets` artifact against the required Mado API surface and failure
|
|
56
|
+
patterns, then builds and runs `test/tickets-smoke.test.mjs`.
|
|
57
|
+
|
|
53
58
|
The main documentation pressure point remains lifecycle: older examples can make
|
|
54
59
|
it look acceptable to create `resource()` directly in `page.view()`. The tickets
|
|
55
60
|
example uses page-level wrapper components instead, so resources are registered
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# API freeze map
|
|
2
|
+
|
|
3
|
+
> What is public, what is internal, and what SemVer will protect at v1.
|
|
4
|
+
|
|
5
|
+
Mado's v1 contract is intentionally small. Import application code from the
|
|
6
|
+
package root:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { component, html, resource, routes, signal } from "@madojs/mado";
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The only public package subpath is the side-effect devtools module:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import "@madojs/mado/devtools.js";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Everything else under `dist/src/` is an implementation detail, even when it is
|
|
19
|
+
visible in the repository.
|
|
20
|
+
|
|
21
|
+
## Stable public API
|
|
22
|
+
|
|
23
|
+
These names are public and protected by SemVer once v1 ships:
|
|
24
|
+
|
|
25
|
+
- Reactivity: `signal`, `computed`, `effect`, `untracked`, `batch`,
|
|
26
|
+
`flushSync`.
|
|
27
|
+
- Templates and directives: `html`, `render`, `each`, `list`, `unsafeHTML`,
|
|
28
|
+
`ref`, `classMap`, `styleMap`.
|
|
29
|
+
- Components and CSS: `component`, `css`, `cssVars`.
|
|
30
|
+
- Routing and pages: `routes`, `router`, `page`, `layout`, `nested`,
|
|
31
|
+
`navigate`, `queryParam`, `prefetchPath`.
|
|
32
|
+
- Data: `resource`, `mutation`, `invalidate`, `jsonFetcher`, `HttpError`.
|
|
33
|
+
- Forms: `useForm`.
|
|
34
|
+
- Head and persistence: `applyHead`, `persisted`.
|
|
35
|
+
- Context: `createContext`, `provide`, `inject`.
|
|
36
|
+
- Advanced lifecycle helpers: `createLifecycle`, `runInLifecycle`,
|
|
37
|
+
`getCurrentLifecycle`.
|
|
38
|
+
- Public TypeScript types exported from `@madojs/mado`.
|
|
39
|
+
|
|
40
|
+
## Internal or unstable
|
|
41
|
+
|
|
42
|
+
These are not public API:
|
|
43
|
+
|
|
44
|
+
- Package subpaths other than `@madojs/mado` and
|
|
45
|
+
`@madojs/mado/devtools.js`.
|
|
46
|
+
- Template parser/binding internals such as `html/parser.js`,
|
|
47
|
+
`html/bindings.js`, `ChildState`, and `EachEntry`.
|
|
48
|
+
- Router implementation modules such as `router/match.js`,
|
|
49
|
+
`router/navigation.js`, and `router/manifest.js`.
|
|
50
|
+
- Diagnostics internals and all `_testHooks`.
|
|
51
|
+
- Exact generated bundle text, chunk names, and internal file layout.
|
|
52
|
+
|
|
53
|
+
The repository's tests may import internal files through relative `dist/` paths.
|
|
54
|
+
Application code should not.
|
|
55
|
+
|
|
56
|
+
## What can change
|
|
57
|
+
|
|
58
|
+
Patch and minor releases may add new root exports, options, diagnostics, docs,
|
|
59
|
+
or starter files. They may also change internals, emitted bundle shape, and
|
|
60
|
+
implementation details as long as the stable API and documented behavior remain
|
|
61
|
+
compatible.
|
|
62
|
+
|
|
63
|
+
Breaking changes to the stable API require a major version.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Reactivity ordering
|
|
2
|
+
|
|
3
|
+
> The small set of ordering guarantees Mado treats as public behaviour.
|
|
4
|
+
|
|
5
|
+
Mado's reactivity is synchronous for reads and scheduled for side effects. The
|
|
6
|
+
goal is boring, predictable UI updates rather than a large scheduling model.
|
|
7
|
+
|
|
8
|
+
## Signals
|
|
9
|
+
|
|
10
|
+
`signal(value)` returns a getter function. Calling `set(next)` changes the value
|
|
11
|
+
immediately unless `Object.is(previous, next)` is true.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const count = signal(0);
|
|
15
|
+
count.set(1);
|
|
16
|
+
count(); // 1, immediately
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Computed values are marked before effects run, so an effect that reads a
|
|
20
|
+
computed value observes the current dependencies, not stale cached data.
|
|
21
|
+
|
|
22
|
+
## Effects
|
|
23
|
+
|
|
24
|
+
`effect(fn)` runs once immediately. Later dependency changes schedule one effect
|
|
25
|
+
run in a microtask. Tests can call `flushSync()` to drain that queue
|
|
26
|
+
synchronously.
|
|
27
|
+
|
|
28
|
+
If an effect returns a cleanup function, Mado runs that cleanup before the next
|
|
29
|
+
effect run and again when the effect disposer is called.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const stop = effect(() => {
|
|
33
|
+
const id = setInterval(tick, 1000);
|
|
34
|
+
return () => clearInterval(id);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
stop();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
In components and pages, prefer `ctx.onDispose()` / page `onDispose()` for
|
|
41
|
+
unmount cleanup. Effect cleanup is per-run cleanup.
|
|
42
|
+
|
|
43
|
+
## Batch
|
|
44
|
+
|
|
45
|
+
`batch(fn)` groups signal writes into one subscriber pass. Effects do not run
|
|
46
|
+
until the outermost batch exits, including nested batches.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
batch(() => {
|
|
50
|
+
first.set("Ada");
|
|
51
|
+
batch(() => last.set("Lovelace"));
|
|
52
|
+
});
|
|
53
|
+
// effects see only the final pair
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Observed `computed({ equals })` values also preserve batch atomicity: they
|
|
57
|
+
recompute once after the outermost batch, on the fully applied state. They must
|
|
58
|
+
not observe a half-applied batch such as `(new x, old y)`.
|
|
59
|
+
|
|
60
|
+
## DOM updates
|
|
61
|
+
|
|
62
|
+
`render(result, container)` reuses the existing template instance when the next
|
|
63
|
+
render has the same template strings. For child bindings that return a nested
|
|
64
|
+
`html```, Mado applies the same rule: same template strings update in place,
|
|
65
|
+
different template strings rebuild that branch.
|
|
66
|
+
|
|
67
|
+
This means unrelated signal changes do not recreate an `<input>` inside a
|
|
68
|
+
stable nested template, so focus, DOM state and listeners survive.
|
|
69
|
+
|
|
70
|
+
Lists should use `each(items, key, renderItem)`. Keys define DOM identity.
|
|
71
|
+
Duplicate keys warn in development and fall back to a positional suffix so every
|
|
72
|
+
item still renders, but duplicate keys are a data bug.
|
|
73
|
+
|
|
74
|
+
## Component teardown
|
|
75
|
+
|
|
76
|
+
Custom elements may receive `disconnectedCallback()` followed by
|
|
77
|
+
`connectedCallback()` during a same-tick move. Mado defers component teardown to
|
|
78
|
+
a microtask and cancels it when the element is reconnected, so keyed reorders
|
|
79
|
+
preserve component state. A genuine removal still runs lifecycle cleanup on the
|
|
80
|
+
next microtask.
|
|
81
|
+
|
|
82
|
+
## Not guaranteed
|
|
83
|
+
|
|
84
|
+
Mado does not guarantee the exact number of internal scheduler microtasks, the
|
|
85
|
+
order of independent effects that do not share dependencies, generated bundle
|
|
86
|
+
shape, or internal module layout. Those are implementation details.
|
|
87
|
+
|
|
88
|
+
The invariant tests for this contract live in:
|
|
89
|
+
|
|
90
|
+
- `test/reactivity-ordering.test.mjs`
|
|
91
|
+
- `test/signal-batch-equals.test.mjs`
|
|
92
|
+
- `test/update-nested-reuse.test.mjs`
|
|
93
|
+
- `test/each-component-state.test.mjs`
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# v1 stability
|
|
2
|
+
|
|
3
|
+
> What Mado promises after v1, and what remains free to evolve.
|
|
4
|
+
|
|
5
|
+
Mado v1 means the public app-facing contract is stable enough for real
|
|
6
|
+
business apps. It does not mean every internal file, generated byte, starter
|
|
7
|
+
copy, or diagnostic string is frozen forever.
|
|
8
|
+
|
|
9
|
+
Read this together with:
|
|
10
|
+
|
|
11
|
+
- [API freeze map](./18-api-freeze-map.md)
|
|
12
|
+
- [Reactivity ordering](./19-reactivity-ordering.md)
|
|
13
|
+
|
|
14
|
+
## Stable under SemVer
|
|
15
|
+
|
|
16
|
+
After v1, Mado treats these as SemVer-protected:
|
|
17
|
+
|
|
18
|
+
- Public exports from `@madojs/mado`.
|
|
19
|
+
- Public TypeScript types exported from `@madojs/mado`.
|
|
20
|
+
- The `@madojs/mado/devtools.js` side-effect subpath.
|
|
21
|
+
- Template binding syntax: child `${}`, `@event`, `.prop`, `?boolean`,
|
|
22
|
+
attribute bindings, directives and `each()`.
|
|
23
|
+
- Signal semantics documented in the reactivity ordering guide.
|
|
24
|
+
- Component lifecycle semantics: setup once per connection lifetime, deferred
|
|
25
|
+
teardown for same-tick moves, cleanup via `ctx.onDispose`.
|
|
26
|
+
- Router/page/resource/form contracts documented in the English docs.
|
|
27
|
+
- CLI command names and broad command intent (`build`, `dev`, `release`,
|
|
28
|
+
`bake`, `bundle`, `preview`, `init`, `new`).
|
|
29
|
+
|
|
30
|
+
Breaking these requires a major version.
|
|
31
|
+
|
|
32
|
+
## Allowed in minor releases
|
|
33
|
+
|
|
34
|
+
Minor releases may add:
|
|
35
|
+
|
|
36
|
+
- New root exports.
|
|
37
|
+
- New options on existing APIs.
|
|
38
|
+
- New diagnostics and warnings.
|
|
39
|
+
- New starters, examples, docs and CLI flags.
|
|
40
|
+
- Performance improvements and internal implementation rewrites.
|
|
41
|
+
|
|
42
|
+
Minor releases should not require existing correct apps to change code.
|
|
43
|
+
|
|
44
|
+
## Allowed in patch releases
|
|
45
|
+
|
|
46
|
+
Patch releases may fix bugs, tighten diagnostics, improve docs, and make
|
|
47
|
+
compatible implementation changes. A patch may change timing only when the old
|
|
48
|
+
timing was an undocumented bug and the change preserves the documented
|
|
49
|
+
reactivity ordering contract.
|
|
50
|
+
|
|
51
|
+
## Not stable
|
|
52
|
+
|
|
53
|
+
These are intentionally not SemVer-protected:
|
|
54
|
+
|
|
55
|
+
- Internal package subpaths other than `@madojs/mado/devtools.js`.
|
|
56
|
+
- Files under `src/`, `dist/src/`, and implementation module boundaries.
|
|
57
|
+
- `_testHooks`, diagnostics internals and warning codes.
|
|
58
|
+
- Exact generated JavaScript text, chunk names, sourcemap content and bundle
|
|
59
|
+
byte layout.
|
|
60
|
+
- Internal parser, binding, router and resource cache data structures.
|
|
61
|
+
- Starter app visual copy and demo data.
|
|
62
|
+
|
|
63
|
+
Apps should not import internal files or assert exact bundle output.
|
|
64
|
+
|
|
65
|
+
## Bundle and release output
|
|
66
|
+
|
|
67
|
+
Mado will keep a size budget and deterministic release tests, but v1 stability
|
|
68
|
+
does not freeze byte-for-byte bundler output. Hashes, chunk boundaries and
|
|
69
|
+
generated asset names may change as long as the documented deployment contract
|
|
70
|
+
continues to work.
|
|
71
|
+
|
|
72
|
+
## If a release breaks you
|
|
73
|
+
|
|
74
|
+
If an update breaks code that uses only public exports and documented behaviour,
|
|
75
|
+
treat it as a bug. Open an issue with:
|
|
76
|
+
|
|
77
|
+
- the Mado version before and after;
|
|
78
|
+
- the public API involved;
|
|
79
|
+
- a minimal reproduction;
|
|
80
|
+
- whether the break is runtime behaviour, TypeScript types, CLI output or docs.
|
|
81
|
+
|
|
82
|
+
If the break depends on an internal subpath or exact generated output, it may
|
|
83
|
+
still be worth reporting, but it is not considered a SemVer break.
|
package/docs/en/README.md
CHANGED
|
@@ -22,3 +22,6 @@ English documentation set.
|
|
|
22
22
|
| Error handling | [15-error-handling.md](./15-error-handling.md) |
|
|
23
23
|
| Bake cookbook | [16-bake-cookbook.md](./16-bake-cookbook.md) |
|
|
24
24
|
| Shadow DOM + Forms | [17-shadow-dom-forms.md](./17-shadow-dom-forms.md) |
|
|
25
|
+
| API freeze map | [18-api-freeze-map.md](./18-api-freeze-map.md) |
|
|
26
|
+
| Reactivity ordering | [19-reactivity-ordering.md](./19-reactivity-ordering.md) |
|
|
27
|
+
| v1 stability | [20-v1-stability.md](./20-v1-stability.md) |
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
> Une seule bonne façon. Des contrats stricts. Pas de magie.
|
|
4
4
|
|
|
5
|
-
Mado
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Mado est un framework pour les équipes qui construisent des panneaux d'admin,
|
|
6
|
+
des outils internes et des SPA métier — des apps qui doivent être simples à
|
|
7
|
+
créer et ennuyeuses à maintenir. Pour cela, il impose un **ensemble de
|
|
8
|
+
conventions**. Si vous les respectez, le projet reste compréhensible même avec
|
|
9
|
+
200 écrans et 5 développeurs. Si vous les enfreignez — les types et le linter
|
|
10
|
+
vous le diront immédiatement.
|
|
8
11
|
|
|
9
12
|
## Principes
|
|
10
13
|
|
|
@@ -42,13 +45,21 @@ tous écrire de la même manière.
|
|
|
42
45
|
|
|
43
46
|
```ts
|
|
44
47
|
// src/components/user-card.ts
|
|
45
|
-
import { component, html, css } from
|
|
46
|
-
|
|
47
|
-
component(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
48
|
+
import { component, html, css } from "@madojs/mado";
|
|
49
|
+
|
|
50
|
+
component(
|
|
51
|
+
"x-user-card",
|
|
52
|
+
() => {
|
|
53
|
+
return () => html`<div class="card"><slot /></div>`;
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
styles: css`
|
|
57
|
+
.card {
|
|
58
|
+
padding: 1rem;
|
|
59
|
+
}
|
|
60
|
+
`,
|
|
61
|
+
},
|
|
62
|
+
);
|
|
52
63
|
```
|
|
53
64
|
|
|
54
65
|
`import './components/user-card.js'` **enregistre** le composant via
|
|
@@ -63,7 +74,7 @@ component('x-user-card', () => {
|
|
|
63
74
|
const user = resource(() => `/api/users/${id()}`, jsonFetcher());
|
|
64
75
|
|
|
65
76
|
// écriture → mutation
|
|
66
|
-
const save = mutation(api.save, { invalidates: [
|
|
77
|
+
const save = mutation(api.save, { invalidates: ["/api/users*"] });
|
|
67
78
|
```
|
|
68
79
|
|
|
69
80
|
Cela fournit la mise en cache, l'annulation, la gestion des erreurs et l'invalidation automatique.
|
|
@@ -72,11 +83,11 @@ Cela fournit la mise en cache, l'annulation, la gestion des erreurs et l'invalid
|
|
|
72
83
|
|
|
73
84
|
```ts
|
|
74
85
|
// src/pages/user-profile.ts
|
|
75
|
-
import { page, html, resource, jsonFetcher } from
|
|
86
|
+
import { page, html, resource, jsonFetcher } from "@madojs/mado";
|
|
76
87
|
|
|
77
88
|
export default page({
|
|
78
89
|
title: ({ id }) => `Utilisateur #${id}`,
|
|
79
|
-
view:
|
|
90
|
+
view: ({ params }) => html`...`,
|
|
80
91
|
});
|
|
81
92
|
```
|
|
82
93
|
|
|
@@ -101,6 +112,7 @@ Voir [`01-routing.md`](./01-routing.md).
|
|
|
101
112
|
## En cas de doute
|
|
102
113
|
|
|
103
114
|
Si vous vous demandez "quelle est la meilleure façon ici ?" — c'est un signal que :
|
|
115
|
+
|
|
104
116
|
1. Soit il existe un helper intégré que vous ne connaissez pas (consultez `docs/`).
|
|
105
117
|
2. Soit c'est une nouvelle situation — discutez-en et **consignez-la** dans ce document
|
|
106
118
|
comme une convention supplémentaire.
|
|
@@ -148,7 +148,6 @@ out/
|
|
|
148
148
|
{"@context":"https://schema.org","@type":"Product","..."}
|
|
149
149
|
</script>
|
|
150
150
|
<meta name="bake-revalidate" content="3600" data-mado-head="baked">
|
|
151
|
-
<meta name="bake-stamp" content="1234567890" data-mado-head="baked">
|
|
152
151
|
</head>
|
|
153
152
|
<body>
|
|
154
153
|
<div id="app">
|
|
@@ -231,7 +230,7 @@ export default page<{ slug: string }>({
|
|
|
231
230
|
|
|
232
231
|
## Revalidate / CDN
|
|
233
232
|
|
|
234
|
-
`bake.revalidate: 3600` écrit `<meta name="bake-revalidate" content="3600">`
|
|
233
|
+
`bake.revalidate: 3600` écrit `<meta name="bake-revalidate" content="3600">`
|
|
235
234
|
dans le HTML. C'est des **métadonnées** — le framework ne re-bake rien lui-même. Stratégies :
|
|
236
235
|
|
|
237
236
|
1. **Option la plus simple** : cron dans CI — `npm run bake && rsync out/ origin:/var/www/`.
|
|
@@ -154,6 +154,12 @@ await save.run(newUser);
|
|
|
154
154
|
// automatiquement : user.data() se mettra à jour si le glob correspond
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
Les clés de `resource()` sont l'identité du cache. Incluez l'endpoint, les query
|
|
158
|
+
params et la forme des données dans la clé : deux `resource()` vivants avec la
|
|
159
|
+
même clé partagent le cache et la requête in-flight. Si la même clé est utilisée
|
|
160
|
+
avec un fetcher différent, Mado avertit, car cela signifie généralement que la
|
|
161
|
+
clé de cache est trop large.
|
|
162
|
+
|
|
157
163
|
Si une telle abstraction existait dans le monde Go pour les caches côté serveur — on
|
|
158
164
|
pleurerait tous de joie.
|
|
159
165
|
|
|
@@ -619,5 +619,7 @@ Plus de détails : [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md).
|
|
|
619
619
|
| `@customElement('x')` | `component('x-name', setup)` |
|
|
620
620
|
| `host.getAttribute('x')` dans render | `ctx.attr('x', default)` (réactif) |
|
|
621
621
|
| `jsonFetcher()` avec auth | `apiFetcher()` (attache le Bearer token) |
|
|
622
|
+
| `setInterval` dans page view | `onDispose(() => clearInterval(id))` |
|
|
623
|
+
| lecture signal dans async init view() | `untracked(() => cursor())` |
|
|
622
624
|
|
|
623
625
|
Si quelque chose ne rentre pas dans cette liste — ouvrez `src/` et **lisez 500 lignes**. Sérieusement. Mado est intentionnellement petit pour être lisible.
|
|
@@ -53,6 +53,11 @@ Cherchez ces éléments après l'implémentation :
|
|
|
53
53
|
L'implémentation actuelle de `examples/tickets` n'a pas nécessité de nouvelles API publiques ni
|
|
54
54
|
de dépendances runtime.
|
|
55
55
|
|
|
56
|
+
CI exécute `npm run llm:smoke` comme proxy déterministe pour cette tâche :
|
|
57
|
+
il vérifie que `llms.txt` contient toujours les règles clés, compare l'artefact
|
|
58
|
+
commité `examples/tickets` à la surface API Mado requise et aux failure
|
|
59
|
+
patterns, puis build le projet et lance `test/tickets-smoke.test.mjs`.
|
|
60
|
+
|
|
56
61
|
Le principal point de pression dans la documentation reste le lifecycle : les anciens exemples
|
|
57
62
|
peuvent donner l'impression qu'il est acceptable de créer `resource()` directement dans
|
|
58
63
|
`page.view()`. L'exemple tickets utilise plutôt des composants wrapper au niveau page, de sorte
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Carte de gel de l'API
|
|
2
|
+
|
|
3
|
+
> Ce qui est public, ce qui est interne, et ce que SemVer protégera en v1.
|
|
4
|
+
|
|
5
|
+
Le contrat v1 de Mado est volontairement petit. Le code applicatif importe
|
|
6
|
+
depuis la racine du package :
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { component, html, resource, routes, signal } from "@madojs/mado";
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Le seul subpath public est le module side-effect devtools :
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import "@madojs/mado/devtools.js";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Tout le reste sous `dist/src/` est un détail d'implémentation, même si le
|
|
19
|
+
fichier est visible dans le dépôt.
|
|
20
|
+
|
|
21
|
+
## API publique stable
|
|
22
|
+
|
|
23
|
+
Ces noms sont publics et protégés par SemVer une fois v1 publiée :
|
|
24
|
+
|
|
25
|
+
- Réactivité : `signal`, `computed`, `effect`, `untracked`, `batch`,
|
|
26
|
+
`flushSync`.
|
|
27
|
+
- Templates et directives : `html`, `render`, `each`, `list`, `unsafeHTML`,
|
|
28
|
+
`ref`, `classMap`, `styleMap`.
|
|
29
|
+
- Composants et CSS : `component`, `css`, `cssVars`.
|
|
30
|
+
- Routage et pages : `routes`, `router`, `page`, `layout`, `nested`,
|
|
31
|
+
`navigate`, `queryParam`, `prefetchPath`.
|
|
32
|
+
- Data : `resource`, `mutation`, `invalidate`, `jsonFetcher`, `HttpError`.
|
|
33
|
+
- Formulaires : `useForm`.
|
|
34
|
+
- Head et persistence : `applyHead`, `persisted`.
|
|
35
|
+
- Context : `createContext`, `provide`, `inject`.
|
|
36
|
+
- Helpers lifecycle avancés : `createLifecycle`, `runInLifecycle`,
|
|
37
|
+
`getCurrentLifecycle`.
|
|
38
|
+
- Types TypeScript publics exportés depuis `@madojs/mado`.
|
|
39
|
+
|
|
40
|
+
## Interne ou instable
|
|
41
|
+
|
|
42
|
+
Ce n'est pas de l'API publique :
|
|
43
|
+
|
|
44
|
+
- Subpaths du package autres que `@madojs/mado` et
|
|
45
|
+
`@madojs/mado/devtools.js`.
|
|
46
|
+
- Internals du parser/binding comme `html/parser.js`, `html/bindings.js`,
|
|
47
|
+
`ChildState` et `EachEntry`.
|
|
48
|
+
- Internals du routeur comme `router/match.js`, `router/navigation.js` et
|
|
49
|
+
`router/manifest.js`.
|
|
50
|
+
- Internals de diagnostics et tous les `_testHooks`.
|
|
51
|
+
- Texte exact du bundle généré, noms des chunks et layout interne des fichiers.
|
|
52
|
+
|
|
53
|
+
Les tests du dépôt peuvent importer des fichiers internes via des chemins
|
|
54
|
+
relatifs `dist/`. Le code applicatif ne doit pas le faire.
|
|
55
|
+
|
|
56
|
+
## Ce qui peut changer
|
|
57
|
+
|
|
58
|
+
Les patch et minor releases peuvent ajouter des root exports, options,
|
|
59
|
+
diagnostics, docs ou starters. Elles peuvent aussi changer les internals, la
|
|
60
|
+
forme du bundle et les détails d'implémentation tant que l'API stable et le
|
|
61
|
+
comportement documenté restent compatibles.
|
|
62
|
+
|
|
63
|
+
Les changements cassants de l'API stable nécessitent une version majeure.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Ordre de la réactivité
|
|
2
|
+
|
|
3
|
+
> Le petit ensemble de garanties d'ordre que Mado traite comme comportement public.
|
|
4
|
+
|
|
5
|
+
La réactivité de Mado est synchrone pour les lectures et planifiée pour les
|
|
6
|
+
side effects. Le but est une mise à jour UI prévisible plutôt qu'un grand modèle
|
|
7
|
+
de scheduling.
|
|
8
|
+
|
|
9
|
+
## Signals
|
|
10
|
+
|
|
11
|
+
`signal(value)` renvoie une fonction getter. `set(next)` change la valeur
|
|
12
|
+
immédiatement sauf si `Object.is(previous, next)` vaut `true`.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
const count = signal(0);
|
|
16
|
+
count.set(1);
|
|
17
|
+
count(); // 1, immédiatement
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Les computed values sont marquées avant l'exécution des effects : un effect qui
|
|
21
|
+
lit un computed observe les dépendances courantes, pas un cache obsolète.
|
|
22
|
+
|
|
23
|
+
## Effects
|
|
24
|
+
|
|
25
|
+
`effect(fn)` s'exécute une première fois immédiatement. Les changements
|
|
26
|
+
ultérieurs de dépendances planifient une seule exécution en microtask. Les tests
|
|
27
|
+
peuvent appeler `flushSync()` pour vider cette file de façon synchrone.
|
|
28
|
+
|
|
29
|
+
Si un effect retourne une fonction de cleanup, Mado l'exécute avant l'exécution
|
|
30
|
+
suivante de l'effect et à nouveau quand le disposer est appelé.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const stop = effect(() => {
|
|
34
|
+
const id = setInterval(tick, 1000);
|
|
35
|
+
return () => clearInterval(id);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
stop();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Dans les composants et pages, préférez `ctx.onDispose()` / page `onDispose()`
|
|
42
|
+
pour le cleanup à l'unmount. Le cleanup d'un effect est un cleanup par run.
|
|
43
|
+
|
|
44
|
+
## Batch
|
|
45
|
+
|
|
46
|
+
`batch(fn)` regroupe les écritures de signals en un seul passage des
|
|
47
|
+
subscribers. Les effects ne s'exécutent pas avant la sortie du batch le plus
|
|
48
|
+
extérieur, y compris avec des batches imbriqués.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
batch(() => {
|
|
52
|
+
first.set("Ada");
|
|
53
|
+
batch(() => last.set("Lovelace"));
|
|
54
|
+
});
|
|
55
|
+
// les effects ne voient que la paire finale
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Les `computed({ equals })` observés préservent aussi l'atomicité du batch : ils
|
|
59
|
+
se recalculent une fois après le batch extérieur, sur l'état entièrement
|
|
60
|
+
appliqué. Ils ne doivent pas observer un batch à moitié appliqué comme
|
|
61
|
+
`(new x, old y)`.
|
|
62
|
+
|
|
63
|
+
## Mises à jour DOM
|
|
64
|
+
|
|
65
|
+
`render(result, container)` réutilise l'instance de template existante quand le
|
|
66
|
+
render suivant a les mêmes template strings. Pour les child bindings qui
|
|
67
|
+
retournent un `html``` imbriqué, la même règle s'applique : mêmes strings =
|
|
68
|
+
mise à jour sur place, autres strings = reconstruction de cette branche.
|
|
69
|
+
|
|
70
|
+
Ainsi, des changements de signals sans rapport ne recréent pas un `<input>`
|
|
71
|
+
dans un template imbriqué stable : focus, état DOM et listeners survivent.
|
|
72
|
+
|
|
73
|
+
Les listes doivent utiliser `each(items, key, renderItem)`. Les keys définissent
|
|
74
|
+
l'identité DOM. Les duplicate keys avertissent en development et retombent sur
|
|
75
|
+
un suffixe positionnel pour que chaque item soit rendu, mais les duplicate keys
|
|
76
|
+
sont un bug de données.
|
|
77
|
+
|
|
78
|
+
## Teardown des composants
|
|
79
|
+
|
|
80
|
+
Les custom elements peuvent recevoir `disconnectedCallback()` puis
|
|
81
|
+
`connectedCallback()` pendant un déplacement dans le même tick. Mado diffère le
|
|
82
|
+
teardown du composant jusqu'à une microtask et l'annule si l'élément est
|
|
83
|
+
reconnecté, donc les reorders keyed préservent l'état du composant. Une vraie
|
|
84
|
+
suppression lance tout de même le cleanup lifecycle à la microtask suivante.
|
|
85
|
+
|
|
86
|
+
## Non garanti
|
|
87
|
+
|
|
88
|
+
Mado ne garantit pas le nombre exact de microtasks internes du scheduler,
|
|
89
|
+
l'ordre des effects indépendants sans dépendances communes, la forme du bundle
|
|
90
|
+
généré ni le layout interne des modules. Ce sont des détails d'implémentation.
|
|
91
|
+
|
|
92
|
+
Les tests invariants de ce contrat vivent dans :
|
|
93
|
+
|
|
94
|
+
- `test/reactivity-ordering.test.mjs`
|
|
95
|
+
- `test/signal-batch-equals.test.mjs`
|
|
96
|
+
- `test/update-nested-reuse.test.mjs`
|
|
97
|
+
- `test/each-component-state.test.mjs`
|