@madojs/mado 0.5.1 → 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 +26 -0
- package/CHANGELOG.md +153 -0
- package/MADO_V1_PLAN.md +179 -0
- package/README.md +31 -13
- package/ROADMAP.md +28 -7
- package/TODO.md +72 -0
- package/dist/src/forms.d.ts +37 -4
- package/dist/src/forms.js +331 -57
- 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 +71 -3
- 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/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/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/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/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 +9 -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 +119 -15
- package/scripts/preview.mjs +22 -12
- package/server/serve.mjs +82 -4
- 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/mado.config.json +20 -0
- package/starters/crud/package.json +8 -4
- package/starters/crud/src/routes.ts +4 -2
- package/starters/minimal/mado.config.json +20 -0
- package/starters/minimal/package.json +7 -3
- package/starters/minimal/src/routes.ts +4 -2
package/dist/src/signal.js
CHANGED
|
@@ -23,7 +23,48 @@
|
|
|
23
23
|
*
|
|
24
24
|
* batch(() => { a.set(1); b.set(2); });
|
|
25
25
|
*/
|
|
26
|
+
const MAX_FLUSH_RUNS_PER_SUBSCRIBER = 100;
|
|
26
27
|
let activeTracker = null;
|
|
28
|
+
class SubscriberSet extends Set {
|
|
29
|
+
onEmpty;
|
|
30
|
+
emptyScheduled = false;
|
|
31
|
+
constructor(onEmpty) {
|
|
32
|
+
super();
|
|
33
|
+
this.onEmpty = onEmpty;
|
|
34
|
+
}
|
|
35
|
+
add(entry) {
|
|
36
|
+
super.add(entry);
|
|
37
|
+
this.emptyScheduled = false;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
delete(entry) {
|
|
41
|
+
const deleted = super.delete(entry);
|
|
42
|
+
if (deleted)
|
|
43
|
+
this.queueEmpty();
|
|
44
|
+
return deleted;
|
|
45
|
+
}
|
|
46
|
+
clear() {
|
|
47
|
+
const hadEntries = this.size > 0;
|
|
48
|
+
super.clear();
|
|
49
|
+
if (hadEntries)
|
|
50
|
+
this.queueEmpty();
|
|
51
|
+
}
|
|
52
|
+
queueEmpty() {
|
|
53
|
+
if (!this.onEmpty || this.size > 0 || this.emptyScheduled)
|
|
54
|
+
return;
|
|
55
|
+
this.emptyScheduled = true;
|
|
56
|
+
queueMicrotask(() => {
|
|
57
|
+
this.emptyScheduled = false;
|
|
58
|
+
if (this.size === 0)
|
|
59
|
+
this.onEmpty?.();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function cleanupTracker(tracker) {
|
|
64
|
+
for (const dep of tracker.deps)
|
|
65
|
+
dep.delete(tracker.entry);
|
|
66
|
+
tracker.deps.clear();
|
|
67
|
+
}
|
|
27
68
|
// ---------- Scheduler ----------
|
|
28
69
|
const pending = new Set();
|
|
29
70
|
let batchDepth = 0;
|
|
@@ -39,11 +80,20 @@ function schedule(sub) {
|
|
|
39
80
|
}
|
|
40
81
|
function flush() {
|
|
41
82
|
flushScheduled = false;
|
|
83
|
+
const runCounts = new Map();
|
|
42
84
|
// guard against Set modification during iteration
|
|
43
85
|
while (pending.size > 0) {
|
|
44
86
|
const subs = [...pending];
|
|
45
87
|
pending.clear();
|
|
46
88
|
for (const sub of subs) {
|
|
89
|
+
const runs = (runCounts.get(sub) ?? 0) + 1;
|
|
90
|
+
runCounts.set(sub, runs);
|
|
91
|
+
if (runs > MAX_FLUSH_RUNS_PER_SUBSCRIBER) {
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.error("[mado] effect cycle detected: subscriber re-ran more than " +
|
|
94
|
+
`${MAX_FLUSH_RUNS_PER_SUBSCRIBER} times in one flush.`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
47
97
|
try {
|
|
48
98
|
sub();
|
|
49
99
|
}
|
|
@@ -81,7 +131,7 @@ export function flushSync() {
|
|
|
81
131
|
}
|
|
82
132
|
export function signal(initial) {
|
|
83
133
|
let value = initial;
|
|
84
|
-
const subscribers = new
|
|
134
|
+
const subscribers = new SubscriberSet();
|
|
85
135
|
const read = (() => {
|
|
86
136
|
if (activeTracker) {
|
|
87
137
|
subscribers.add(activeTracker.entry);
|
|
@@ -115,6 +165,7 @@ export function signal(initial) {
|
|
|
115
165
|
};
|
|
116
166
|
read.update = (fn) => read.set(fn(value));
|
|
117
167
|
read.peek = () => value;
|
|
168
|
+
debugInfo.set(read, { subscribers });
|
|
118
169
|
return read;
|
|
119
170
|
}
|
|
120
171
|
/**
|
|
@@ -131,17 +182,55 @@ export function signal(initial) {
|
|
|
131
182
|
* instead of actually recomputing we mark ourselves dirty and
|
|
132
183
|
* propagate to subscribers.
|
|
133
184
|
*/
|
|
134
|
-
export function computed(fn) {
|
|
135
|
-
const subscribers = new Set();
|
|
185
|
+
export function computed(fn, options = {}) {
|
|
136
186
|
let value = undefined;
|
|
137
187
|
let dirty = true;
|
|
188
|
+
let hasValue = false;
|
|
189
|
+
let computing = false;
|
|
138
190
|
const onInvalidate = () => {
|
|
139
191
|
// dep changed → mark dirty synchronously and cascade.
|
|
140
192
|
// Sync subscribers (other computed) are triggered immediately — they also
|
|
141
193
|
// set dirty without delay. Async (effects) go through the scheduler.
|
|
142
194
|
if (dirty)
|
|
143
195
|
return;
|
|
196
|
+
if (subscribers.size === 0) {
|
|
197
|
+
dirty = true;
|
|
198
|
+
suspend();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (options.equals && hasValue) {
|
|
202
|
+
const prevValue = value;
|
|
203
|
+
recompute();
|
|
204
|
+
if (options.equals(prevValue, value))
|
|
205
|
+
return;
|
|
206
|
+
notifySubscribers();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
144
209
|
dirty = true;
|
|
210
|
+
notifySubscribers();
|
|
211
|
+
};
|
|
212
|
+
const tracker = {
|
|
213
|
+
deps: new Set(),
|
|
214
|
+
entry: { run: onInvalidate, sync: true },
|
|
215
|
+
};
|
|
216
|
+
const subscribers = new SubscriberSet(() => {
|
|
217
|
+
suspend();
|
|
218
|
+
});
|
|
219
|
+
const suspend = () => {
|
|
220
|
+
if (subscribers.size > 0)
|
|
221
|
+
return;
|
|
222
|
+
cleanupTracker(tracker);
|
|
223
|
+
dirty = true;
|
|
224
|
+
};
|
|
225
|
+
const queueSuspendIfUnobserved = () => {
|
|
226
|
+
if (subscribers.size > 0)
|
|
227
|
+
return;
|
|
228
|
+
queueMicrotask(() => {
|
|
229
|
+
if (subscribers.size === 0)
|
|
230
|
+
suspend();
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
const notifySubscribers = () => {
|
|
145
234
|
const snapshot = [...subscribers];
|
|
146
235
|
for (const e of snapshot) {
|
|
147
236
|
if (e.sync) {
|
|
@@ -158,23 +247,24 @@ export function computed(fn) {
|
|
|
158
247
|
}
|
|
159
248
|
}
|
|
160
249
|
};
|
|
161
|
-
const tracker = {
|
|
162
|
-
deps: new Set(),
|
|
163
|
-
entry: { run: onInvalidate, sync: true },
|
|
164
|
-
};
|
|
165
250
|
const recompute = () => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
251
|
+
if (computing) {
|
|
252
|
+
throw new Error("[mado] computed cycle detected");
|
|
253
|
+
}
|
|
254
|
+
cleanupTracker(tracker);
|
|
169
255
|
const prev = activeTracker;
|
|
170
256
|
activeTracker = tracker;
|
|
257
|
+
computing = true;
|
|
171
258
|
try {
|
|
172
259
|
value = fn();
|
|
173
260
|
}
|
|
174
261
|
finally {
|
|
262
|
+
computing = false;
|
|
175
263
|
activeTracker = prev;
|
|
176
264
|
}
|
|
177
265
|
dirty = false;
|
|
266
|
+
hasValue = true;
|
|
267
|
+
queueSuspendIfUnobserved();
|
|
178
268
|
};
|
|
179
269
|
const read = (() => {
|
|
180
270
|
if (activeTracker) {
|
|
@@ -190,14 +280,13 @@ export function computed(fn) {
|
|
|
190
280
|
recompute();
|
|
191
281
|
return value;
|
|
192
282
|
};
|
|
283
|
+
debugInfo.set(read, { subscribers, tracker });
|
|
193
284
|
return read;
|
|
194
285
|
}
|
|
195
286
|
export function effect(fn) {
|
|
196
287
|
let cleanup;
|
|
197
288
|
const run = () => {
|
|
198
|
-
|
|
199
|
-
dep.delete(tracker.entry);
|
|
200
|
-
tracker.deps.clear();
|
|
289
|
+
cleanupTracker(tracker);
|
|
201
290
|
if (typeof cleanup === "function")
|
|
202
291
|
cleanup();
|
|
203
292
|
const prev = activeTracker;
|
|
@@ -215,9 +304,7 @@ export function effect(fn) {
|
|
|
215
304
|
};
|
|
216
305
|
run();
|
|
217
306
|
return () => {
|
|
218
|
-
|
|
219
|
-
dep.delete(tracker.entry);
|
|
220
|
-
tracker.deps.clear();
|
|
307
|
+
cleanupTracker(tracker);
|
|
221
308
|
if (typeof cleanup === "function")
|
|
222
309
|
cleanup();
|
|
223
310
|
};
|
|
@@ -235,4 +322,13 @@ export function untracked(fn) {
|
|
|
235
322
|
activeTracker = prev;
|
|
236
323
|
}
|
|
237
324
|
}
|
|
325
|
+
const debugInfo = new WeakMap();
|
|
326
|
+
export const _testHooks = {
|
|
327
|
+
subscriberCount(source) {
|
|
328
|
+
return debugInfo.get(source)?.subscribers.size ?? 0;
|
|
329
|
+
},
|
|
330
|
+
dependencyCount(source) {
|
|
331
|
+
return debugInfo.get(source)?.tracker?.deps.size ?? 0;
|
|
332
|
+
},
|
|
333
|
+
};
|
|
238
334
|
//# sourceMappingURL=signal.js.map
|
package/dist/src/signal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/signal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;
|
|
1
|
+
{"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/signal.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,58 +1,117 @@
|
|
|
1
1
|
# Project layout
|
|
2
2
|
|
|
3
|
-
Every
|
|
3
|
+
Every Mado app uses the same shape. This is a **mandatory** convention — it
|
|
4
|
+
exists so that you, your teammates, and any LLM assistant always know where
|
|
5
|
+
things live.
|
|
4
6
|
|
|
5
7
|
```
|
|
6
8
|
my-app/
|
|
7
|
-
├── package.json # exactly
|
|
8
|
-
├── tsconfig.json #
|
|
9
|
-
├──
|
|
10
|
-
├── .
|
|
11
|
-
├──
|
|
12
|
-
├── scripts/
|
|
13
|
-
│ ├── bundle.mjs # esbuild prod bundle
|
|
14
|
-
│ └── new.mjs # page scaffolder
|
|
15
|
-
├── templates/ # templates for new.mjs
|
|
16
|
-
├── docs/ # project docs (can copy our guides)
|
|
17
|
-
├── public/ # static assets (favicon, manifests)
|
|
9
|
+
├── package.json # exactly one runtime dep: @madojs/mado
|
|
10
|
+
├── tsconfig.json # strict TS, ES2022, Bundler resolution
|
|
11
|
+
├── mado.config.json # single config file (dev/build/bake/bundle)
|
|
12
|
+
├── index.html # SPA shell (also the template for `mado bake`)
|
|
13
|
+
├── public/ # static assets (favicons, images, robots.txt)
|
|
18
14
|
└── src/
|
|
19
|
-
├── main.ts # entry:
|
|
20
|
-
├── routes.ts # route manifest
|
|
21
|
-
├──
|
|
22
|
-
├──
|
|
23
|
-
├──
|
|
15
|
+
├── main.ts # entry: mount router into #app
|
|
16
|
+
├── routes.ts # route manifest (default + named `manifest`)
|
|
17
|
+
├── layouts/ # `page({ child })` layouts for nested routes
|
|
18
|
+
├── pages/ # one page = one file
|
|
19
|
+
├── components/ # reusable x-* Web Components
|
|
24
20
|
└── lib/
|
|
25
|
-
├── api.ts #
|
|
26
|
-
├──
|
|
27
|
-
|
|
28
|
-
└── ... # utilities, types, business rules
|
|
21
|
+
├── api.ts # API client + error type
|
|
22
|
+
├── auth.ts # auth recipe (token + guard)
|
|
23
|
+
└── ... # contexts, helpers, business rules
|
|
29
24
|
```
|
|
30
25
|
|
|
26
|
+
## The three artifact states (read this once, never wonder again)
|
|
27
|
+
|
|
28
|
+
| Folder | What it is | Who writes | Who reads | Deploy? |
|
|
29
|
+
|-------------|----------------------------------------------------------------|-------------------|----------------------------|-------------------|
|
|
30
|
+
| `src/` | your source (TypeScript) | you | `tsc`, `esbuild` | ❌ no |
|
|
31
|
+
| `dist/` | `tsc` output — native ESM `.js` for the browser | `mado build` | `mado dev` (during dev) | ❌ no (internal) |
|
|
32
|
+
| `public/` | static assets you authored (favicon, images, robots.txt) | you | `mado release` copies it | ✅ via `out/` |
|
|
33
|
+
| `out/` | **the deploy artifact**: SPA shell + bundles + baked HTML | `mado release` | nginx / CDN / Cloudflare | ✅ **yes** |
|
|
34
|
+
|
|
35
|
+
One-liner to remember:
|
|
36
|
+
> Develop with `mado dev`. To ship: run `mado release`, then upload `out/`.
|
|
37
|
+
|
|
38
|
+
`mado release` = `typecheck` + `build` (tsc → `dist/`) + `bundle` (esbuild
|
|
39
|
+
→ `out/assets/`) + `bake` (HTML → `out/baked/`) + copy `public/*` → `out/`.
|
|
40
|
+
|
|
41
|
+
You almost never need to look inside `dist/`. It exists so the dev browser can
|
|
42
|
+
load native ESM modules without a bundler during development. In production
|
|
43
|
+
the equivalent code is bundled and hashed into `out/assets/`.
|
|
44
|
+
|
|
45
|
+
### Quick deployment matrix
|
|
46
|
+
|
|
47
|
+
| Target | Command | Where it goes |
|
|
48
|
+
|-----------------------|--------------------------------------|----------------------------|
|
|
49
|
+
| VPS + nginx | `mado release && rsync -avz out/ …` | `/var/www/<app>/` |
|
|
50
|
+
| Cloudflare Pages | `mado release && wrangler pages deploy out` | CF Pages |
|
|
51
|
+
| Netlify / S3 / GH Pages | `mado release && upload out/*` | any static host |
|
|
52
|
+
|
|
53
|
+
See `docs/en/13-deployment.md` for full recipes.
|
|
54
|
+
|
|
31
55
|
## Where to put a new file?
|
|
32
56
|
|
|
33
|
-
| What
|
|
34
|
-
|
|
35
|
-
| Page for a new URL
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
57
|
+
| What | Where |
|
|
58
|
+
|-------------------------------------|------------------------------------------------------|
|
|
59
|
+
| Page for a new URL | `src/pages/<name>.ts` + add to `src/routes.ts` |
|
|
60
|
+
| Layout for a group of routes | `src/layouts/<name>.ts` (referenced from `routes.ts`)|
|
|
61
|
+
| Reusable UI widget | `src/components/<x-name>.ts` |
|
|
62
|
+
| API call | `src/lib/api.ts` (add a method) |
|
|
63
|
+
| Auth/session | `src/lib/auth.ts` |
|
|
64
|
+
| Global context (theme, user, i18n) | `src/lib/<name>.ts` |
|
|
65
|
+
| Pure function with no UI | `src/lib/util/<name>.ts` |
|
|
66
|
+
| Static image / favicon | `public/<file>` |
|
|
40
67
|
|
|
41
|
-
If you don't know where — that is a signal that **the architecture is
|
|
42
|
-
Ask the team
|
|
68
|
+
If you don't know where — that is a signal that **the architecture is
|
|
69
|
+
suffering**. Ask the team and **record** the answer in `docs/`. Don't invent a
|
|
70
|
+
new top-level folder.
|
|
43
71
|
|
|
44
72
|
## Naming rules
|
|
45
73
|
|
|
46
|
-
| What
|
|
47
|
-
|
|
48
|
-
| File
|
|
49
|
-
| Component tag
|
|
50
|
-
| Context
|
|
51
|
-
| Signal
|
|
52
|
-
| Page
|
|
74
|
+
| What | Style | Example |
|
|
75
|
+
|-------------------------------------|----------------------|------------------------|
|
|
76
|
+
| File | kebab-case | `user-profile.ts` |
|
|
77
|
+
| Component tag | `x-` + kebab | `<x-user-profile>` |
|
|
78
|
+
| Context | PascalCase + `Ctx` | `ThemeCtx`, `AuthCtx` |
|
|
79
|
+
| Signal | camelCase | `userId`, `isLoggedIn` |
|
|
80
|
+
| Page-internal element | `x-<route>-page` | `<x-posts-page>` |
|
|
81
|
+
|
|
82
|
+
## `mado.config.json` in one screen
|
|
83
|
+
|
|
84
|
+
```jsonc
|
|
85
|
+
{
|
|
86
|
+
"dev": {
|
|
87
|
+
"port": 5173,
|
|
88
|
+
"proxy": { "/api": "http://localhost:3000" } // dev → backend
|
|
89
|
+
},
|
|
90
|
+
"build": {
|
|
91
|
+
"out": "out",
|
|
92
|
+
"dist": "dist",
|
|
93
|
+
"publicDir": "public"
|
|
94
|
+
},
|
|
95
|
+
"bake": {
|
|
96
|
+
"entry": "src/routes.ts",
|
|
97
|
+
"template": "index.html",
|
|
98
|
+
"baseUrl": "https://example.com"
|
|
99
|
+
},
|
|
100
|
+
"bundle": {
|
|
101
|
+
"splitting": true,
|
|
102
|
+
"compress": ["gz", "br"]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Precedence: built-in defaults < `mado.config.json` < CLI flags
|
|
108
|
+
(< legacy env vars). All keys are optional.
|
|
53
109
|
|
|
54
|
-
## What does NOT go in src
|
|
110
|
+
## What does NOT go in `src/`
|
|
55
111
|
|
|
56
112
|
- ❌ Build tool configs (webpack, rollup, vite) — we don't have any.
|
|
57
|
-
- ❌ `.env` files — env
|
|
58
|
-
|
|
113
|
+
- ❌ `.env` files — read env in `src/lib/config.ts` from `import.meta.env` /
|
|
114
|
+
`process.env` and import that one module everywhere.
|
|
115
|
+
- ❌ Tests mixed with code — put them in `test/`.
|
|
116
|
+
- ❌ `examples/` folder — the framework repository has examples, your app
|
|
117
|
+
does not need one.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# App architecture
|
|
2
|
+
|
|
3
|
+
This is the default shape for a production Mado app. It is intentionally boring:
|
|
4
|
+
one route manifest, one shell, one API client, one auth module, and page files
|
|
5
|
+
that own their feature components.
|
|
6
|
+
|
|
7
|
+
## File tree
|
|
8
|
+
|
|
9
|
+
```txt
|
|
10
|
+
src/
|
|
11
|
+
├── main.ts
|
|
12
|
+
├── routes.ts
|
|
13
|
+
├── layouts/
|
|
14
|
+
│ ├── app.ts
|
|
15
|
+
│ └── auth.ts
|
|
16
|
+
├── pages/
|
|
17
|
+
│ ├── home.ts
|
|
18
|
+
│ ├── login.ts
|
|
19
|
+
│ ├── not-found.ts
|
|
20
|
+
│ └── admin/
|
|
21
|
+
│ ├── dashboard.ts
|
|
22
|
+
│ ├── orders.ts
|
|
23
|
+
│ └── order-detail.ts
|
|
24
|
+
├── components/
|
|
25
|
+
│ ├── x-button.ts
|
|
26
|
+
│ └── x-input.ts
|
|
27
|
+
├── lib/
|
|
28
|
+
│ ├── api.ts
|
|
29
|
+
│ └── auth.ts
|
|
30
|
+
└── styles/
|
|
31
|
+
└── global.ts
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Keep business logic in `lib/`, route wrapping in `layouts/`, and UI leaves in
|
|
35
|
+
`components/`. A page should import the components it renders.
|
|
36
|
+
|
|
37
|
+
## Entry point
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// src/main.ts
|
|
41
|
+
import { html, render } from "@madojs/mado";
|
|
42
|
+
import "./styles/global.js";
|
|
43
|
+
import "./components/x-button.js";
|
|
44
|
+
import routesApi from "./routes.js";
|
|
45
|
+
|
|
46
|
+
render(html`${routesApi.view}`, document.getElementById("app")!);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Import global providers and tiny shared components here. Do not bulk-import
|
|
50
|
+
every feature component.
|
|
51
|
+
|
|
52
|
+
## Routes
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// src/routes.ts
|
|
56
|
+
import { layout, routes } from "@madojs/mado";
|
|
57
|
+
import { requireAuth } from "./lib/auth.js";
|
|
58
|
+
|
|
59
|
+
export const manifest = {
|
|
60
|
+
"/": () => import("./pages/home.js"),
|
|
61
|
+
"/login": layout({
|
|
62
|
+
layout: () => import("./layouts/auth.js"),
|
|
63
|
+
routes: { "/": () => import("./pages/login.js") },
|
|
64
|
+
}),
|
|
65
|
+
"/admin": layout({
|
|
66
|
+
layout: () => import("./layouts/app.js"),
|
|
67
|
+
guard: requireAuth,
|
|
68
|
+
routes: {
|
|
69
|
+
"/": () => import("./pages/admin/dashboard.js"),
|
|
70
|
+
"/orders": () => import("./pages/admin/orders.js"),
|
|
71
|
+
"/orders/:id": () => import("./pages/admin/order-detail.js"),
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
"*": () => import("./pages/not-found.js"),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default routes(manifest, {
|
|
78
|
+
errorPage: (err) => html`<main><h1>Something went wrong</h1><pre>${err.message}</pre></main>`,
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Exporting `manifest` lets `mado bake` inspect the same route table.
|
|
83
|
+
|
|
84
|
+
## API and auth
|
|
85
|
+
|
|
86
|
+
Use one API client and one auth module. The admin starter ships a complete
|
|
87
|
+
version with token storage, a single-flight refresh request, and a route guard.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// pages/admin/orders.ts
|
|
91
|
+
import { each, html, page, resource } from "@madojs/mado";
|
|
92
|
+
import { api } from "../../lib/api.js";
|
|
93
|
+
|
|
94
|
+
const orders = resource(() => "/api/orders", () => api.get("/orders"));
|
|
95
|
+
|
|
96
|
+
export default page({
|
|
97
|
+
title: "Orders",
|
|
98
|
+
view: () => html`
|
|
99
|
+
<main>
|
|
100
|
+
<h1>Orders</h1>
|
|
101
|
+
<ul>
|
|
102
|
+
${() => each(orders.data() ?? [], o => o.id, o => html`<li>${o.number}</li>`)}
|
|
103
|
+
</ul>
|
|
104
|
+
</main>
|
|
105
|
+
`,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Mutations should declare invalidation near the write:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const save = mutation((payload) => api.post("/orders", payload), {
|
|
113
|
+
invalidates: ["/api/orders*"],
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Forms
|
|
118
|
+
|
|
119
|
+
Prefer one `useForm()` per user workflow.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const form = useForm({
|
|
123
|
+
email: { required: true, type: "email" },
|
|
124
|
+
"items.*.title": { required: true },
|
|
125
|
+
});
|
|
126
|
+
const items = form.array("items");
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Use dotted paths for arrays (`items.0.title`) and keep async validation in
|
|
130
|
+
`validateAsync` when it talks to the backend.
|
|
131
|
+
|
|
132
|
+
## Release
|
|
133
|
+
|
|
134
|
+
Local development uses `mado dev`. Production uses exactly one artifact:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
mado release
|
|
138
|
+
rsync -avz out/ user@server:/var/www/app/
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`out/` is the deploy folder. `dist/` is internal build output.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Layouts
|
|
2
|
+
|
|
3
|
+
> **One blessed path.** Layouts in Mado are nested-route groups with a shared
|
|
4
|
+
> shell. There is exactly one canonical place to declare a layout — your
|
|
5
|
+
> `routes.ts` manifest. Putting layout code anywhere else (in `main.ts`, in a
|
|
6
|
+
> page view, in a global custom-element wrapper) is a bug pattern: the LLM and
|
|
7
|
+
> the human both produce visually broken UI when they guess differently.
|
|
8
|
+
|
|
9
|
+
## The canonical recipe
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// src/routes.ts
|
|
13
|
+
import { layout, routes } from "@madojs/mado";
|
|
14
|
+
import { requireAuth } from "./lib/auth.js";
|
|
15
|
+
|
|
16
|
+
export const manifest = {
|
|
17
|
+
"/": () => import("./pages/home.js"), // no layout
|
|
18
|
+
"/login": layout({
|
|
19
|
+
layout: () => import("./layouts/auth.js"), // centered card
|
|
20
|
+
routes: { "/": () => import("./pages/login.js") },
|
|
21
|
+
}),
|
|
22
|
+
"/admin": layout({
|
|
23
|
+
layout: () => import("./layouts/app.js"), // admin shell
|
|
24
|
+
guard: requireAuth, // ← see 12-auth-and-api.md
|
|
25
|
+
routes: {
|
|
26
|
+
"/": () => import("./pages/admin/dashboard.js"),
|
|
27
|
+
"/orders": () => import("./pages/admin/orders.js"),
|
|
28
|
+
"/orders/:id": () => import("./pages/admin/order-detail.js"),
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
"*": () => import("./pages/not-found.js"),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default routes(manifest);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A layout is just a `page({ view })` that renders `${ctx.child}` somewhere:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// src/layouts/app.ts
|
|
41
|
+
import { html, page } from "@madojs/mado";
|
|
42
|
+
import "../components/app-shell.js"; // <x-app-shell> (sidebar + topbar + slot)
|
|
43
|
+
|
|
44
|
+
export default page({
|
|
45
|
+
view: ({ child }) => html`
|
|
46
|
+
<x-app-shell>${child}</x-app-shell>
|
|
47
|
+
`,
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That is the whole API.
|
|
52
|
+
|
|
53
|
+
- **Order of layouts** matters: outer groups wrap inner groups. The order in
|
|
54
|
+
the manifest is exactly the order of rendering.
|
|
55
|
+
- **One shell per group**, not one shell per page. If you want a different
|
|
56
|
+
shell for a subtree, create a new group with its own `layout`.
|
|
57
|
+
- **Layouts can be lazy** (`() => import(...)`). They are loaded together
|
|
58
|
+
with the page.
|
|
59
|
+
|
|
60
|
+
## Why "one blessed path"
|
|
61
|
+
|
|
62
|
+
Without this convention, every page accumulates `<x-app-shell>${...}</x-app-shell>`
|
|
63
|
+
boilerplate, the LLM eventually puts the shell wrapper into `main.ts` "to make
|
|
64
|
+
it consistent", and the next refactor produces the classic
|
|
65
|
+
*"navigation appears below the page content"* screenshot. The nested-routes
|
|
66
|
+
recipe makes the shell the **outer frame** structurally; there is no way to
|
|
67
|
+
re-order it by accident.
|
|
68
|
+
|
|
69
|
+
## Two acceptable alternatives (with caveats)
|
|
70
|
+
|
|
71
|
+
These exist for completeness. Reach for them only if you cannot use nested
|
|
72
|
+
routes.
|
|
73
|
+
|
|
74
|
+
### a) A single shell with the router slot in `main.ts`
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { html, render } from "@madojs/mado";
|
|
78
|
+
import "./components/app-shell.js";
|
|
79
|
+
import router from "./routes.js";
|
|
80
|
+
|
|
81
|
+
render(html`<x-app-shell>${router.view}</x-app-shell>`, app);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Caveat: every route now lives inside one shell. You cannot have a centered
|
|
85
|
+
login page or a marketing landing page without the admin chrome around it.
|
|
86
|
+
Use this only for single-shell apps.
|
|
87
|
+
|
|
88
|
+
### b) Per-page wrapping inside `view`
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
export default page({
|
|
92
|
+
view: () => html`
|
|
93
|
+
<x-app-shell>
|
|
94
|
+
<h1>Orders</h1>
|
|
95
|
+
...
|
|
96
|
+
</x-app-shell>
|
|
97
|
+
`,
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Caveat: repetition. Every new page must remember the wrapper. The first time
|
|
102
|
+
someone forgets it, the layout disappears and the LLM "fixes" it in the wrong
|
|
103
|
+
place. **Do not start with this.**
|
|
104
|
+
|
|
105
|
+
## Where to find more
|
|
106
|
+
|
|
107
|
+
- `src/page.ts` defines `layout()`, `page()`, `Guard` and `NestedRoutes`.
|
|
108
|
+
- `src/router/manifest.ts` flattens the nested manifest and applies guards
|
|
109
|
+
outer → inner before the page renders.
|
|
110
|
+
- The `admin` starter (`mado init my-app --starter admin`) ships with three
|
|
111
|
+
groups (`/`, `/login`, `/admin`) and is the reference implementation.
|
|
112
|
+
|
|
113
|
+
If you ever feel tempted to invent a fourth pattern, write it down in your
|
|
114
|
+
project `docs/` first and discuss it with the team. The cost of inconsistency
|
|
115
|
+
in this exact spot is higher than the cost of a slightly awkward layout.
|