@madojs/mado 0.5.0 → 0.6.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 +49 -1
- package/CHANGELOG.md +188 -0
- package/MADO_V1_PLAN.md +179 -0
- package/README.md +53 -14
- package/ROADMAP.md +36 -5
- package/TODO.md +72 -0
- package/dist/src/forms.d.ts +41 -7
- package/dist/src/forms.js +334 -59
- package/dist/src/forms.js.map +1 -1
- package/dist/src/html/bindings.d.ts +41 -0
- package/dist/src/html/bindings.js +163 -6
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/html.d.ts +2 -0
- package/dist/src/html.js +1 -0
- package/dist/src/html.js.map +1 -1
- package/dist/src/index.d.ts +6 -6
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/page.d.ts +56 -0
- package/dist/src/page.js +17 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/router/manifest.d.ts +16 -1
- package/dist/src/router/manifest.js +181 -38
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +7 -2
- package/dist/src/router/match.js +14 -4
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.d.ts +10 -0
- package/dist/src/router/navigation.js +73 -12
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/signal.d.ts +15 -1
- package/dist/src/signal.js +112 -16
- package/dist/src/signal.js.map +1 -1
- package/docs/en/02-project-layout.md +99 -40
- package/docs/en/05-why-mado.md +1 -1
- package/docs/en/06-for-backenders.md +1 -1
- package/docs/en/07-llm-pitfalls.md +1 -1
- package/docs/en/09-shadow-vs-light-dom.md +60 -0
- package/docs/en/10-app-architecture.md +141 -0
- package/docs/en/11-layouts.md +115 -0
- package/docs/en/12-auth-and-api.md +217 -0
- package/docs/en/13-deployment.md +192 -0
- package/docs/en/14-testing.md +82 -0
- package/docs/en/15-error-handling.md +100 -0
- package/docs/en/16-bake-cookbook.md +93 -0
- package/docs/en/README.md +7 -0
- package/docs/fr/05-why-mado.md +1 -1
- package/docs/fr/06-for-backenders.md +1 -1
- package/docs/fr/07-llm-pitfalls.md +1 -1
- package/docs/fr/09-shadow-vs-light-dom.md +63 -0
- package/docs/fr/10-app-architecture.md +61 -0
- package/docs/fr/11-layouts.md +35 -0
- package/docs/fr/12-auth-and-api.md +35 -0
- package/docs/fr/13-deployment.md +39 -0
- package/docs/fr/14-testing.md +41 -0
- package/docs/fr/15-error-handling.md +50 -0
- package/docs/fr/16-bake-cookbook.md +35 -0
- package/docs/fr/README.md +7 -0
- package/docs/ru/05-why-mado.md +2 -2
- package/docs/ru/06-for-backenders.md +1 -1
- package/docs/ru/09-shadow-vs-light-dom.md +60 -0
- package/docs/ru/10-app-architecture.md +100 -0
- package/docs/ru/11-layouts.md +47 -0
- package/docs/ru/12-auth-and-api.md +53 -0
- package/docs/ru/13-deployment.md +60 -0
- package/docs/ru/14-testing.md +50 -0
- package/docs/ru/15-error-handling.md +56 -0
- package/docs/ru/16-bake-cookbook.md +55 -0
- package/docs/ru/README.md +7 -0
- package/docs/uk/06-for-backenders.md +2 -2
- package/docs/uk/09-shadow-vs-light-dom.md +91 -24
- package/docs/uk/10-app-architecture.md +56 -0
- package/docs/uk/11-layouts.md +34 -0
- package/docs/uk/12-auth-and-api.md +34 -0
- package/docs/uk/13-deployment.md +39 -0
- package/docs/uk/14-testing.md +34 -0
- package/docs/uk/15-error-handling.md +32 -0
- package/docs/uk/16-bake-cookbook.md +36 -0
- package/docs/uk/README.md +7 -0
- package/llms.txt +24 -1
- package/package.json +3 -1
- package/scripts/_config.mjs +224 -0
- package/scripts/bake.mjs +217 -120
- package/scripts/bundle.mjs +110 -67
- package/scripts/cli.mjs +127 -16
- package/scripts/preview.mjs +22 -12
- package/server/serve.mjs +101 -11
- package/starters/admin/README.md +63 -0
- package/starters/admin/index.html +21 -0
- package/starters/admin/mado.config.json +22 -0
- package/starters/admin/package.json +22 -0
- package/starters/admin/public/favicon.svg +4 -0
- package/starters/admin/src/components/x-button.ts +55 -0
- package/starters/admin/src/components/x-input.ts +74 -0
- package/starters/admin/src/layouts/app.ts +101 -0
- package/starters/admin/src/layouts/auth.ts +41 -0
- package/starters/admin/src/lib/api.ts +133 -0
- package/starters/admin/src/lib/auth.ts +83 -0
- package/starters/admin/src/main.ts +15 -0
- package/starters/admin/src/pages/admin/dashboard.ts +48 -0
- package/starters/admin/src/pages/admin/order-detail.ts +78 -0
- package/starters/admin/src/pages/admin/orders.ts +117 -0
- package/starters/admin/src/pages/home.ts +25 -0
- package/starters/admin/src/pages/login.ts +70 -0
- package/starters/admin/src/pages/not-found.ts +12 -0
- package/starters/admin/src/routes.ts +40 -0
- package/starters/admin/src/styles/global.ts +86 -0
- package/starters/admin/tsconfig.json +15 -0
- package/starters/crud/README.md +14 -2
- package/starters/crud/mado.config.json +20 -0
- package/starters/crud/package.json +9 -4
- package/starters/crud/src/components/app-shell.ts +13 -8
- package/starters/crud/src/main.ts +1 -4
- package/starters/crud/src/pages/ticket-detail.ts +1 -0
- package/starters/crud/src/pages/ticket-new.ts +1 -0
- package/starters/crud/src/pages/tickets.ts +1 -0
- package/starters/crud/src/routes.ts +4 -2
- package/starters/minimal/README.md +4 -2
- package/starters/minimal/mado.config.json +20 -0
- package/starters/minimal/package.json +8 -3
- package/starters/minimal/src/components/app-counter.ts +1 -1
- package/starters/minimal/src/routes.ts +4 -2
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Mado projects are plain TypeScript and browser APIs, so tests should stay plain
|
|
4
|
+
too. The framework repository uses Node's built-in test runner plus `linkedom`
|
|
5
|
+
for DOM tests.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm run typecheck
|
|
11
|
+
npm run build
|
|
12
|
+
npm test
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Before publishing or merging a release branch, run all three.
|
|
16
|
+
|
|
17
|
+
## Unit tests with DOM
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import test from "node:test";
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
|
|
23
|
+
const { parseHTML } = await import("linkedom");
|
|
24
|
+
const { window } = parseHTML("<!doctype html><html><body></body></html>");
|
|
25
|
+
globalThis.window = window;
|
|
26
|
+
globalThis.document = window.document;
|
|
27
|
+
globalThis.Node = window.Node;
|
|
28
|
+
globalThis.HTMLElement = window.HTMLElement;
|
|
29
|
+
|
|
30
|
+
const { html, render } = await import("../dist/src/html.js");
|
|
31
|
+
|
|
32
|
+
test("renders a value", () => {
|
|
33
|
+
const root = document.createElement("div");
|
|
34
|
+
render(html`<p>${"hello"}</p>`, root);
|
|
35
|
+
assert.equal(root.querySelector("p").textContent, "hello");
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Build first, then import from `dist/`. This tests the same files the browser
|
|
40
|
+
will load.
|
|
41
|
+
|
|
42
|
+
## What to cover
|
|
43
|
+
|
|
44
|
+
Test behavior, not internal implementation details:
|
|
45
|
+
|
|
46
|
+
- signal/computed scheduling and cleanup;
|
|
47
|
+
- template binding edges: child values, attributes, events, directives;
|
|
48
|
+
- route guards, redirects, scroll/focus behavior, error boundaries;
|
|
49
|
+
- forms: validation, async validation races, field arrays;
|
|
50
|
+
- resources/mutations: cache keys, invalidation, lifecycle cleanup;
|
|
51
|
+
- CLI flows: `mado release`, `mado bake`, `mado preview`.
|
|
52
|
+
|
|
53
|
+
## Browser smoke tests
|
|
54
|
+
|
|
55
|
+
Use Playwright or another browser runner for flows that require real layout,
|
|
56
|
+
focus, navigation or custom-element lifecycle. Keep them small:
|
|
57
|
+
|
|
58
|
+
1. start the app;
|
|
59
|
+
2. visit one route;
|
|
60
|
+
3. click one link or submit one form;
|
|
61
|
+
4. assert the user-visible result.
|
|
62
|
+
|
|
63
|
+
Most regression tests should still be fast Node tests.
|
|
64
|
+
|
|
65
|
+
## Test data
|
|
66
|
+
|
|
67
|
+
Keep API data in local fixtures or tiny in-memory fake clients. Do not call real
|
|
68
|
+
services from framework tests. For app tests, make the API client injectable via
|
|
69
|
+
`createContext()` so pages can run against a fake client.
|
|
70
|
+
|
|
71
|
+
## Release checklist
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm run typecheck
|
|
75
|
+
npm run build
|
|
76
|
+
npm test
|
|
77
|
+
npm run bundle
|
|
78
|
+
npm run bake
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`mado release` runs the production path for an app. In the framework repository,
|
|
82
|
+
the lower-level commands remain useful when debugging a single stage.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Error handling
|
|
2
|
+
|
|
3
|
+
Mado has three practical error layers: route loading, data loading, and user
|
|
4
|
+
actions. Handle each layer where the user can recover.
|
|
5
|
+
|
|
6
|
+
## Route errors
|
|
7
|
+
|
|
8
|
+
Use a global `errorPage` in `routes()` for lazy import, `load()` and `view()`
|
|
9
|
+
failures.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
export default routes(manifest, {
|
|
13
|
+
errorPage: (err) => html`
|
|
14
|
+
<main>
|
|
15
|
+
<h1>Something went wrong</h1>
|
|
16
|
+
<pre>${err.message}</pre>
|
|
17
|
+
<a data-link href="/">Go home</a>
|
|
18
|
+
</main>
|
|
19
|
+
`,
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For a specific page, `page({ errorView })` wins over the global route boundary.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
export default page({
|
|
27
|
+
load: async () => api.get("/reports"),
|
|
28
|
+
errorView: (err) => html`<x-report-error .error=${err}></x-report-error>`,
|
|
29
|
+
view: ({ data }) => html`<x-report .data=${data}></x-report>`,
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Resource errors
|
|
34
|
+
|
|
35
|
+
`resource()` exposes `error()` and `loading()`. Render a retry path near the data.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const users = resource(() => "/api/users", jsonFetcher<User[]>());
|
|
39
|
+
|
|
40
|
+
html`
|
|
41
|
+
${() => users.error()
|
|
42
|
+
? html`<p role="alert">${users.error()!.message}</p>
|
|
43
|
+
<button @click=${users.refresh}>Retry</button>`
|
|
44
|
+
: null}
|
|
45
|
+
`;
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `HttpError` or your own API error type when the UI needs status codes.
|
|
49
|
+
|
|
50
|
+
## Form and mutation errors
|
|
51
|
+
|
|
52
|
+
Validation errors belong in `useForm()`. Server errors from writes belong near
|
|
53
|
+
the submit button.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const form = useForm(
|
|
57
|
+
{ email: { required: true, type: "email" } },
|
|
58
|
+
{ validateAsync: (values) => api.validateUser(values) },
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const save = mutation((values) => api.post("/users", values), {
|
|
62
|
+
invalidates: ["/api/users*"],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
html`
|
|
66
|
+
<form @submit=${form.onSubmit(async values => {
|
|
67
|
+
await save.run(values);
|
|
68
|
+
})}>
|
|
69
|
+
<button ?disabled=${() => form.validating() || form.submitting()}>
|
|
70
|
+
Save
|
|
71
|
+
</button>
|
|
72
|
+
${() => save.error() ? html`<p role="alert">${save.error()!.message}</p>` : null}
|
|
73
|
+
</form>
|
|
74
|
+
`;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Component cleanup
|
|
78
|
+
|
|
79
|
+
If you subscribe to external browser APIs, clean them with `ctx.onDispose()`.
|
|
80
|
+
Signals, effects and resources created inside setup are lifecycle-aware.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
component("x-online", (ctx) => {
|
|
84
|
+
const online = signal(navigator.onLine);
|
|
85
|
+
const onChange = () => online.set(navigator.onLine);
|
|
86
|
+
window.addEventListener("online", onChange);
|
|
87
|
+
window.addEventListener("offline", onChange);
|
|
88
|
+
ctx.onDispose(() => {
|
|
89
|
+
window.removeEventListener("online", onChange);
|
|
90
|
+
window.removeEventListener("offline", onChange);
|
|
91
|
+
});
|
|
92
|
+
return () => html`${() => online() ? "Online" : "Offline"}`;
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Logging rule
|
|
97
|
+
|
|
98
|
+
Log once at the boundary that owns recovery. Avoid logging the same failure in
|
|
99
|
+
the API client, resource, page and component. The user should get one visible
|
|
100
|
+
message and developers should get one useful console error.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Bake cookbook
|
|
2
|
+
|
|
3
|
+
`mado bake` renders selected routes into static HTML. It is for SEO and fast
|
|
4
|
+
first paint, not for server-side hydration.
|
|
5
|
+
|
|
6
|
+
## Minimal baked page
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
export default page({
|
|
10
|
+
head: () => ({ title: "Products", description: "Product catalog" }),
|
|
11
|
+
view: ({ data }) => html`
|
|
12
|
+
<main>
|
|
13
|
+
<h1>Products</h1>
|
|
14
|
+
${data.products.map((p) => html`<article><h2>${p.name}</h2></article>`)}
|
|
15
|
+
</main>
|
|
16
|
+
`,
|
|
17
|
+
bake: {
|
|
18
|
+
paths: () => [{}],
|
|
19
|
+
data: async () => ({ products: await api.products() }),
|
|
20
|
+
revalidate: 3600,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For baked pages, use plain arrays in `view()` when possible. Runtime-only
|
|
26
|
+
directives such as keyed `each()` are for the browser.
|
|
27
|
+
|
|
28
|
+
## Dynamic routes
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
export default page<{ slug: string }>({
|
|
32
|
+
head: ({ slug }, data) => ({ title: data.title, canonical: `/blog/${slug}` }),
|
|
33
|
+
view: ({ data }) => html`<article>${unsafeHTML(data.html)}</article>`,
|
|
34
|
+
bake: {
|
|
35
|
+
paths: async () => (await api.posts()).map((p) => ({ slug: p.slug })),
|
|
36
|
+
data: ({ slug }) => api.post(slug),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`unsafeHTML()` is allowed only for trusted or already-sanitized HTML.
|
|
42
|
+
|
|
43
|
+
## Route manifest
|
|
44
|
+
|
|
45
|
+
`mado bake` needs the source manifest:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
export const manifest = {
|
|
49
|
+
"/": () => import("./pages/home.js"),
|
|
50
|
+
"/blog/:slug": () => import("./pages/blog-post.js"),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default routes(manifest);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Output
|
|
57
|
+
|
|
58
|
+
By default app-mode writes baked pages under `out/baked/`. `mado release`
|
|
59
|
+
produces the final deploy artifact:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
mado release
|
|
63
|
+
tree out
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The deployable folder is `out/`, not `dist/`.
|
|
67
|
+
|
|
68
|
+
## Unsupported values
|
|
69
|
+
|
|
70
|
+
Bake intentionally fails loudly instead of writing `[object Object]`. If a baked
|
|
71
|
+
view throws an unsupported directive error:
|
|
72
|
+
|
|
73
|
+
- replace `each()` with `items.map(...)` in baked markup;
|
|
74
|
+
- keep interactive-only widgets behind client routes;
|
|
75
|
+
- make sure every value can be serialized to static HTML.
|
|
76
|
+
|
|
77
|
+
## Canonical links
|
|
78
|
+
|
|
79
|
+
Pass `--base-url` or set `bake.baseUrl` in `mado.config.json` so generated
|
|
80
|
+
canonical links and sitemap entries point to production.
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"bake": {
|
|
85
|
+
"baseUrl": "https://example.com"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## When not to bake
|
|
91
|
+
|
|
92
|
+
Do not bake pages whose content is user-specific, permission-dependent, or
|
|
93
|
+
changes every few seconds. Use a normal SPA route with `resource()` instead.
|
package/docs/en/README.md
CHANGED
|
@@ -14,3 +14,10 @@ English documentation set.
|
|
|
14
14
|
| LLM pitfalls | [07-llm-pitfalls.md](./07-llm-pitfalls.md) |
|
|
15
15
|
| LLM zero-history test | [08-llm-zero-history-test.md](./08-llm-zero-history-test.md) |
|
|
16
16
|
| Shadow DOM vs Light DOM | [09-shadow-vs-light-dom.md](./09-shadow-vs-light-dom.md) |
|
|
17
|
+
| App architecture | [10-app-architecture.md](./10-app-architecture.md) |
|
|
18
|
+
| Layouts | [11-layouts.md](./11-layouts.md) |
|
|
19
|
+
| Auth and API | [12-auth-and-api.md](./12-auth-and-api.md) |
|
|
20
|
+
| Deployment | [13-deployment.md](./13-deployment.md) |
|
|
21
|
+
| Testing | [14-testing.md](./14-testing.md) |
|
|
22
|
+
| Error handling | [15-error-handling.md](./15-error-handling.md) |
|
|
23
|
+
| Bake cookbook | [16-bake-cookbook.md](./16-bake-cookbook.md) |
|
package/docs/fr/05-why-mado.md
CHANGED
|
@@ -33,7 +33,7 @@ Si votre cas ne tombe pas dans le dernier point — Mado n'est probablement pas
|
|
|
33
33
|
| Réactivité | décorateurs `@property` + `requestUpdate` manuel | signals (`signal`/`computed`/`effect`) intégrés |
|
|
34
34
|
| Router | aucun, vous devez en trouver un (`@lit-labs/router`, etc.) | inclus : `routes()` + nested + prefetch + sync-cache |
|
|
35
35
|
| Chargement de données | aucun, vous devez l'assembler | `resource()` + `mutation()` + invalidation glob |
|
|
36
|
-
| Forms | aucun | `useForm()`
|
|
36
|
+
| Forms | aucun | `useForm()` avec contraintes proches du HTML |
|
|
37
37
|
| SEO / statique | complexe (`@lit-labs/ssr`) | `bake` (linkedom) + edge-prerender |
|
|
38
38
|
| Build | nécessite esbuild/rollup/webpack | `tsc` suffit |
|
|
39
39
|
| Style de code | classes + décorateurs | fonctions + tagged templates |
|
|
@@ -195,7 +195,7 @@ Mado ne fait que le coller avec les signals.
|
|
|
195
195
|
|
|
196
196
|
## Forms — comme `form.Validate()` côté backend
|
|
197
197
|
|
|
198
|
-
Mado utilise
|
|
198
|
+
Mado utilise une **validation par schéma proche des contraintes HTML natives**, plus le suivi d'état.
|
|
199
199
|
|
|
200
200
|
```ts
|
|
201
201
|
import { useForm } from "@madojs/mado";
|
|
@@ -256,7 +256,7 @@ Cela signifie :
|
|
|
256
256
|
// ❌ Pas une telle API
|
|
257
257
|
const f = useForm({ resolver: zodResolver(schema) });
|
|
258
258
|
|
|
259
|
-
// ✅ Correct : validation
|
|
259
|
+
// ✅ Correct : validation proche du HTML via le schéma useForm
|
|
260
260
|
const f = useForm({
|
|
261
261
|
email: { required: true, type: "email" },
|
|
262
262
|
age: { required: true, type: "number", min: 18 },
|
|
@@ -5,6 +5,19 @@ autonomes, mais ce n'est pas le bon défaut pour chaque composant dans une appli
|
|
|
5
5
|
|
|
6
6
|
## Règle générale
|
|
7
7
|
|
|
8
|
+
Dans Mado, un layout est aussi un composant. Si un fichier décrit une partie
|
|
9
|
+
visible et réutilisable de l'arbre UI — app shell, sidebar, modal, table,
|
|
10
|
+
section de page — préférez un Web Component déclaré avec `component()`.
|
|
11
|
+
|
|
12
|
+
Gardez les fonctions simples pour de petits helpers inline :
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
const money = (value: number) => html`<span>${formatMoney(value)}</span>`;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Ne faites pas d'app shell sous forme de fonction dans les exemples publics. Cela
|
|
19
|
+
fonctionne, mais cela cache le modèle du navigateur au lieu de l'enseigner.
|
|
20
|
+
|
|
8
21
|
Utilisez **Shadow DOM** pour les widgets feuilles :
|
|
9
22
|
|
|
10
23
|
- boutons, badges, cartes, métriques ;
|
|
@@ -22,6 +35,16 @@ les utilitaires CSS globaux :
|
|
|
22
35
|
tableau ;
|
|
23
36
|
- endroits où les enfants doivent simplement rester dans le DOM normal du document.
|
|
24
37
|
|
|
38
|
+
Utilisez **Shadow DOM** pour les layouts basés sur des slots :
|
|
39
|
+
|
|
40
|
+
- app shells qui rendent `<slot>` ;
|
|
41
|
+
- wrappers sidebar/contenu ;
|
|
42
|
+
- frames de layout réutilisables qui possèdent leur propre CSS grid/header/sidebar.
|
|
43
|
+
|
|
44
|
+
`<slot>` est une fonctionnalité Shadow DOM. Dans un composant `shadow: false`,
|
|
45
|
+
`<slot>` est juste un élément DOM normal et ne déplace pas les enfants à cet
|
|
46
|
+
endroit du layout.
|
|
47
|
+
|
|
25
48
|
## Le piège
|
|
26
49
|
|
|
27
50
|
Le CSS global ne franchit pas une frontière Shadow DOM.
|
|
@@ -89,6 +112,18 @@ component("x-toast-stack", setup);
|
|
|
89
112
|
Cela donne aux écrans d'admin backend un CSS prévisible tout en préservant l'encapsulation
|
|
90
113
|
pour les widgets réutilisables et les shells basés sur slot.
|
|
91
114
|
|
|
115
|
+
Le modèle d'import est volontairement natif au navigateur :
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import "./components/app-layout.js";
|
|
119
|
+
|
|
120
|
+
render(html`<x-app-layout>${router.view}</x-app-layout>`, app);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
L'import enregistre le custom element avec `customElements.define()`. Le template
|
|
124
|
+
crée un élément `<x-app-layout>`. Le navigateur relie les deux. Il n'y a pas de
|
|
125
|
+
valeur de composant à la React que l'on passe comme fonction.
|
|
126
|
+
|
|
92
127
|
Si un layout n'a pas besoin de projection slot et doit être entièrement stylé par du CSS
|
|
93
128
|
global, `shadow: false` peut rester un bon choix. S'il contient `<slot>`, gardez Shadow DOM
|
|
94
129
|
et mettez les styles du shell dans `styles: css\`\``.
|
|
@@ -107,6 +142,34 @@ component("x-card-link", () => () => html`
|
|
|
107
142
|
|
|
108
143
|
Le lien peut être en Shadow DOM ; la navigation reste SPA.
|
|
109
144
|
|
|
145
|
+
## Où importer les composants
|
|
146
|
+
|
|
147
|
+
Les custom elements sont globaux après leur enregistrement, mais cet
|
|
148
|
+
enregistrement reste un import JavaScript explicite.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
// main.ts : frame global de l'app
|
|
152
|
+
import "./components/app-shell.js";
|
|
153
|
+
|
|
154
|
+
// pages/tickets.ts : composant possédé par cette page
|
|
155
|
+
import "../components/ticket-list.js";
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Le navigateur ne télécharge **pas** `ticket-list.js` simplement parce qu'il voit
|
|
159
|
+
`<ticket-list>`. Le fichier doit d'abord être importé quelque part. Une fois
|
|
160
|
+
importé, il appelle `customElements.define(...)`, et le tag devient connu dans
|
|
161
|
+
le document courant.
|
|
162
|
+
|
|
163
|
+
N'importez pas tous les composants en masse dans `main.ts` "au cas où". Cela
|
|
164
|
+
fonctionne pour de petites démos, mais cache l'ownership et casse le chargement
|
|
165
|
+
paresseux des routes. Préférez :
|
|
166
|
+
|
|
167
|
+
- app shell/providers globaux dans `main.ts` ;
|
|
168
|
+
- composants utilisés par une seule page dans ce fichier page ;
|
|
169
|
+
- composants partagés d'une feature dans la page d'entrée de cette feature ;
|
|
170
|
+
- petits leaf components vraiment globaux dans `main.ts` seulement s'ils sont
|
|
171
|
+
utilisés partout.
|
|
172
|
+
|
|
110
173
|
## Leçon du Showcase
|
|
111
174
|
|
|
112
175
|
`examples/showcase` utilise cette séparation délibérément :
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Architecture d'application
|
|
2
|
+
|
|
3
|
+
La forme recommandée d'une app Mado en production est volontairement simple :
|
|
4
|
+
un manifeste de routes, un shell, un client API, un module auth, et des pages
|
|
5
|
+
qui importent leurs propres composants.
|
|
6
|
+
|
|
7
|
+
## Structure
|
|
8
|
+
|
|
9
|
+
```txt
|
|
10
|
+
src/
|
|
11
|
+
├── main.ts
|
|
12
|
+
├── routes.ts
|
|
13
|
+
├── layouts/
|
|
14
|
+
├── pages/
|
|
15
|
+
├── components/
|
|
16
|
+
├── lib/
|
|
17
|
+
└── styles/
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`lib/` contient la logique métier, `layouts/` enveloppe les groupes de routes,
|
|
21
|
+
`components/` contient les tags réutilisables, et `pages/` contient un fichier
|
|
22
|
+
par page.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { html, render } from "@madojs/mado";
|
|
26
|
+
import "./styles/global.js";
|
|
27
|
+
import routesApi from "./routes.js";
|
|
28
|
+
|
|
29
|
+
render(html`${routesApi.view}`, document.getElementById("app")!);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
N'importe pas tous les composants dans `main.ts`. Une page importe les
|
|
33
|
+
composants qu'elle rend.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { layout, routes } from "@madojs/mado";
|
|
37
|
+
import { requireAuth } from "./lib/auth.js";
|
|
38
|
+
|
|
39
|
+
export const manifest = {
|
|
40
|
+
"/": () => import("./pages/home.js"),
|
|
41
|
+
"/admin": layout({
|
|
42
|
+
layout: () => import("./layouts/app.js"),
|
|
43
|
+
guard: requireAuth,
|
|
44
|
+
routes: { "/": () => import("./pages/admin/dashboard.js") },
|
|
45
|
+
}),
|
|
46
|
+
"*": () => import("./pages/not-found.js"),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default routes(manifest);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Exporte `manifest` pour `mado bake`. Utilise `resource()` pour les lectures,
|
|
53
|
+
`mutation(..., { invalidates })` pour les écritures, et `useForm()` pour les
|
|
54
|
+
workflows utilisateur.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
mado dev
|
|
58
|
+
mado release
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Le dossier déployable est `out/`. `dist/` est interne.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Layouts
|
|
2
|
+
|
|
3
|
+
Le chemin recommandé pour les layouts Mado est un groupe de routes imbriqué
|
|
4
|
+
dans `routes.ts`.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { layout, routes } from "@madojs/mado";
|
|
8
|
+
import { requireAuth } from "./lib/auth.js";
|
|
9
|
+
|
|
10
|
+
export const manifest = {
|
|
11
|
+
"/": () => import("./pages/home.js"),
|
|
12
|
+
"/login": layout({
|
|
13
|
+
layout: () => import("./layouts/auth.js"),
|
|
14
|
+
routes: { "/": () => import("./pages/login.js") },
|
|
15
|
+
}),
|
|
16
|
+
"/admin": layout({
|
|
17
|
+
layout: () => import("./layouts/app.js"),
|
|
18
|
+
guard: requireAuth,
|
|
19
|
+
routes: { "/": () => import("./pages/admin/dashboard.js") },
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default routes(manifest);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Un layout est une `page({ view })` qui rend `child` :
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
export default page({
|
|
30
|
+
view: ({ child }) => html`<x-app-shell>${child}</x-app-shell>`,
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Règles : un shell par groupe, pas par page ; les layouts externes enveloppent
|
|
35
|
+
les layouts internes ; un guard sur le groupe protège tout le sous-arbre.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Auth et API
|
|
2
|
+
|
|
3
|
+
Le starter `admin` contient la recette recommandée :
|
|
4
|
+
|
|
5
|
+
- `src/lib/api.ts` — un seul client HTTP, `ApiError`, refresh après 401 ;
|
|
6
|
+
- `src/lib/auth.ts` — `accessToken`, `restoreSession()`, `login()`,
|
|
7
|
+
`logout()`, `requireAuth`.
|
|
8
|
+
|
|
9
|
+
Modèle :
|
|
10
|
+
|
|
11
|
+
- access token en mémoire via `signal`, pas dans `localStorage` ;
|
|
12
|
+
- refresh cookie HttpOnly pour restaurer la session ;
|
|
13
|
+
- toutes les requêtes passent par le client API ;
|
|
14
|
+
- les routes protégées utilisent un group guard.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
export const requireAuth: Guard = async ({ path }) => {
|
|
18
|
+
if (accessToken()) return;
|
|
19
|
+
if (await restoreSession()) return;
|
|
20
|
+
return { redirect: `/login?return=${encodeURIComponent(path)}`, replace: true };
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Dev proxy :
|
|
25
|
+
|
|
26
|
+
```jsonc
|
|
27
|
+
{
|
|
28
|
+
"dev": {
|
|
29
|
+
"proxy": { "/api": "http://localhost:3000" }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Si ton backend a une autre forme, modifie `api.ts` et `auth.ts`. Les pages ne
|
|
35
|
+
doivent pas connaître les détails d'authentification.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Déploiement
|
|
2
|
+
|
|
3
|
+
Une commande, un artefact :
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
mado release
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Résultat :
|
|
10
|
+
|
|
11
|
+
```txt
|
|
12
|
+
out/
|
|
13
|
+
├── index.html
|
|
14
|
+
├── assets/
|
|
15
|
+
├── baked/
|
|
16
|
+
├── _redirects
|
|
17
|
+
└── _headers
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Déploie `out/` sur nginx, Cloudflare Pages, Netlify, S3/CloudFront ou GitHub
|
|
21
|
+
Pages.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
mado release
|
|
25
|
+
mado preview
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`mado preview` sert `out/` comme un hébergeur statique : HTML baked d'abord,
|
|
29
|
+
fallback SPA ensuite.
|
|
30
|
+
|
|
31
|
+
Pour VPS + nginx :
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
mado release
|
|
35
|
+
rsync -avz --delete out/ user@server:/var/www/myapp/
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Le `nginx.conf` fourni gère cache immutable pour les bundles hashés, no-cache
|
|
39
|
+
pour HTML, et fallback SPA pour les deep links.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Tests
|
|
2
|
+
|
|
3
|
+
Mado utilise TypeScript et les APIs du navigateur. Le dépôt du framework teste
|
|
4
|
+
avec le test runner de Node et `linkedom`.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm run typecheck
|
|
8
|
+
npm run build
|
|
9
|
+
npm test
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Un test DOM minimal :
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
|
|
18
|
+
const { parseHTML } = await import("linkedom");
|
|
19
|
+
const { window } = parseHTML("<!doctype html><html><body></body></html>");
|
|
20
|
+
globalThis.window = window;
|
|
21
|
+
globalThis.document = window.document;
|
|
22
|
+
globalThis.Node = window.Node;
|
|
23
|
+
globalThis.HTMLElement = window.HTMLElement;
|
|
24
|
+
|
|
25
|
+
const { html, render } = await import("../dist/src/html.js");
|
|
26
|
+
|
|
27
|
+
test("renders", () => {
|
|
28
|
+
const root = document.createElement("div");
|
|
29
|
+
render(html`<p>${"hello"}</p>`, root);
|
|
30
|
+
assert.equal(root.querySelector("p").textContent, "hello");
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
À couvrir :
|
|
35
|
+
|
|
36
|
+
- signals/computed/effect : scheduling et nettoyage ;
|
|
37
|
+
- bindings HTML : children, attributs, événements, directives ;
|
|
38
|
+
- routes : guards, redirects, scroll/focus, error boundaries ;
|
|
39
|
+
- formulaires : validation sync/async, races, field arrays ;
|
|
40
|
+
- resources/mutations : clés de cache, invalidation, lifecycle ;
|
|
41
|
+
- CLI : `mado release`, `mado bake`, `mado preview`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Gestion des erreurs
|
|
2
|
+
|
|
3
|
+
Traite les erreurs au niveau où l'utilisateur peut récupérer : routes, données,
|
|
4
|
+
actions utilisateur.
|
|
5
|
+
|
|
6
|
+
## Routes
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
export default routes(manifest, {
|
|
10
|
+
errorPage: (err) => html`
|
|
11
|
+
<main>
|
|
12
|
+
<h1>Une erreur est survenue</h1>
|
|
13
|
+
<pre>${err.message}</pre>
|
|
14
|
+
<a data-link href="/">Accueil</a>
|
|
15
|
+
</main>
|
|
16
|
+
`,
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`page({ errorView })` a priorité sur cette boundary globale.
|
|
21
|
+
|
|
22
|
+
## Données
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const users = resource(() => "/api/users", jsonFetcher<User[]>());
|
|
26
|
+
|
|
27
|
+
html`
|
|
28
|
+
${() => users.error()
|
|
29
|
+
? html`<p role="alert">${users.error()!.message}</p>
|
|
30
|
+
<button @click=${users.refresh}>Réessayer</button>`
|
|
31
|
+
: null}
|
|
32
|
+
`;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Formulaires et mutations
|
|
36
|
+
|
|
37
|
+
La validation va dans `useForm()`. Les erreurs serveur d'écriture restent près
|
|
38
|
+
du bouton de soumission.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const form = useForm(
|
|
42
|
+
{ email: { required: true, type: "email" } },
|
|
43
|
+
{ validateAsync: (values) => api.validateUser(values) },
|
|
44
|
+
);
|
|
45
|
+
const save = mutation((values) => api.post("/users", values), {
|
|
46
|
+
invalidates: ["/api/users*"],
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Nettoie les subscriptions navigateur externes avec `ctx.onDispose()`.
|