@madojs/mado 0.11.1 → 0.12.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 +102 -25
- package/CHANGELOG.md +158 -1
- package/README.md +135 -172
- package/dist/src/component.d.ts +3 -2
- package/dist/src/component.js +98 -6
- package/dist/src/component.js.map +1 -1
- package/dist/src/css.js +34 -5
- package/dist/src/css.js.map +1 -1
- package/dist/src/head.d.ts +6 -6
- package/dist/src/head.js +6 -6
- package/dist/src/html/parser.d.ts +1 -1
- package/dist/src/html/parser.js +1 -1
- package/dist/src/html/template.js +24 -10
- package/dist/src/html/template.js.map +1 -1
- package/dist/src/index.d.ts +8 -4
- package/dist/src/index.js +13 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/page.d.ts +62 -28
- package/dist/src/page.js +5 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/resource.js +2 -1
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/base.d.ts +65 -0
- package/dist/src/router/base.js +168 -0
- package/dist/src/router/base.js.map +1 -0
- package/dist/src/router/manifest.js +75 -31
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +16 -2
- package/dist/src/router/match.js +41 -1
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.js +58 -9
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/static-runtime.d.ts +81 -0
- package/dist/src/static-runtime.js +209 -0
- package/dist/src/static-runtime.js.map +1 -0
- package/dist/src/vite/index.d.ts +31 -3
- package/dist/src/vite/index.js +49 -18
- package/dist/src/vite/index.js.map +1 -1
- package/docs/README.md +5 -8
- package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
- package/docs/en/00-the-mado-way.md +1 -1
- package/docs/en/01-quickstart.md +183 -0
- package/docs/en/10-pages-and-components.md +271 -0
- package/docs/en/11-templates-and-signals.md +211 -0
- package/docs/en/12-routing.md +229 -0
- package/docs/en/13-data.md +225 -0
- package/docs/en/14-forms.md +244 -0
- package/docs/en/15-static-snapshots.md +181 -0
- package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
- package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
- package/docs/en/{14-testing.md → 22-testing.md} +6 -5
- package/docs/en/23-cookbook.md +292 -0
- package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
- package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
- package/docs/en/40-llm-guide.md +776 -0
- package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
- package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
- package/docs/en/README.md +97 -27
- package/docs/recipes/nginx/Containerfile +26 -0
- package/docs/recipes/nginx/nginx.conf +42 -0
- package/llms.txt +246 -211
- package/package.json +17 -10
- package/scripts/bake.mjs +4 -569
- package/scripts/cli/generate.mjs +55 -5
- package/scripts/cli/help.mjs +11 -5
- package/scripts/cli/index.mjs +20 -4
- package/scripts/cli/init.mjs +7 -2
- package/scripts/cli/release.mjs +20 -68
- package/scripts/docs-lint.mjs +170 -0
- package/scripts/package-smoke.mjs +67 -9
- package/scripts/preview.mjs +95 -6
- package/scripts/size-budget.mjs +5 -1
- package/scripts/static/browser.mjs +654 -0
- package/scripts/static/discover.mjs +281 -0
- package/scripts/static/output.mjs +141 -0
- package/scripts/static/serialize.mjs +212 -0
- package/scripts/static/server.mjs +167 -0
- package/scripts/static.mjs +191 -0
- package/starters/default/README.md +57 -55
- package/starters/default/package.json +3 -9
- package/starters/default/src/app.routes.ts +20 -33
- package/starters/default/src/components/feature-card.component.ts +40 -0
- package/starters/default/src/components/live-counter.component.ts +57 -0
- package/starters/default/src/content/guides.ts +55 -0
- package/starters/default/src/main.ts +9 -13
- package/starters/default/src/pages/app.page.ts +41 -0
- package/starters/default/src/pages/guide.page.ts +40 -0
- package/starters/default/src/pages/home.page.ts +49 -0
- package/starters/default/src/pages/not-found.page.ts +18 -0
- package/starters/default/src/styles/document.css +39 -0
- package/starters/default/src/styles/reset.css +37 -0
- package/starters/default/src/styles/tokens.css +30 -0
- package/starters/default/src/styles.d.ts +3 -1
- package/starters/default/src/vite-env.d.ts +1 -1
- package/starters/default/vite.config.ts +14 -1
- package/starters/modular/README.md +142 -0
- package/starters/modular/index.html +13 -0
- package/starters/modular/package.json +30 -0
- package/starters/modular/src/app.routes.ts +39 -0
- package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
- package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
- package/starters/modular/src/main.ts +16 -0
- package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
- package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
- package/starters/modular/src/modules/home/not-found.page.ts +14 -0
- package/starters/modular/src/styles.d.ts +1 -0
- package/starters/modular/src/vite-env.d.ts +1 -0
- package/starters/modular/tsconfig.json +24 -0
- package/starters/modular/vite.config.ts +131 -0
- package/TODO.md +0 -79
- package/docs/en/01-routing.md +0 -152
- package/docs/en/02-project-layout.md +0 -124
- package/docs/en/03-static-bake.md +0 -249
- package/docs/en/04-ide-setup.md +0 -162
- package/docs/en/07-llm-pitfalls.md +0 -724
- package/docs/en/08-llm-zero-history-test.md +0 -56
- package/docs/en/09-shadow-vs-light-dom.md +0 -174
- package/docs/en/11-layouts.md +0 -113
- package/docs/en/12-auth-and-api.md +0 -124
- package/docs/en/16-bake-cookbook.md +0 -100
- package/docs/en/17-shadow-dom-forms.md +0 -192
- package/docs/fr/00-the-mado-way.md +0 -85
- package/docs/fr/01-routing.md +0 -120
- package/docs/fr/02-project-layout.md +0 -84
- package/docs/fr/03-static-bake.md +0 -289
- package/docs/fr/04-ide-setup.md +0 -162
- package/docs/fr/05-why-mado.md +0 -193
- package/docs/fr/06-for-backenders.md +0 -438
- package/docs/fr/07-llm-pitfalls.md +0 -625
- package/docs/fr/08-llm-zero-history-test.md +0 -38
- package/docs/fr/09-shadow-vs-light-dom.md +0 -65
- package/docs/fr/10-app-architecture.md +0 -138
- package/docs/fr/11-layouts.md +0 -47
- package/docs/fr/12-auth-and-api.md +0 -76
- package/docs/fr/13-deployment.md +0 -57
- package/docs/fr/14-testing.md +0 -41
- package/docs/fr/15-error-handling.md +0 -50
- package/docs/fr/16-bake-cookbook.md +0 -88
- package/docs/fr/17-shadow-dom-forms.md +0 -196
- package/docs/fr/18-api-freeze-map.md +0 -63
- package/docs/fr/19-reactivity-ordering.md +0 -97
- package/docs/fr/20-v1-stability.md +0 -88
- package/docs/fr/README.md +0 -27
- package/docs/ru/00-the-mado-way.md +0 -84
- package/docs/ru/01-routing.md +0 -119
- package/docs/ru/02-project-layout.md +0 -84
- package/docs/ru/03-static-bake.md +0 -250
- package/docs/ru/04-ide-setup.md +0 -144
- package/docs/ru/05-why-mado.md +0 -193
- package/docs/ru/06-for-backenders.md +0 -428
- package/docs/ru/07-llm-pitfalls.md +0 -624
- package/docs/ru/08-llm-zero-history-test.md +0 -57
- package/docs/ru/09-shadow-vs-light-dom.md +0 -63
- package/docs/ru/10-app-architecture.md +0 -152
- package/docs/ru/11-layouts.md +0 -47
- package/docs/ru/12-auth-and-api.md +0 -75
- package/docs/ru/13-deployment.md +0 -66
- package/docs/ru/14-testing.md +0 -50
- package/docs/ru/15-error-handling.md +0 -56
- package/docs/ru/16-bake-cookbook.md +0 -95
- package/docs/ru/17-shadow-dom-forms.md +0 -193
- package/docs/ru/18-api-freeze-map.md +0 -64
- package/docs/ru/19-reactivity-ordering.md +0 -95
- package/docs/ru/20-v1-stability.md +0 -82
- package/docs/ru/README.md +0 -25
- package/docs/uk/00-the-mado-way.md +0 -82
- package/docs/uk/01-routing.md +0 -76
- package/docs/uk/02-project-layout.md +0 -73
- package/docs/uk/03-static-bake.md +0 -48
- package/docs/uk/04-ide-setup.md +0 -26
- package/docs/uk/05-why-mado.md +0 -34
- package/docs/uk/06-for-backenders.md +0 -55
- package/docs/uk/07-llm-pitfalls.md +0 -145
- package/docs/uk/08-llm-zero-history-test.md +0 -34
- package/docs/uk/09-shadow-vs-light-dom.md +0 -58
- package/docs/uk/10-app-architecture.md +0 -97
- package/docs/uk/11-layouts.md +0 -47
- package/docs/uk/12-auth-and-api.md +0 -70
- package/docs/uk/13-deployment.md +0 -40
- package/docs/uk/14-testing.md +0 -34
- package/docs/uk/15-error-handling.md +0 -32
- package/docs/uk/16-bake-cookbook.md +0 -36
- package/docs/uk/17-shadow-dom-forms.md +0 -193
- package/docs/uk/18-api-freeze-map.md +0 -61
- package/docs/uk/19-reactivity-ordering.md +0 -95
- package/docs/uk/20-v1-stability.md +0 -83
- package/docs/uk/README.md +0 -27
- package/starters/default/src/modules/home/not-found.page.ts +0 -11
- /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
- /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
- /package/starters/{default → modular}/.editorconfig +0 -0
- /package/starters/{default → modular}/eslint.config.mjs +0 -0
- /package/starters/{default → modular}/public/favicon.svg +0 -0
- /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
- /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
- /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Templates and signals
|
|
2
|
+
|
|
3
|
+
> Reactivity in Mado is one primitive — `signal()` — composed into
|
|
4
|
+
> three reading patterns. Templates are tagged `html\`\`` literals
|
|
5
|
+
> the browser understands directly.
|
|
6
|
+
|
|
7
|
+
By the end of this page you should be able to read and write any
|
|
8
|
+
Mado view without surprises.
|
|
9
|
+
|
|
10
|
+
## Signals
|
|
11
|
+
|
|
12
|
+
A signal is a getter function with a `.set()` method.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { signal, computed, effect } from "@madojs/mado";
|
|
16
|
+
|
|
17
|
+
const count = signal(0);
|
|
18
|
+
|
|
19
|
+
count(); // 0 — read
|
|
20
|
+
count.set(5); // — write
|
|
21
|
+
count.update((n) => n + 1); // 6 — derived write
|
|
22
|
+
count.peek(); // 6 — read without subscribing
|
|
23
|
+
|
|
24
|
+
const doubled = computed(() => count() * 2);
|
|
25
|
+
doubled(); // 12
|
|
26
|
+
|
|
27
|
+
effect(() => console.log("count =", count()));
|
|
28
|
+
// → "count = 6"
|
|
29
|
+
count.set(7);
|
|
30
|
+
// → "count = 7"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Rules of thumb:
|
|
34
|
+
|
|
35
|
+
- A signal is a **function**: `count()`, not `count.value`.
|
|
36
|
+
- `effect()` and `computed()` track every signal read **during their
|
|
37
|
+
callback**. There is no dependency array.
|
|
38
|
+
- `computed()` is lazy and de-duplicates: if its result is `Object.is`-
|
|
39
|
+
equal to the previous one, subscribers do not re-run.
|
|
40
|
+
- `untracked(() => ...)` reads without subscribing. Use it inside
|
|
41
|
+
async callbacks created from a synchronous `view()` so the router's
|
|
42
|
+
render effect does not accidentally subscribe.
|
|
43
|
+
- `batch(() => { a.set(...); b.set(...); })` flushes once after the
|
|
44
|
+
callback returns. `flushSync()` flushes pending effects immediately.
|
|
45
|
+
|
|
46
|
+
`effect()` cleanups run before the next run and on disposal. Inside a
|
|
47
|
+
component or a page they happen automatically when the host leaves
|
|
48
|
+
the DOM; outside, register the disposer through
|
|
49
|
+
`getCurrentLifecycle()` or pass an explicit one to a
|
|
50
|
+
`createLifecycle()` you own.
|
|
51
|
+
|
|
52
|
+
## Templates
|
|
53
|
+
|
|
54
|
+
`html\`\`` parses once per template literal into a static fragment +
|
|
55
|
+
binding indices, then patches only the changing slots on each
|
|
56
|
+
re-render.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { html } from "@madojs/mado";
|
|
60
|
+
|
|
61
|
+
html`<h1>Hello, ${name}</h1>`;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Five binding shapes — and only these:
|
|
65
|
+
|
|
66
|
+
| Shape | Meaning |
|
|
67
|
+
| -------------------- | ----------------------------------------------------------------------- |
|
|
68
|
+
| `${value}` | Child content: text, nodes, arrays, nested `html`, `each()` results. |
|
|
69
|
+
| `attr=${v}` | HTML attribute. String / number / falsy (`false`/`null`/`undefined` → remove). |
|
|
70
|
+
| `.prop=${v}` | DOM property. Use for `.value` of inputs, arrays, objects, numbers. |
|
|
71
|
+
| `?attr=${flag}` | Boolean attribute. `true` → present, `false` → absent (`?disabled`). |
|
|
72
|
+
| `@event=${fn}` | Event listener. Removed automatically on re-render. |
|
|
73
|
+
|
|
74
|
+
### The single most common mistake
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const count = signal(0);
|
|
78
|
+
|
|
79
|
+
// ❌ NOT REACTIVE — count() is read once at render
|
|
80
|
+
html`<div>${count() * 2}</div>`
|
|
81
|
+
|
|
82
|
+
// ✅ REACTIVE — the function is re-invoked on signal change
|
|
83
|
+
html`<div>${() => count() * 2}</div>`
|
|
84
|
+
|
|
85
|
+
// ✅ ALSO OK — the signal itself IS a function
|
|
86
|
+
html`<div>${count}</div>`
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Rule of thumb: if you wrote `signal()` (with parens) inside `${...}`,
|
|
90
|
+
wrap the whole expression in `() => ...`.
|
|
91
|
+
|
|
92
|
+
### Lists — `each(items, keyFn, renderFn)`
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { each } from "@madojs/mado";
|
|
96
|
+
|
|
97
|
+
html`<ul>
|
|
98
|
+
${() =>
|
|
99
|
+
each(
|
|
100
|
+
users(),
|
|
101
|
+
(u) => u.id,
|
|
102
|
+
(u) => html`<li>${u.name}</li>`,
|
|
103
|
+
)}
|
|
104
|
+
</ul>`;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`each()` is keyed: rows keep DOM identity across re-orders, so input
|
|
108
|
+
focus, scroll position and component state survive. A plain `.map()`
|
|
109
|
+
works but is not keyed; avoid it for anything but throwaway lists.
|
|
110
|
+
|
|
111
|
+
### Directives
|
|
112
|
+
|
|
113
|
+
Inline helpers you import alongside `html`:
|
|
114
|
+
|
|
115
|
+
- `unsafeHTML(string)` — interpolate trusted HTML.
|
|
116
|
+
- `ref((el) => …)` — get a callback when the element mounts.
|
|
117
|
+
- `classMap({ active: isActive(), error: hasError() })` — toggle
|
|
118
|
+
class names declaratively.
|
|
119
|
+
- `styleMap({ color: theme().fg, "--bg": theme().bg })` — set
|
|
120
|
+
inline styles, including custom properties.
|
|
121
|
+
|
|
122
|
+
### Parser hard errors
|
|
123
|
+
|
|
124
|
+
Mado refuses templates it cannot represent safely. Two cases you
|
|
125
|
+
will hit:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// ❌ slots inside RAW_TEXT elements
|
|
129
|
+
html`<textarea>${draft}</textarea>`;
|
|
130
|
+
html`<title>${title}</title>`;
|
|
131
|
+
|
|
132
|
+
// ✅ use properties / page APIs
|
|
133
|
+
html`<textarea .value=${draft}></textarea>`;
|
|
134
|
+
page({ title: ({ id }) => `User ${id}`, view: () => html`<main></main>` });
|
|
135
|
+
|
|
136
|
+
// ❌ nested SVG-only templates (namespace context is lost)
|
|
137
|
+
html`<svg>${html`<circle r="5"></circle>`}</svg>`;
|
|
138
|
+
|
|
139
|
+
// ✅ keep SVG internals in one template
|
|
140
|
+
html`<svg viewBox="0 0 10 10"><circle r="5"></circle></svg>`;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
No dynamic `${...}` child slots inside `<script>`, `<style>`,
|
|
144
|
+
`<textarea>` or `<title>`.
|
|
145
|
+
|
|
146
|
+
## How the two compose
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { html, page, signal } from "@madojs/mado";
|
|
150
|
+
|
|
151
|
+
export default page({
|
|
152
|
+
title: "Counter",
|
|
153
|
+
view: () => {
|
|
154
|
+
// 1. local state
|
|
155
|
+
const n = signal(0);
|
|
156
|
+
|
|
157
|
+
// 2. event handlers
|
|
158
|
+
const inc = () => n.update((x) => x + 1);
|
|
159
|
+
|
|
160
|
+
// 3. view — values that change are wrapped in arrow functions
|
|
161
|
+
return html`
|
|
162
|
+
<main>
|
|
163
|
+
<h1>Counter</h1>
|
|
164
|
+
<button @click=${inc} ?disabled=${() => n() >= 10}>${n}</button>
|
|
165
|
+
</main>
|
|
166
|
+
`;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
A page's `view()` runs **once** when the route commits; the returned
|
|
172
|
+
template stays mounted until the route changes. Every reactive slot
|
|
173
|
+
re-evaluates independently — Mado never re-runs the whole view.
|
|
174
|
+
|
|
175
|
+
## Lifecycle reads
|
|
176
|
+
|
|
177
|
+
Inside a `component(setup, …)` you get the lifecycle through `ctx`:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
component("x-timer", (ctx) => {
|
|
181
|
+
const tick = signal(0);
|
|
182
|
+
const id = setInterval(() => tick.update((n) => n + 1), 1000);
|
|
183
|
+
ctx.onDispose(() => clearInterval(id));
|
|
184
|
+
return () => html`<span>${tick}</span>`;
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Inside a `page({ view })` use the same `onDispose` from the view
|
|
189
|
+
context:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
export default page({
|
|
193
|
+
view: ({ onDispose }) => {
|
|
194
|
+
const handler = () => { /* … */ };
|
|
195
|
+
window.addEventListener("resize", handler);
|
|
196
|
+
onDispose?.(() => window.removeEventListener("resize", handler));
|
|
197
|
+
return html`<main>…</main>`;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`resource()`, `effect()` and `mutation()` subscribe to the active
|
|
203
|
+
lifecycle automatically — you do not write cleanup for them.
|
|
204
|
+
|
|
205
|
+
## Further reading
|
|
206
|
+
|
|
207
|
+
- [10-pages-and-components.md](./10-pages-and-components.md) — the
|
|
208
|
+
page vs component decision.
|
|
209
|
+
- [13-data.md](./13-data.md) — `resource()` and `mutation()`.
|
|
210
|
+
- [31-reactivity-ordering.md](./31-reactivity-ordering.md) — signal
|
|
211
|
+
scheduling, equality and teardown guarantees.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
> One app map. No folder scanners. No special path syntax. Layouts
|
|
4
|
+
> are pages that render `${child}`.
|
|
5
|
+
|
|
6
|
+
Mado does not infer routes from files. Route composition lives in
|
|
7
|
+
one file — `src/app.routes.ts` — so the whole map is readable at a
|
|
8
|
+
glance.
|
|
9
|
+
|
|
10
|
+
## The app manifest
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// src/app.routes.ts
|
|
14
|
+
import { layout, routes } from "@madojs/mado";
|
|
15
|
+
import { requireAuth } from "./modules/auth/auth.public";
|
|
16
|
+
import { authRoutes } from "./modules/auth/auth.routes";
|
|
17
|
+
import { billingRoutes } from "./modules/billing/billing.routes";
|
|
18
|
+
|
|
19
|
+
export const manifest = {
|
|
20
|
+
"/": () => import("./modules/home/home.page"),
|
|
21
|
+
"/login": layout({
|
|
22
|
+
layout: () => import("./layouts/auth-shell.layout"),
|
|
23
|
+
routes: authRoutes,
|
|
24
|
+
}),
|
|
25
|
+
"/billing": layout({
|
|
26
|
+
layout: () => import("./layouts/app-shell.layout"),
|
|
27
|
+
guard: requireAuth,
|
|
28
|
+
routes: billingRoutes,
|
|
29
|
+
}),
|
|
30
|
+
"*": () => import("./modules/home/not-found.page"),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default routes(manifest);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Opening `app.routes.ts` shows the whole app: public pages, auth
|
|
37
|
+
zone, protected zones, guards and shells.
|
|
38
|
+
|
|
39
|
+
Exporting `manifest` is **required**: `mado static` discovers
|
|
40
|
+
static routes by reading it.
|
|
41
|
+
|
|
42
|
+
## Module routes
|
|
43
|
+
|
|
44
|
+
Modules export plain route maps. They do not decide which shell
|
|
45
|
+
wraps them; that happens in `app.routes.ts`.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// src/modules/billing/billing.routes.ts
|
|
49
|
+
export const billingRoutes = {
|
|
50
|
+
"/invoices": () => import("./pages/invoices-list.page"),
|
|
51
|
+
"/invoices/:id": () => import("./pages/invoice-detail.page"),
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
When mounted under `"/billing"` the public URLs become
|
|
56
|
+
`/billing/invoices` and `/billing/invoices/:id`.
|
|
57
|
+
|
|
58
|
+
## Layouts (the one blessed path)
|
|
59
|
+
|
|
60
|
+
A layout is a `page({ view })` that renders `${ctx.child}` somewhere:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// src/layouts/app-shell.layout.ts
|
|
64
|
+
import { html, page } from "@madojs/mado";
|
|
65
|
+
|
|
66
|
+
export default page({
|
|
67
|
+
view: ({ child }) => html`
|
|
68
|
+
<header class="app-header">…</header>
|
|
69
|
+
<main class="app-main">${child}</main>
|
|
70
|
+
`,
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Mount it through `layout({ layout, routes, guard? })` in
|
|
75
|
+
`app.routes.ts`. That is the **only** canonical place to declare a
|
|
76
|
+
layout.
|
|
77
|
+
|
|
78
|
+
Two corollaries that prevent the classic "navigation appears below
|
|
79
|
+
content" bug:
|
|
80
|
+
|
|
81
|
+
- **Order matters.** Outer manifest entries wrap inner ones.
|
|
82
|
+
- **One shell per group.** If a subtree needs a different shell,
|
|
83
|
+
open a new `layout({...})` group; do not branch inside one shell.
|
|
84
|
+
|
|
85
|
+
Two acceptable alternatives exist but are escape hatches, not the
|
|
86
|
+
default:
|
|
87
|
+
|
|
88
|
+
- *Single shell in `main.ts`* — `render(html\`<x-shell>${routes.view}</x-shell>\`)`.
|
|
89
|
+
Caveat: every route lives inside one shell; no centred login page,
|
|
90
|
+
no marketing landing without admin chrome.
|
|
91
|
+
- *Per-page wrapping inside `view`* — each page repeats the shell.
|
|
92
|
+
Caveat: the first time someone forgets it the layout disappears.
|
|
93
|
+
Do not start with this.
|
|
94
|
+
|
|
95
|
+
Layouts are stateless wrappers. Per-page state belongs in the page
|
|
96
|
+
or in a `resource()` keyed by the page's identity — never in layout
|
|
97
|
+
view locals.
|
|
98
|
+
|
|
99
|
+
## What goes on the right side
|
|
100
|
+
|
|
101
|
+
### Lazy page import (preferred)
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
"/users/:id": () => import("./modules/users/pages/user-profile.page"),
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Vite emits a separate chunk per dynamic import in production.
|
|
108
|
+
|
|
109
|
+
### Eager page
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import home from "./modules/home/home.page";
|
|
113
|
+
|
|
114
|
+
export const manifest = { "/": home };
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use only for tiny critical pages.
|
|
118
|
+
|
|
119
|
+
### Layout group
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
"/admin": layout({
|
|
123
|
+
layout: () => import("./layouts/app-shell.layout"),
|
|
124
|
+
guard: requireAuth,
|
|
125
|
+
routes: adminRoutes,
|
|
126
|
+
}),
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Page contract
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { html, page } from "@madojs/mado";
|
|
133
|
+
|
|
134
|
+
export default page<{ id: string }>({
|
|
135
|
+
title: ({ id }) => `User ${id}`,
|
|
136
|
+
view: ({ params }) => html`<h1>${params.id}</h1>`,
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If a lazy route does not default-export `page({...})`, `routes()`
|
|
141
|
+
throws a clear error at load time.
|
|
142
|
+
|
|
143
|
+
## Guards
|
|
144
|
+
|
|
145
|
+
A guard is a function returning `true | false | string | { redirect, replace? } | { halt }`:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
export function requireAuth(): boolean | string {
|
|
149
|
+
return isAuthed() ? true : "/login";
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Guards apply outer → inner: layout-group guards run before page
|
|
154
|
+
guards. Static routes refuse all guards (they are public by
|
|
155
|
+
definition).
|
|
156
|
+
|
|
157
|
+
## Internal links — `routeUrl()` + `data-link`
|
|
158
|
+
|
|
159
|
+
Vite's `base` flows into `appBase` and `routeUrl()` automatically.
|
|
160
|
+
Internal links MUST use both:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { html, routeUrl } from "@madojs/mado";
|
|
164
|
+
|
|
165
|
+
html`<a data-link href=${routeUrl("/users/42")}>User 42</a>`;
|
|
166
|
+
html`<a data-link href=${routeUrl("/")}>Home</a>`; // → "/mado/" under base
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- `routeUrl(path)` returns a base-prefixed URL (preserves query and
|
|
170
|
+
hash).
|
|
171
|
+
- `data-link` opts the anchor into SPA navigation. A bare
|
|
172
|
+
`<a href>` performs a full document load — intentional for
|
|
173
|
+
foreign URLs and downloads.
|
|
174
|
+
|
|
175
|
+
The router intercepts links inside open Shadow DOM (it uses
|
|
176
|
+
`event.composedPath()`).
|
|
177
|
+
|
|
178
|
+
## Programmatic navigation
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import appRoutes from "./app.routes";
|
|
182
|
+
import { navigate } from "@madojs/mado";
|
|
183
|
+
|
|
184
|
+
appRoutes.navigate("/billing/invoices");
|
|
185
|
+
appRoutes.navigate("/billing/invoices?page=2");
|
|
186
|
+
appRoutes.navigate("/login", { replace: true });
|
|
187
|
+
|
|
188
|
+
// Standalone helper — dispatches popstate, every active router updates.
|
|
189
|
+
navigate("/users/42");
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Both accept route paths (no base prefix). The active base is
|
|
193
|
+
re-applied internally before `history.pushState`.
|
|
194
|
+
|
|
195
|
+
## Query parameters
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { queryParam } from "@madojs/mado";
|
|
199
|
+
|
|
200
|
+
const page = queryParam("page", "1");
|
|
201
|
+
page(); // current value (reactive)
|
|
202
|
+
page.set("2"); // history.replaceState
|
|
203
|
+
page.set("3", { push: true }); // history.pushState
|
|
204
|
+
page.set(null); // delete the parameter
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`queryParam()` returns a `Signal<string>`. Reading inside a
|
|
208
|
+
template subscribes; updating triggers a re-render.
|
|
209
|
+
|
|
210
|
+
## Prefetch
|
|
211
|
+
|
|
212
|
+
Hover-prefetch is on by default for `<a data-link>`. Programmatic
|
|
213
|
+
prefetch:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { prefetchPath } from "@madojs/mado";
|
|
217
|
+
prefetchPath("/billing/invoices/123");
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Foreign-base links (outside the active prefix) are ignored — the
|
|
221
|
+
manifest only knows base-free route paths.
|
|
222
|
+
|
|
223
|
+
## What is intentionally absent
|
|
224
|
+
|
|
225
|
+
- No auto-scan of page folders.
|
|
226
|
+
- No filesystem route syntax (`[id]`, `(group)`, `_layout`).
|
|
227
|
+
- No server routes in the client manifest.
|
|
228
|
+
- No hidden layout discovery — app zones are explicit in
|
|
229
|
+
`app.routes.ts`.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Data
|
|
2
|
+
|
|
3
|
+
> `resource()` reads, `mutation()` writes, `jsonFetcher()` shapes
|
|
4
|
+
> the wire. Backends, auth and modules all sit on top of this layer.
|
|
5
|
+
|
|
6
|
+
Mado does not ship a generic HTTP client because the browser already
|
|
7
|
+
has one. Instead it ships two small primitives that wrap `fetch()`
|
|
8
|
+
into a cache + lifecycle:
|
|
9
|
+
|
|
10
|
+
- `resource()` — keyed cache, loading / error / data signals, glob
|
|
11
|
+
invalidation, automatic cleanup.
|
|
12
|
+
- `mutation()` — async action runner with the same loading / error /
|
|
13
|
+
data signals, plus declarative invalidation of related resources.
|
|
14
|
+
|
|
15
|
+
A third helper, `jsonFetcher()`, is the default body parser: parses
|
|
16
|
+
JSON, throws `HttpError` on `!ok`.
|
|
17
|
+
|
|
18
|
+
## `resource()`
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { resource, jsonFetcher } from "@madojs/mado";
|
|
22
|
+
|
|
23
|
+
const userId = signal(1);
|
|
24
|
+
|
|
25
|
+
const user = resource(
|
|
26
|
+
() => `/api/users/${userId()}`, // key (reactive — recomputed on change)
|
|
27
|
+
jsonFetcher<User>(), // how to load
|
|
28
|
+
{ staleTime: 60_000 }, // cache age
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
user.data(); // User | undefined
|
|
32
|
+
user.error(); // Error | null
|
|
33
|
+
user.loading(); // boolean
|
|
34
|
+
|
|
35
|
+
await user.refresh();
|
|
36
|
+
user.mutate((u) => (u ? { ...u, name: "patched" } : u));
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Rules:
|
|
40
|
+
|
|
41
|
+
- The key is the cache identity. Same key → shared cache and shared
|
|
42
|
+
in-flight request, even across components.
|
|
43
|
+
- Distinct data needs distinct keys. Encode query params, auth scope
|
|
44
|
+
and tenant in the key string itself.
|
|
45
|
+
- `staleTime` is "how long after a successful fetch the cached value
|
|
46
|
+
is reused without a re-fetch". `0` means "always re-fetch on
|
|
47
|
+
remount".
|
|
48
|
+
- `resource()` inside a `component()` / `page()` cleans itself up
|
|
49
|
+
when the host leaves the DOM.
|
|
50
|
+
|
|
51
|
+
## `mutation()`
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { mutation } from "@madojs/mado";
|
|
55
|
+
|
|
56
|
+
const save = mutation<User, User>(
|
|
57
|
+
(u) =>
|
|
58
|
+
fetch("/api/users", {
|
|
59
|
+
method: "POST",
|
|
60
|
+
body: JSON.stringify(u),
|
|
61
|
+
}).then((r) => r.json()),
|
|
62
|
+
{ invalidates: ["/api/users*"] },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
await save.run(newUser); // throws on failure
|
|
66
|
+
save.loading(); // true while ANY run is in flight
|
|
67
|
+
save.error(); // last failure
|
|
68
|
+
save.data(); // last success
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `run()` is concurrent by default. `loading()` stays true while any
|
|
72
|
+
run is in flight. Use `{ abortPrevious: true }` for
|
|
73
|
+
search-as-you-type ("latest request wins").
|
|
74
|
+
- `invalidates` runs after a successful mutation and is best-effort:
|
|
75
|
+
invalidation failures are logged but do not turn the mutation
|
|
76
|
+
itself into a failure.
|
|
77
|
+
- `invalidates` may also be a function of `(result, args)` for
|
|
78
|
+
dynamic keys.
|
|
79
|
+
|
|
80
|
+
## `jsonFetcher()` and `HttpError`
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { jsonFetcher, HttpError, resource } from "@madojs/mado";
|
|
84
|
+
|
|
85
|
+
const me = resource(() => "/api/auth/me", jsonFetcher<User>());
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await me.refresh();
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (err instanceof HttpError && err.status === 401) {
|
|
91
|
+
navigate("/login");
|
|
92
|
+
} else throw err;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`HttpError` carries `status`, `url` and the parsed body (JSON when
|
|
97
|
+
possible, raw text otherwise). Use it for typed handling in pages
|
|
98
|
+
and guards.
|
|
99
|
+
|
|
100
|
+
## Modular shape — connector → resource → page
|
|
101
|
+
|
|
102
|
+
The modular starter encodes one rule per layer; every business
|
|
103
|
+
module follows the same flow:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
connector → resource / mutation → page
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
src/shared/http/
|
|
111
|
+
http-client.ts # one fetch wrapper + query/body/error handling
|
|
112
|
+
http-error.ts # HttpError type
|
|
113
|
+
interceptors.ts # request/response hooks (auth header, retry, …)
|
|
114
|
+
|
|
115
|
+
src/modules/<name>/
|
|
116
|
+
<name>.connector.ts # external API contract, DTO -> domain
|
|
117
|
+
<name>.resource.ts # resource()/mutation() with cache keys
|
|
118
|
+
<name>.types.ts # domain types
|
|
119
|
+
pages/ # page()s — UI only
|
|
120
|
+
_contracts/ # backend DTO shapes, private to the connector
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Hard rules:
|
|
124
|
+
|
|
125
|
+
- Pages **never** import DTOs and **never** call `fetch()`.
|
|
126
|
+
- Connectors **never** import signals, resources, html, components,
|
|
127
|
+
pages or services.
|
|
128
|
+
- Public surface of a module is `<name>.public.ts`. Anything not
|
|
129
|
+
re-exported there is private.
|
|
130
|
+
|
|
131
|
+
The universal starter does not insist on this layering — for a small
|
|
132
|
+
app a single `src/lib/api.ts` is enough. Use the modular shape when
|
|
133
|
+
the app grows enough that "which file owns this fetch?" becomes a
|
|
134
|
+
real question.
|
|
135
|
+
|
|
136
|
+
## Auth — service + guard
|
|
137
|
+
|
|
138
|
+
Auth state is an ES-module singleton built from the same primitives:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// src/modules/auth/auth.service.ts
|
|
142
|
+
import { computed, signal } from "@madojs/mado";
|
|
143
|
+
import { authApi } from "./auth.connector";
|
|
144
|
+
import type { Credentials, User } from "./auth.types";
|
|
145
|
+
|
|
146
|
+
const _user = signal<User | null>(null);
|
|
147
|
+
const _token = signal<string | null>(null);
|
|
148
|
+
|
|
149
|
+
export const user = (): User | null => _user();
|
|
150
|
+
export const isAuthed = computed(() => _user() !== null);
|
|
151
|
+
|
|
152
|
+
export async function login(creds: Credentials): Promise<void> {
|
|
153
|
+
const res = await authApi.login(creds);
|
|
154
|
+
_token.set(res.token);
|
|
155
|
+
_user.set(res.user);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function logout(): Promise<void> {
|
|
159
|
+
try { await authApi.logout(); } catch { /* best effort */ }
|
|
160
|
+
_token.set(null);
|
|
161
|
+
_user.set(null);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Expose only what other modules need through `auth.public.ts`.
|
|
166
|
+
|
|
167
|
+
The guard is a plain function returning `true | false | string`:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// src/modules/auth/auth.guard.ts
|
|
171
|
+
import { isAuthed } from "./auth.service";
|
|
172
|
+
|
|
173
|
+
export function requireAuth(): boolean | string {
|
|
174
|
+
return isAuthed() ? true : "/login";
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Mount it in `src/app.routes.ts`:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
"/billing": layout({
|
|
182
|
+
layout: () => import("./layouts/app-shell.layout"),
|
|
183
|
+
guard: requireAuth,
|
|
184
|
+
routes: billingRoutes,
|
|
185
|
+
}),
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Full guard contract: [12-routing.md](./12-routing.md#guards).
|
|
189
|
+
|
|
190
|
+
## Dev proxy
|
|
191
|
+
|
|
192
|
+
Configure backend proxying in `vite.config.ts`:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { defineConfig } from "vite";
|
|
196
|
+
import { mado } from "@madojs/mado/vite";
|
|
197
|
+
|
|
198
|
+
export default defineConfig({
|
|
199
|
+
plugins: [mado()],
|
|
200
|
+
server: {
|
|
201
|
+
proxy: { "/api": "http://localhost:3000" },
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The modular starter ships a dev-only in-memory mock for `/api/auth/*`
|
|
207
|
+
and `/api/billing/*` (`MADO_MOCK_API=0` to disable). Remove it once
|
|
208
|
+
your real backend is wired.
|
|
209
|
+
|
|
210
|
+
## Rule of thumb
|
|
211
|
+
|
|
212
|
+
- `shared/http` knows HTTP.
|
|
213
|
+
- `*.connector.ts` knows one external system and one DTO shape.
|
|
214
|
+
- `*.resource.ts` knows cache keys and invalidation.
|
|
215
|
+
- `*.page.ts` knows UI.
|
|
216
|
+
- `*.public.ts` is the only cross-module surface.
|
|
217
|
+
|
|
218
|
+
## Further reading
|
|
219
|
+
|
|
220
|
+
- [12-routing.md](./12-routing.md) — guards, layouts, `routeUrl`.
|
|
221
|
+
- [14-forms.md](./14-forms.md) — `useForm()`, then `mutation().run()`.
|
|
222
|
+
- [21-error-handling.md](./21-error-handling.md) — route / data /
|
|
223
|
+
action error boundaries.
|
|
224
|
+
- [16-app-architecture.md](./16-app-architecture.md) — the modular
|
|
225
|
+
starter shape end-to-end.
|