@madojs/mado 0.10.0 → 0.11.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 +24 -26
- package/CHANGELOG.md +98 -0
- package/README.md +18 -45
- package/TODO.md +52 -48
- package/dist/src/component.d.ts +2 -1
- package/dist/src/component.js +5 -2
- package/dist/src/component.js.map +1 -1
- package/dist/src/each.d.ts +1 -1
- package/dist/src/each.js +1 -1
- package/dist/src/each.js.map +1 -1
- package/dist/src/html/template.js +10 -0
- package/dist/src/html/template.js.map +1 -1
- package/dist/src/index.d.ts +11 -6
- package/dist/src/index.js +5 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/lazy.d.ts +1 -1
- package/dist/src/lazy.js +1 -1
- package/dist/src/lazy.js.map +1 -1
- package/dist/src/page.d.ts +17 -21
- package/dist/src/page.js +7 -12
- package/dist/src/page.js.map +1 -1
- package/dist/src/router/manifest.d.ts +1 -1
- package/dist/src/router/manifest.js +21 -13
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +2 -2
- package/dist/src/router/match.js +3 -3
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.js +1 -1
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/vite/index.d.ts +10 -0
- package/dist/src/vite/index.js +33 -0
- package/dist/src/vite/index.js.map +1 -0
- package/docs/en/00-the-mado-way.md +25 -12
- package/docs/en/01-routing.md +90 -142
- package/docs/en/02-project-layout.md +59 -52
- package/docs/en/03-static-bake.md +5 -6
- package/docs/en/05-why-mado.md +6 -6
- package/docs/en/06-for-backenders.md +18 -22
- package/docs/en/08-llm-zero-history-test.md +9 -14
- package/docs/en/09-shadow-vs-light-dom.md +28 -36
- package/docs/en/10-app-architecture.md +158 -96
- package/docs/en/11-layouts.md +22 -24
- package/docs/en/12-auth-and-api.md +89 -182
- package/docs/en/13-deployment.md +25 -26
- package/docs/en/14-testing.md +4 -4
- package/docs/en/16-bake-cookbook.md +17 -10
- package/docs/en/18-api-freeze-map.md +6 -4
- package/docs/en/20-v1-stability.md +1 -1
- package/docs/fr/00-the-mado-way.md +55 -90
- package/docs/fr/01-routing.md +70 -152
- package/docs/fr/02-project-layout.md +74 -48
- package/docs/fr/03-static-bake.md +1 -1
- package/docs/fr/05-why-mado.md +6 -6
- package/docs/fr/06-for-backenders.md +7 -7
- package/docs/fr/08-llm-zero-history-test.md +21 -48
- package/docs/fr/09-shadow-vs-light-dom.md +43 -162
- package/docs/fr/10-app-architecture.md +110 -33
- package/docs/fr/11-layouts.md +24 -12
- package/docs/fr/12-auth-and-api.md +63 -22
- package/docs/fr/13-deployment.md +30 -12
- package/docs/fr/14-testing.md +1 -1
- package/docs/fr/16-bake-cookbook.md +57 -4
- package/docs/fr/18-api-freeze-map.md +1 -1
- package/docs/fr/20-v1-stability.md +1 -1
- package/docs/recipes/nginx/README.md +13 -0
- package/docs/ru/00-the-mado-way.md +53 -75
- package/docs/ru/01-routing.md +68 -143
- package/docs/ru/02-project-layout.md +75 -48
- package/docs/ru/03-static-bake.md +2 -2
- package/docs/ru/05-why-mado.md +6 -6
- package/docs/ru/06-for-backenders.md +7 -7
- package/docs/ru/08-llm-zero-history-test.md +9 -14
- package/docs/ru/09-shadow-vs-light-dom.md +43 -178
- package/docs/ru/10-app-architecture.md +115 -63
- package/docs/ru/11-layouts.md +24 -24
- package/docs/ru/12-auth-and-api.md +57 -35
- package/docs/ru/13-deployment.md +19 -13
- package/docs/ru/14-testing.md +1 -1
- package/docs/ru/16-bake-cookbook.md +48 -8
- package/docs/ru/18-api-freeze-map.md +5 -3
- package/docs/ru/20-v1-stability.md +1 -1
- package/docs/uk/00-the-mado-way.md +70 -44
- package/docs/uk/01-routing.md +41 -47
- package/docs/uk/02-project-layout.md +68 -41
- package/docs/uk/03-static-bake.md +1 -2
- package/docs/uk/06-for-backenders.md +3 -3
- package/docs/uk/08-llm-zero-history-test.md +22 -24
- package/docs/uk/09-shadow-vs-light-dom.md +37 -86
- package/docs/uk/10-app-architecture.md +72 -31
- package/docs/uk/11-layouts.md +25 -12
- package/docs/uk/12-auth-and-api.md +58 -22
- package/docs/uk/13-deployment.md +4 -3
- package/docs/uk/14-testing.md +1 -1
- package/docs/uk/18-api-freeze-map.md +1 -1
- package/docs/uk/20-v1-stability.md +1 -1
- package/llms.txt +14 -15
- package/package.json +18 -11
- package/scripts/_config.mjs +15 -161
- package/scripts/bake.mjs +71 -58
- package/scripts/cli/generate.mjs +348 -0
- package/scripts/cli/help.mjs +27 -0
- package/scripts/cli/index.mjs +79 -0
- package/scripts/cli/init.mjs +153 -0
- package/scripts/cli/release.mjs +152 -0
- package/scripts/cli/run.mjs +96 -0
- package/scripts/cli.mjs +2 -560
- package/scripts/package-smoke.mjs +4 -1
- package/scripts/preview.mjs +17 -61
- package/scripts/size-budget.mjs +5 -2
- package/scripts/vite.default.mjs +11 -0
- package/starters/default/.editorconfig +12 -0
- package/starters/default/README.md +74 -0
- package/starters/default/eslint.config.mjs +256 -0
- package/starters/default/index.html +13 -0
- package/starters/default/package.json +30 -0
- package/starters/default/public/favicon.svg +4 -0
- package/starters/default/src/app.routes.ts +39 -0
- package/starters/default/src/layouts/app-shell.layout.ts +35 -0
- package/starters/default/src/layouts/auth-shell.layout.ts +17 -0
- package/starters/default/src/main.ts +16 -0
- package/starters/default/src/modules/auth/_contracts/auth-api.types.ts +17 -0
- package/starters/default/src/modules/auth/auth.connector.ts +45 -0
- package/starters/default/src/modules/auth/auth.guard.ts +22 -0
- package/starters/default/src/modules/auth/auth.public.ts +9 -0
- package/starters/default/src/modules/auth/auth.routes.ts +8 -0
- package/starters/default/src/modules/auth/auth.service.ts +71 -0
- package/starters/default/src/modules/auth/auth.types.ts +15 -0
- package/starters/default/src/modules/auth/login.page.ts +62 -0
- package/starters/default/src/modules/billing/_contracts/stripe.types.ts +17 -0
- package/starters/default/src/modules/billing/api/stripe.connector.ts +71 -0
- package/starters/default/src/modules/billing/billing.public.ts +5 -0
- package/starters/default/src/modules/billing/billing.routes.ts +9 -0
- package/starters/default/src/modules/billing/billing.types.ts +15 -0
- package/starters/default/src/modules/billing/components/invoice-status-badge.component.ts +43 -0
- package/starters/default/src/modules/billing/data/invoices.resource.ts +35 -0
- package/starters/default/src/modules/billing/pages/invoice-detail.page.ts +70 -0
- package/starters/default/src/modules/billing/pages/invoices-list.page.ts +73 -0
- package/starters/default/src/modules/home/home.page.ts +34 -0
- package/starters/default/src/modules/home/not-found.page.ts +11 -0
- package/starters/default/src/shared/http/http-client.ts +86 -0
- package/starters/default/src/shared/http/http-error.ts +37 -0
- package/starters/default/src/shared/http/interceptors.ts +59 -0
- package/starters/default/src/shared/lib/format-date.ts +19 -0
- package/starters/default/src/shared/styles/content.css +70 -0
- package/starters/default/src/shared/styles/reset.css +32 -0
- package/starters/default/src/shared/styles/shell.css +57 -0
- package/starters/default/src/shared/styles/tokens.css +44 -0
- package/starters/default/src/shared/ui/x-button.component.ts +49 -0
- package/starters/default/src/shared/ui/x-spinner.component.ts +22 -0
- package/starters/default/src/styles.d.ts +1 -0
- package/starters/default/src/vite-env.d.ts +1 -0
- package/starters/default/tsconfig.json +24 -0
- package/starters/default/vite.config.ts +9 -0
- package/MADO_V1_PLAN.md +0 -179
- package/ROADMAP.md +0 -178
- package/dist/src/html.d.ts +0 -18
- package/dist/src/html.js +0 -17
- package/dist/src/html.js.map +0 -1
- package/dist/src/router.d.ts +0 -13
- package/dist/src/router.js +0 -13
- package/dist/src/router.js.map +0 -1
- package/scripts/bundle.mjs +0 -212
- package/scripts/llm-zero-history-smoke.mjs +0 -93
- package/scripts/new.mjs +0 -80
- package/scripts/showcase-regression.mjs +0 -392
- package/server/serve.mjs +0 -455
- package/starters/admin/README.md +0 -63
- package/starters/admin/index.html +0 -28
- package/starters/admin/mado.config.json +0 -22
- package/starters/admin/package.json +0 -24
- package/starters/admin/public/favicon.svg +0 -4
- package/starters/admin/src/components/x-button.ts +0 -82
- package/starters/admin/src/components/x-input.ts +0 -105
- package/starters/admin/src/layouts/app.ts +0 -101
- package/starters/admin/src/layouts/auth.ts +0 -41
- package/starters/admin/src/lib/api.ts +0 -184
- package/starters/admin/src/lib/auth.ts +0 -83
- package/starters/admin/src/main.ts +0 -15
- package/starters/admin/src/pages/admin/dashboard.ts +0 -48
- package/starters/admin/src/pages/admin/order-detail.ts +0 -80
- package/starters/admin/src/pages/admin/orders.ts +0 -117
- package/starters/admin/src/pages/home.ts +0 -34
- package/starters/admin/src/pages/login.ts +0 -70
- package/starters/admin/src/pages/not-found.ts +0 -12
- package/starters/admin/src/routes.ts +0 -40
- package/starters/admin/src/styles/global.ts +0 -86
- package/starters/admin/tsconfig.json +0 -15
- package/starters/crud/README.md +0 -33
- package/starters/crud/index.html +0 -28
- package/starters/crud/mado.config.json +0 -20
- package/starters/crud/package.json +0 -24
- package/starters/crud/src/components/app-shell.ts +0 -56
- package/starters/crud/src/components/ticket-detail.ts +0 -33
- package/starters/crud/src/components/ticket-form.ts +0 -69
- package/starters/crud/src/components/ticket-list.ts +0 -66
- package/starters/crud/src/lib/api.ts +0 -76
- package/starters/crud/src/main.ts +0 -9
- package/starters/crud/src/pages/home.ts +0 -34
- package/starters/crud/src/pages/not-found.ts +0 -12
- package/starters/crud/src/pages/ticket-detail.ts +0 -7
- package/starters/crud/src/pages/ticket-new.ts +0 -7
- package/starters/crud/src/pages/tickets.ts +0 -7
- package/starters/crud/src/routes.ts +0 -11
- package/starters/crud/src/styles/global.ts +0 -155
- package/starters/crud/tsconfig.json +0 -15
- package/starters/minimal/README.md +0 -21
- package/starters/minimal/index.html +0 -28
- package/starters/minimal/mado.config.json +0 -20
- package/starters/minimal/package.json +0 -24
- package/starters/minimal/src/components/app-counter.ts +0 -31
- package/starters/minimal/src/main.ts +0 -9
- package/starters/minimal/src/pages/home.ts +0 -35
- package/starters/minimal/src/pages/not-found.ts +0 -14
- package/starters/minimal/src/routes.ts +0 -8
- package/starters/minimal/src/styles/global.ts +0 -60
- package/starters/minimal/tsconfig.json +0 -15
- package/templates/page-detail.ts +0 -63
- package/templates/page-form.ts +0 -94
- package/templates/page-list.ts +0 -79
package/scripts/preview.mjs
CHANGED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
// mado preview
|
|
4
4
|
//
|
|
5
5
|
// What it does:
|
|
6
|
-
// 1.
|
|
6
|
+
// 1. Serves OUT_DIR or `out/`.
|
|
7
7
|
// 2. If `out/` is missing AND we are in a project root, refuses to run and
|
|
8
|
-
// points the user at `mado release`.
|
|
9
|
-
//
|
|
10
|
-
// repo.)
|
|
8
|
+
// points the user at `mado release`. Auto-build is opt-in via
|
|
9
|
+
// PREVIEW_AUTOBUILD=1.
|
|
11
10
|
// 3. Starts a static server with:
|
|
12
11
|
// - immutable cache for hashed bundles;
|
|
13
12
|
// - SPA fallback to index.html;
|
|
14
|
-
// -
|
|
13
|
+
// - exact `out/` route files before SPA fallback;
|
|
15
14
|
// - precompressed .gz / .br serving via Accept-Encoding.
|
|
16
15
|
//
|
|
17
16
|
// Goal: see production-like output locally without Docker/nginx, identical to
|
|
@@ -22,8 +21,6 @@ import { readFile, stat, access } from "node:fs/promises";
|
|
|
22
21
|
import { extname, join, resolve, sep } from "node:path";
|
|
23
22
|
import { spawnSync } from "node:child_process";
|
|
24
23
|
|
|
25
|
-
import { loadConfig } from "./_config.mjs";
|
|
26
|
-
|
|
27
24
|
// Tiny argv parser. Supports --flag, --flag=value, --flag value.
|
|
28
25
|
function parsePreviewArgs(argv) {
|
|
29
26
|
const flags = {};
|
|
@@ -51,22 +48,10 @@ function parsePreviewArgs(argv) {
|
|
|
51
48
|
|
|
52
49
|
const PREVIEW_FLAGS = parsePreviewArgs(process.argv.slice(2));
|
|
53
50
|
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
process.env.OUT_DIR ?? cfg.build.out ?? "out",
|
|
59
|
-
);
|
|
60
|
-
// Baked HTML lives in <out>/baked/ by default (see scripts/bake.mjs and
|
|
61
|
-
// mado.config.json bake.outDir). Preview serves it with priority over the
|
|
62
|
-
// SPA shell so URLs that have a prerendered HTML page render real markup
|
|
63
|
-
// instead of an empty <div id="app"></div>.
|
|
64
|
-
const BAKED = resolve(
|
|
65
|
-
ROOT,
|
|
66
|
-
process.env.BAKED_DIR ?? cfg.bake?.outDir ?? join(cfg.build.out ?? "out", "baked"),
|
|
67
|
-
);
|
|
68
|
-
const PORT = Number(PREVIEW_FLAGS.port ?? process.env.PORT ?? cfg.dev?.port ?? 4173);
|
|
69
|
-
const HOST = String(PREVIEW_FLAGS.host ?? process.env.HOST ?? cfg.dev?.host ?? "localhost");
|
|
51
|
+
const ROOT = resolve(process.cwd());
|
|
52
|
+
const OUT = resolve(ROOT, process.env.OUT_DIR ?? "out");
|
|
53
|
+
const PORT = Number(PREVIEW_FLAGS.port ?? process.env.PORT ?? 4173);
|
|
54
|
+
const HOST = String(PREVIEW_FLAGS.host ?? process.env.HOST ?? "localhost");
|
|
70
55
|
const AUTOBUILD = process.env.PREVIEW_AUTOBUILD === "1";
|
|
71
56
|
const SKIP_BUILD = process.env.SKIP_BUILD === "1" || !AUTOBUILD;
|
|
72
57
|
|
|
@@ -87,23 +72,16 @@ const MIME = {
|
|
|
87
72
|
".map": "application/json; charset=utf-8",
|
|
88
73
|
};
|
|
89
74
|
|
|
90
|
-
// ----------
|
|
75
|
+
// ---------- Optional release ----------
|
|
91
76
|
|
|
92
77
|
if (!SKIP_BUILD) {
|
|
93
|
-
console.log("[preview]
|
|
94
|
-
run("
|
|
95
|
-
|
|
96
|
-
// bake is optional (only when there are pages with bake config)
|
|
97
|
-
console.log("[preview] step 2/3 — bake (optional)");
|
|
98
|
-
run("node", ["scripts/bake.mjs"], { allowFail: true });
|
|
99
|
-
|
|
100
|
-
console.log("[preview] step 3/3 — bundle");
|
|
101
|
-
run("node", ["scripts/bundle.mjs"]);
|
|
78
|
+
console.log("[preview] PREVIEW_AUTOBUILD=1 — running mado release");
|
|
79
|
+
run("node", ["scripts/cli.mjs", "release"]);
|
|
102
80
|
}
|
|
103
81
|
|
|
104
82
|
if (!(await exists(OUT))) {
|
|
105
83
|
console.error(
|
|
106
|
-
`[preview] missing ${OUT}/ — run \`mado release\`
|
|
84
|
+
`[preview] missing ${OUT}/ — run \`mado release\` first.`,
|
|
107
85
|
);
|
|
108
86
|
process.exit(1);
|
|
109
87
|
}
|
|
@@ -111,7 +89,7 @@ if (!(await exists(OUT))) {
|
|
|
111
89
|
const spaShell = join(OUT, "index.html");
|
|
112
90
|
if (!(await exists(spaShell))) {
|
|
113
91
|
console.error(
|
|
114
|
-
`[preview] missing ${spaShell} — \`mado
|
|
92
|
+
`[preview] missing ${spaShell} — \`mado release\` did not produce an HTML entry.\n` +
|
|
115
93
|
`[preview] Without it any non-baked route will 404 instead of falling back to the SPA.`,
|
|
116
94
|
);
|
|
117
95
|
process.exit(1);
|
|
@@ -120,7 +98,7 @@ if (!(await exists(spaShell))) {
|
|
|
120
98
|
// ---------- 4) Server ----------
|
|
121
99
|
|
|
122
100
|
const isImmutable = (filename) =>
|
|
123
|
-
|
|
101
|
+
/\.[A-Za-z0-9_-]{6,}\.(js|css|png|jpg|jpeg|webp|svg|woff2?)$/i.test(filename);
|
|
124
102
|
|
|
125
103
|
const server = createServer(async (req, res) => {
|
|
126
104
|
try {
|
|
@@ -178,12 +156,10 @@ server.on("error", (err) => {
|
|
|
178
156
|
|
|
179
157
|
server.listen(PORT, HOST, async () => {
|
|
180
158
|
const urlHost = HOST === "0.0.0.0" || HOST === "::" ? "localhost" : HOST;
|
|
181
|
-
const bakedReady = await exists(BAKED);
|
|
182
159
|
console.log("");
|
|
183
160
|
console.log("Mado preview (production-like)");
|
|
184
161
|
console.log(` url: http://${urlHost}:${PORT}/`);
|
|
185
162
|
console.log(` out: ${OUT}`);
|
|
186
|
-
console.log(` baked: ${bakedReady ? BAKED : "(none — SPA-only)"}`);
|
|
187
163
|
console.log(" (Ctrl-C to stop)");
|
|
188
164
|
console.log("");
|
|
189
165
|
});
|
|
@@ -215,30 +191,10 @@ function basenameSafe(p) {
|
|
|
215
191
|
async function resolveTarget(pathname) {
|
|
216
192
|
if (pathname === "/") pathname = "/index.html";
|
|
217
193
|
|
|
218
|
-
// 1) Baked HTML wins. `mado bake` writes prerendered pages into
|
|
219
|
-
// <out>/baked/<path>/index.html. Serve them with priority over the
|
|
220
|
-
// SPA shell so search engines AND human users hitting a prerendered
|
|
221
|
-
// URL see real content immediately. Without this branch preview
|
|
222
|
-
// served the empty SPA shell for every URL, which looked like a
|
|
223
|
-
// "blank page" bug even when bake had succeeded.
|
|
224
|
-
if (await exists(BAKED)) {
|
|
225
|
-
if (!extname(pathname) || pathname.endsWith("/index.html")) {
|
|
226
|
-
const bakedDir = join(BAKED, pathname.replace(/\/index\.html$/, ""));
|
|
227
|
-
const bakedIdx = join(bakedDir, "index.html");
|
|
228
|
-
if (await exists(bakedIdx)) return bakedIdx;
|
|
229
|
-
}
|
|
230
|
-
// Direct file (sitemap.xml etc.) from the baked dir.
|
|
231
|
-
const bakedFile = resolve(join(BAKED, pathname));
|
|
232
|
-
if (bakedFile.startsWith(BAKED + sep) && (await exists(bakedFile))) {
|
|
233
|
-
const s = await stat(bakedFile);
|
|
234
|
-
if (!s.isDirectory()) return bakedFile;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
194
|
const candidate = resolve(join(OUT, pathname));
|
|
239
195
|
if (!candidate.startsWith(OUT + sep) && candidate !== OUT) return null;
|
|
240
196
|
|
|
241
|
-
//
|
|
197
|
+
// 1) Exact match inside out/.
|
|
242
198
|
if (await exists(candidate)) {
|
|
243
199
|
const s = await stat(candidate);
|
|
244
200
|
if (s.isDirectory()) {
|
|
@@ -249,13 +205,13 @@ async function resolveTarget(pathname) {
|
|
|
249
205
|
}
|
|
250
206
|
}
|
|
251
207
|
|
|
252
|
-
//
|
|
208
|
+
// 2) /foo → /foo/index.html (for sub-folders without trailing slash).
|
|
253
209
|
if (!extname(pathname)) {
|
|
254
210
|
const asDir = join(OUT, pathname, "index.html");
|
|
255
211
|
if (await exists(asDir)) return asDir;
|
|
256
212
|
}
|
|
257
213
|
|
|
258
|
-
//
|
|
214
|
+
// 3) SPA-fallback: any non-asset path falls back to the SPA shell so
|
|
259
215
|
// client-side routing handles it. Asset-looking paths (with an
|
|
260
216
|
// extension) deliberately 404 instead — otherwise a 200 on
|
|
261
217
|
// /missing.png would mask real bugs.
|
package/scripts/size-budget.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
import { gzipSync } from "node:zlib";
|
|
8
8
|
|
|
9
9
|
const API_ENTRY = "src/index.ts";
|
|
10
|
-
const SAMPLE_ENTRY = "
|
|
10
|
+
const SAMPLE_ENTRY = "starters/default/src/main.ts";
|
|
11
11
|
const API_GZIP_LIMIT = readLimit("MADO_SIZE_API_GZIP_LIMIT", 16 * 1024);
|
|
12
12
|
const SAMPLE_GZIP_LIMIT = readLimit("MADO_SIZE_SAMPLE_GZIP_LIMIT", 42 * 1024);
|
|
13
13
|
|
|
@@ -17,7 +17,7 @@ const api = await bundlePublicApi();
|
|
|
17
17
|
report("public API", api.gzip, API_GZIP_LIMIT);
|
|
18
18
|
|
|
19
19
|
const sample = await bundleSampleApp();
|
|
20
|
-
report("
|
|
20
|
+
report("starter app", sample.gzip, SAMPLE_GZIP_LIMIT);
|
|
21
21
|
|
|
22
22
|
if (failed) process.exit(1);
|
|
23
23
|
|
|
@@ -49,6 +49,9 @@ async function bundleSampleApp() {
|
|
|
49
49
|
platform: "browser",
|
|
50
50
|
splitting: true,
|
|
51
51
|
outdir,
|
|
52
|
+
alias: {
|
|
53
|
+
"@madojs/mado": "./src/index.ts",
|
|
54
|
+
},
|
|
52
55
|
legalComments: "none",
|
|
53
56
|
});
|
|
54
57
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Mado Starter
|
|
2
|
+
|
|
3
|
+
Canonical starter for Mado business apps: admin panels, internal tools and
|
|
4
|
+
long-lived SPAs.
|
|
5
|
+
|
|
6
|
+
## Commands
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
npm run dev
|
|
11
|
+
npm run release
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`npm run release` writes the deploy artifact to `out/`.
|
|
15
|
+
|
|
16
|
+
## Shape
|
|
17
|
+
|
|
18
|
+
```txt
|
|
19
|
+
public/ static assets copied by Vite
|
|
20
|
+
src/
|
|
21
|
+
main.ts imports global CSS and mounts the router
|
|
22
|
+
app.routes.ts app map: zones, layouts, guards and modules
|
|
23
|
+
layouts/ app-zone shells
|
|
24
|
+
shared/
|
|
25
|
+
http/ HTTP client and interceptors
|
|
26
|
+
lib/ pure utilities
|
|
27
|
+
styles/ tokens, reset, shell and content CSS
|
|
28
|
+
ui/ reusable x-* components
|
|
29
|
+
modules/ business modules
|
|
30
|
+
<name>/
|
|
31
|
+
<name>.routes.ts
|
|
32
|
+
<name>.public.ts
|
|
33
|
+
<name>.types.ts
|
|
34
|
+
pages/
|
|
35
|
+
data/
|
|
36
|
+
api/
|
|
37
|
+
components/
|
|
38
|
+
_contracts/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## CSS Contract
|
|
42
|
+
|
|
43
|
+
- `tokens.css` defines CSS custom properties and is safe for Shadow DOM
|
|
44
|
+
components through `var(...)`.
|
|
45
|
+
- `reset.css` normalizes the document/light DOM surface.
|
|
46
|
+
- `shell.css` styles app-zone layouts from `src/layouts/`.
|
|
47
|
+
- `content.css` styles page-level light DOM: forms, tables, prose and simple
|
|
48
|
+
states.
|
|
49
|
+
- Reusable leaf components keep their own styles in ``css`...` `` inside
|
|
50
|
+
`component()` options.
|
|
51
|
+
|
|
52
|
+
Vite uses Lightning CSS for CSS transforms/minification in this starter.
|
|
53
|
+
|
|
54
|
+
## Generate Files
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm run new -- module billing
|
|
58
|
+
npm run new -- page billing/pages/invoices-list
|
|
59
|
+
npm run new -- connector billing/api/stripe
|
|
60
|
+
npm run new -- resource billing/data/invoices
|
|
61
|
+
npm run new -- service billing/cart
|
|
62
|
+
npm run new -- form billing/invoice
|
|
63
|
+
npm run new -- component billing/components/invoice-status-badge
|
|
64
|
+
npm run new -- guard billing/billing
|
|
65
|
+
npm run new -- layout admin-shell
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The generator writes files only. Wire new routes in `src/app.routes.ts` or the
|
|
69
|
+
module route map by hand.
|
|
70
|
+
|
|
71
|
+
## More
|
|
72
|
+
|
|
73
|
+
The full architecture guide lives in the framework docs:
|
|
74
|
+
https://github.com/madojs/mado/tree/main/docs/en
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// ESLint config for the Mado modular starter.
|
|
2
|
+
//
|
|
3
|
+
// Goals:
|
|
4
|
+
// 1. Enforce architectural boundaries (shared/ vs modules/, public surface).
|
|
5
|
+
// 2. Enforce file-form conventions (a *.connector.ts cannot import a *.page.ts, etc.).
|
|
6
|
+
// 3. Forbid barrel files and runtime CSS imports outside main.ts.
|
|
7
|
+
//
|
|
8
|
+
// One ESLint config, always strict. There is no "loose" mode.
|
|
9
|
+
|
|
10
|
+
import tseslint from "@typescript-eslint/eslint-plugin";
|
|
11
|
+
import tsparser from "@typescript-eslint/parser";
|
|
12
|
+
import boundaries from "eslint-plugin-boundaries";
|
|
13
|
+
|
|
14
|
+
export default [
|
|
15
|
+
{
|
|
16
|
+
files: ["src/**/*.ts"],
|
|
17
|
+
languageOptions: {
|
|
18
|
+
parser: tsparser,
|
|
19
|
+
parserOptions: {
|
|
20
|
+
ecmaVersion: 2022,
|
|
21
|
+
sourceType: "module",
|
|
22
|
+
project: "./tsconfig.json",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
plugins: {
|
|
26
|
+
"@typescript-eslint": tseslint,
|
|
27
|
+
boundaries,
|
|
28
|
+
},
|
|
29
|
+
settings: {
|
|
30
|
+
"boundaries/elements": [
|
|
31
|
+
// App composition root — entry points + manifest.
|
|
32
|
+
{ type: "app-root", pattern: "src/{main,app.routes}.ts", mode: "file" },
|
|
33
|
+
|
|
34
|
+
// App-level layouts. Live in src/layouts/ because they describe
|
|
35
|
+
// app zones (auth zone, app zone, marketing zone), not domains.
|
|
36
|
+
{ type: "app-layout", pattern: "src/layouts/*.layout.ts", mode: "file" },
|
|
37
|
+
|
|
38
|
+
// Cross-cutting layer.
|
|
39
|
+
{ type: "shared", pattern: "src/shared/**" },
|
|
40
|
+
|
|
41
|
+
// Module sub-layers (more specific patterns first).
|
|
42
|
+
// NB: there is no `module-layout` — modules don't own layouts.
|
|
43
|
+
{ type: "module-public", pattern: "src/modules/*/*.public.ts", mode: "file" },
|
|
44
|
+
{ type: "module-types", pattern: "src/modules/*/*.types.ts", mode: "file" },
|
|
45
|
+
{ type: "module-routes", pattern: "src/modules/*/*.routes.ts", mode: "file" },
|
|
46
|
+
{ type: "module-guard", pattern: "src/modules/*/*.guard.ts", mode: "file" },
|
|
47
|
+
{ type: "module-contracts", pattern: "src/modules/*/_contracts/**" },
|
|
48
|
+
{ type: "module-connector", pattern: "src/modules/**/*.connector.ts", mode: "file" },
|
|
49
|
+
{ type: "module-resource", pattern: "src/modules/**/*.resource.ts", mode: "file" },
|
|
50
|
+
{ type: "module-service", pattern: "src/modules/**/*.service.ts", mode: "file" },
|
|
51
|
+
{ type: "module-form", pattern: "src/modules/**/*.form.ts", mode: "file" },
|
|
52
|
+
{ type: "module-page", pattern: "src/modules/**/*.page.ts", mode: "file" },
|
|
53
|
+
{ type: "module-component", pattern: "src/modules/**/*.component.ts", mode: "file" },
|
|
54
|
+
{ type: "module-internal", pattern: "src/modules/*/**" },
|
|
55
|
+
],
|
|
56
|
+
"boundaries/include": ["src/**/*.ts"],
|
|
57
|
+
},
|
|
58
|
+
rules: {
|
|
59
|
+
// ---------------------------------------------------------------------
|
|
60
|
+
// 1. Module boundaries
|
|
61
|
+
// ---------------------------------------------------------------------
|
|
62
|
+
"boundaries/element-types": [
|
|
63
|
+
2,
|
|
64
|
+
{
|
|
65
|
+
default: "disallow",
|
|
66
|
+
rules: [
|
|
67
|
+
// app-root may import anything (it composes the app)
|
|
68
|
+
{ from: "app-root", allow: ["*"] },
|
|
69
|
+
|
|
70
|
+
// app-layout: pure UI wrapper. May read other modules' public
|
|
71
|
+
// for nav state (auth, i18n…). No connectors/resources/services.
|
|
72
|
+
{
|
|
73
|
+
from: "app-layout",
|
|
74
|
+
allow: ["shared", "module-public", "module-types"],
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// shared/* lives in isolation
|
|
78
|
+
{ from: "shared", allow: ["shared"] },
|
|
79
|
+
|
|
80
|
+
// module-public: gateway only re-exports module internals
|
|
81
|
+
{
|
|
82
|
+
from: "module-public",
|
|
83
|
+
allow: [
|
|
84
|
+
"shared",
|
|
85
|
+
"module-internal",
|
|
86
|
+
"module-types",
|
|
87
|
+
"module-service",
|
|
88
|
+
"module-resource",
|
|
89
|
+
"module-form",
|
|
90
|
+
"module-guard",
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// module-types are pure types
|
|
95
|
+
{ from: "module-types", allow: ["module-types"] },
|
|
96
|
+
|
|
97
|
+
// module-routes: plain map of paths to lazy pages of THIS module.
|
|
98
|
+
// Modules do not own layouts — composition happens in app.routes.ts.
|
|
99
|
+
{
|
|
100
|
+
from: "module-routes",
|
|
101
|
+
allow: ["module-page"],
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// module-guard: reads service state of its own module
|
|
105
|
+
{
|
|
106
|
+
from: "module-guard",
|
|
107
|
+
allow: ["module-service", "module-public", "module-types", "shared"],
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// _contracts are private DTOs of one connector
|
|
111
|
+
{ from: "module-contracts", allow: ["module-contracts"] },
|
|
112
|
+
|
|
113
|
+
// Connectors talk to HTTP and contracts. No UI, no signals.
|
|
114
|
+
{
|
|
115
|
+
from: "module-connector",
|
|
116
|
+
allow: ["shared", "module-contracts", "module-types"],
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// Resources/mutations bridge connector ↔ app
|
|
120
|
+
{
|
|
121
|
+
from: "module-resource",
|
|
122
|
+
allow: ["shared", "module-connector", "module-types", "module-public"],
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// Services hold module state. May talk to other modules' public.
|
|
126
|
+
{
|
|
127
|
+
from: "module-service",
|
|
128
|
+
allow: [
|
|
129
|
+
"shared",
|
|
130
|
+
"module-connector",
|
|
131
|
+
"module-resource",
|
|
132
|
+
"module-types",
|
|
133
|
+
"module-public",
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Forms = schema + validators
|
|
138
|
+
{
|
|
139
|
+
from: "module-form",
|
|
140
|
+
allow: ["shared", "module-types", "module-public"],
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// Pages may use everything inside their own module + shared + others' public
|
|
144
|
+
{
|
|
145
|
+
from: "module-page",
|
|
146
|
+
allow: [
|
|
147
|
+
"shared",
|
|
148
|
+
"module-component",
|
|
149
|
+
"module-service",
|
|
150
|
+
"module-resource",
|
|
151
|
+
"module-form",
|
|
152
|
+
"module-types",
|
|
153
|
+
"module-public",
|
|
154
|
+
"module-internal",
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// Reusable components stay UI-only: no business state.
|
|
159
|
+
{
|
|
160
|
+
from: "module-component",
|
|
161
|
+
allow: ["shared", "module-component", "module-types"],
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// Fallback for module-internal helpers (_parts/*, etc.)
|
|
165
|
+
{
|
|
166
|
+
from: "module-internal",
|
|
167
|
+
allow: ["shared", "module-internal", "module-types", "module-public"],
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------
|
|
174
|
+
// 2. No barrel files. Encourages explicit imports, helps tree-shaking
|
|
175
|
+
// and reduces LLM context noise.
|
|
176
|
+
// ---------------------------------------------------------------------
|
|
177
|
+
"no-restricted-syntax": [
|
|
178
|
+
"error",
|
|
179
|
+
{
|
|
180
|
+
selector: "ExportAllDeclaration",
|
|
181
|
+
message:
|
|
182
|
+
"Barrel re-exports are forbidden. Re-export individual symbols in *.public.ts.",
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------
|
|
187
|
+
// 3. Imports
|
|
188
|
+
// ---------------------------------------------------------------------
|
|
189
|
+
"no-restricted-imports": [
|
|
190
|
+
"error",
|
|
191
|
+
{
|
|
192
|
+
patterns: [
|
|
193
|
+
{
|
|
194
|
+
group: ["**/index", "**/index.ts", "**/index.js"],
|
|
195
|
+
message: "Do not import from index files. Import the file directly.",
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------
|
|
202
|
+
// 4. TypeScript hygiene
|
|
203
|
+
// ---------------------------------------------------------------------
|
|
204
|
+
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
|
205
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
206
|
+
"@typescript-eslint/no-unused-vars": [
|
|
207
|
+
"error",
|
|
208
|
+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
// Only main.ts may import CSS at runtime.
|
|
214
|
+
{
|
|
215
|
+
files: ["src/**/*.ts"],
|
|
216
|
+
ignores: ["src/main.ts"],
|
|
217
|
+
rules: {
|
|
218
|
+
"no-restricted-imports": [
|
|
219
|
+
"error",
|
|
220
|
+
{
|
|
221
|
+
patterns: [
|
|
222
|
+
{
|
|
223
|
+
group: ["*.css", "**/*.css"],
|
|
224
|
+
message:
|
|
225
|
+
"Import CSS only in src/main.ts. Component styles must use css`` inside the component file.",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
group: ["**/index", "**/index.ts", "**/index.js"],
|
|
229
|
+
message: "Do not import from index files. Import the file directly.",
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
// _contracts are DTOs for connectors only.
|
|
238
|
+
{
|
|
239
|
+
files: ["src/**/*.ts"],
|
|
240
|
+
ignores: ["src/**/*.connector.ts"],
|
|
241
|
+
rules: {
|
|
242
|
+
"no-restricted-imports": [
|
|
243
|
+
"error",
|
|
244
|
+
{
|
|
245
|
+
patterns: [
|
|
246
|
+
{
|
|
247
|
+
group: ["**/_contracts/*"],
|
|
248
|
+
message:
|
|
249
|
+
"External DTOs (_contracts) are private to the connector. Use the domain type instead.",
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="/favicon.svg" />
|
|
7
|
+
<title>Mado App</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "mado dev",
|
|
8
|
+
"build": "mado release",
|
|
9
|
+
"typecheck": "mado typecheck",
|
|
10
|
+
"test": "npm run typecheck && npm run lint",
|
|
11
|
+
"release": "mado release",
|
|
12
|
+
"preview": "mado preview",
|
|
13
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
14
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
15
|
+
"new": "mado new"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@madojs/mado": "__MADOJS_VERSION__"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
22
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
23
|
+
"eslint": "^9.0.0",
|
|
24
|
+
"eslint-plugin-boundaries": "^5.0.0",
|
|
25
|
+
"linkedom": "^0.18.12",
|
|
26
|
+
"lightningcss": "^1.32.0",
|
|
27
|
+
"typescript": "^5.5.0",
|
|
28
|
+
"vite": "^8.0.16"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Single source of truth for the application route table.
|
|
2
|
+
//
|
|
3
|
+
// This file is the APP MAP. Reading it = understanding the app.
|
|
4
|
+
//
|
|
5
|
+
// One rule:
|
|
6
|
+
// Modules export plain `routes` maps. The choice of SHELL and GUARD for
|
|
7
|
+
// each zone of the app is made HERE, by wrapping a module's routes with
|
|
8
|
+
// a `layout({...})` block.
|
|
9
|
+
//
|
|
10
|
+
// `manifest` is exported separately so `mado bake` can discover pages that
|
|
11
|
+
// declare `bake: { paths, data }`.
|
|
12
|
+
|
|
13
|
+
import { layout, routes } from "@madojs/mado";
|
|
14
|
+
|
|
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
|
+
// Public landing (no shell, no guard).
|
|
21
|
+
"/": () => import("./modules/home/home.page"),
|
|
22
|
+
|
|
23
|
+
// AUTH ZONE — centered card, no guard.
|
|
24
|
+
"/login": layout({
|
|
25
|
+
layout: () => import("./layouts/auth-shell.layout"),
|
|
26
|
+
routes: authRoutes,
|
|
27
|
+
}),
|
|
28
|
+
|
|
29
|
+
// APP ZONE — header + nav, guarded by requireAuth.
|
|
30
|
+
"/billing": layout({
|
|
31
|
+
layout: () => import("./layouts/app-shell.layout"),
|
|
32
|
+
guard: requireAuth,
|
|
33
|
+
routes: billingRoutes,
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
"*": () => import("./modules/home/not-found.page"),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default routes(manifest);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// App-wide shell used by the authenticated zone (every module that lives
|
|
2
|
+
// behind requireAuth).
|
|
3
|
+
//
|
|
4
|
+
// A layout is a STRAIGHTFORWARD wrapper:
|
|
5
|
+
// - it owns the chrome (header / nav / footer);
|
|
6
|
+
// - it does NOT know which page is rendered inside (`child` is anonymous);
|
|
7
|
+
// - it does NOT keep per-route state;
|
|
8
|
+
// - it MAY read cross-cutting modules' public surfaces (auth, i18n…).
|
|
9
|
+
//
|
|
10
|
+
// Layouts live in src/layouts/ because they describe APP ZONES, not domains.
|
|
11
|
+
|
|
12
|
+
import { html, page } from "@madojs/mado";
|
|
13
|
+
|
|
14
|
+
import { isAuthed, logout, user } from "../modules/auth/auth.public";
|
|
15
|
+
import "../shared/ui/x-button.component";
|
|
16
|
+
|
|
17
|
+
export default page({
|
|
18
|
+
title: "Mado App",
|
|
19
|
+
view: ({ child }) => html`
|
|
20
|
+
<div class="layout layout--app">
|
|
21
|
+
<header class="app-header">
|
|
22
|
+
<a href="/" class="brand">Mado App</a>
|
|
23
|
+
<nav>
|
|
24
|
+
<a href="/billing/invoices">Invoices</a>
|
|
25
|
+
${() =>
|
|
26
|
+
isAuthed()
|
|
27
|
+
? html`<span class="who">${() => user()?.email ?? ""}</span>
|
|
28
|
+
<x-button variant="ghost" @click=${logout}>Sign out</x-button>`
|
|
29
|
+
: html`<a href="/login">Sign in</a>`}
|
|
30
|
+
</nav>
|
|
31
|
+
</header>
|
|
32
|
+
<main class="app-main">${child}</main>
|
|
33
|
+
</div>
|
|
34
|
+
`,
|
|
35
|
+
});
|