@madojs/mado 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +102 -25
- package/CHANGELOG.md +185 -1
- package/README.md +137 -172
- package/dist/src/component.d.ts +3 -2
- package/dist/src/component.js +98 -6
- package/dist/src/component.js.map +1 -1
- package/dist/src/css.js +34 -5
- package/dist/src/css.js.map +1 -1
- package/dist/src/head.d.ts +6 -6
- package/dist/src/head.js +6 -6
- package/dist/src/html/bindings.js +3 -3
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/html/parser.d.ts +1 -1
- package/dist/src/html/parser.js +1 -1
- package/dist/src/html/template.js +24 -10
- package/dist/src/html/template.js.map +1 -1
- package/dist/src/index.d.ts +8 -4
- package/dist/src/index.js +13 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/page.d.ts +62 -28
- package/dist/src/page.js +5 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/resource.js +2 -1
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/base.d.ts +65 -0
- package/dist/src/router/base.js +168 -0
- package/dist/src/router/base.js.map +1 -0
- package/dist/src/router/manifest.js +75 -31
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +16 -2
- package/dist/src/router/match.js +41 -1
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.js +58 -9
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/static-runtime.d.ts +81 -0
- package/dist/src/static-runtime.js +209 -0
- package/dist/src/static-runtime.js.map +1 -0
- package/dist/src/vite/index.d.ts +31 -3
- package/dist/src/vite/index.js +49 -18
- package/dist/src/vite/index.js.map +1 -1
- package/docs/README.md +5 -8
- package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
- package/docs/en/00-the-mado-way.md +1 -1
- package/docs/en/01-quickstart.md +183 -0
- package/docs/en/10-pages-and-components.md +271 -0
- package/docs/en/11-templates-and-signals.md +211 -0
- package/docs/en/12-routing.md +229 -0
- package/docs/en/13-data.md +225 -0
- package/docs/en/14-forms.md +244 -0
- package/docs/en/15-static-snapshots.md +181 -0
- package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
- package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
- package/docs/en/{14-testing.md → 22-testing.md} +6 -5
- package/docs/en/23-cookbook.md +292 -0
- package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
- package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
- package/docs/en/40-llm-guide.md +776 -0
- package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
- package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
- package/docs/en/README.md +97 -27
- package/docs/recipes/nginx/Containerfile +26 -0
- package/docs/recipes/nginx/nginx.conf +42 -0
- package/llms.txt +246 -211
- package/package.json +17 -10
- package/scripts/bake.mjs +4 -568
- package/scripts/cli/generate.mjs +55 -5
- package/scripts/cli/help.mjs +11 -5
- package/scripts/cli/index.mjs +20 -4
- package/scripts/cli/init.mjs +7 -2
- package/scripts/cli/release.mjs +20 -68
- package/scripts/docs-lint.mjs +170 -0
- package/scripts/package-smoke.mjs +67 -9
- package/scripts/preview.mjs +95 -6
- package/scripts/size-budget.mjs +5 -1
- package/scripts/static/browser.mjs +654 -0
- package/scripts/static/discover.mjs +281 -0
- package/scripts/static/output.mjs +141 -0
- package/scripts/static/serialize.mjs +212 -0
- package/scripts/static/server.mjs +167 -0
- package/scripts/static.mjs +191 -0
- package/starters/default/README.md +57 -55
- package/starters/default/package.json +3 -9
- package/starters/default/src/app.routes.ts +20 -33
- package/starters/default/src/components/feature-card.component.ts +40 -0
- package/starters/default/src/components/live-counter.component.ts +57 -0
- package/starters/default/src/content/guides.ts +55 -0
- package/starters/default/src/main.ts +9 -13
- package/starters/default/src/pages/app.page.ts +41 -0
- package/starters/default/src/pages/guide.page.ts +40 -0
- package/starters/default/src/pages/home.page.ts +49 -0
- package/starters/default/src/pages/not-found.page.ts +18 -0
- package/starters/default/src/styles/document.css +39 -0
- package/starters/default/src/styles/reset.css +37 -0
- package/starters/default/src/styles/tokens.css +30 -0
- package/starters/default/src/styles.d.ts +3 -1
- package/starters/default/src/vite-env.d.ts +1 -1
- package/starters/default/vite.config.ts +14 -1
- package/starters/modular/README.md +142 -0
- package/starters/modular/index.html +13 -0
- package/starters/modular/package.json +30 -0
- package/starters/modular/src/app.routes.ts +39 -0
- package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
- package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
- package/starters/modular/src/main.ts +16 -0
- package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
- package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
- package/starters/modular/src/modules/home/not-found.page.ts +14 -0
- package/starters/modular/src/styles.d.ts +1 -0
- package/starters/modular/src/vite-env.d.ts +1 -0
- package/starters/modular/tsconfig.json +24 -0
- package/starters/modular/vite.config.ts +131 -0
- package/TODO.md +0 -79
- package/docs/en/01-routing.md +0 -152
- package/docs/en/02-project-layout.md +0 -124
- package/docs/en/03-static-bake.md +0 -249
- package/docs/en/04-ide-setup.md +0 -162
- package/docs/en/07-llm-pitfalls.md +0 -724
- package/docs/en/08-llm-zero-history-test.md +0 -56
- package/docs/en/09-shadow-vs-light-dom.md +0 -174
- package/docs/en/11-layouts.md +0 -113
- package/docs/en/12-auth-and-api.md +0 -124
- package/docs/en/16-bake-cookbook.md +0 -100
- package/docs/en/17-shadow-dom-forms.md +0 -192
- package/docs/fr/00-the-mado-way.md +0 -85
- package/docs/fr/01-routing.md +0 -120
- package/docs/fr/02-project-layout.md +0 -84
- package/docs/fr/03-static-bake.md +0 -289
- package/docs/fr/04-ide-setup.md +0 -162
- package/docs/fr/05-why-mado.md +0 -193
- package/docs/fr/06-for-backenders.md +0 -438
- package/docs/fr/07-llm-pitfalls.md +0 -625
- package/docs/fr/08-llm-zero-history-test.md +0 -38
- package/docs/fr/09-shadow-vs-light-dom.md +0 -65
- package/docs/fr/10-app-architecture.md +0 -138
- package/docs/fr/11-layouts.md +0 -47
- package/docs/fr/12-auth-and-api.md +0 -76
- package/docs/fr/13-deployment.md +0 -57
- package/docs/fr/14-testing.md +0 -41
- package/docs/fr/15-error-handling.md +0 -50
- package/docs/fr/16-bake-cookbook.md +0 -88
- package/docs/fr/17-shadow-dom-forms.md +0 -196
- package/docs/fr/18-api-freeze-map.md +0 -63
- package/docs/fr/19-reactivity-ordering.md +0 -97
- package/docs/fr/20-v1-stability.md +0 -88
- package/docs/fr/README.md +0 -27
- package/docs/ru/00-the-mado-way.md +0 -84
- package/docs/ru/01-routing.md +0 -119
- package/docs/ru/02-project-layout.md +0 -84
- package/docs/ru/03-static-bake.md +0 -250
- package/docs/ru/04-ide-setup.md +0 -144
- package/docs/ru/05-why-mado.md +0 -193
- package/docs/ru/06-for-backenders.md +0 -428
- package/docs/ru/07-llm-pitfalls.md +0 -624
- package/docs/ru/08-llm-zero-history-test.md +0 -57
- package/docs/ru/09-shadow-vs-light-dom.md +0 -63
- package/docs/ru/10-app-architecture.md +0 -152
- package/docs/ru/11-layouts.md +0 -47
- package/docs/ru/12-auth-and-api.md +0 -75
- package/docs/ru/13-deployment.md +0 -66
- package/docs/ru/14-testing.md +0 -50
- package/docs/ru/15-error-handling.md +0 -56
- package/docs/ru/16-bake-cookbook.md +0 -95
- package/docs/ru/17-shadow-dom-forms.md +0 -193
- package/docs/ru/18-api-freeze-map.md +0 -64
- package/docs/ru/19-reactivity-ordering.md +0 -95
- package/docs/ru/20-v1-stability.md +0 -82
- package/docs/ru/README.md +0 -25
- package/docs/uk/00-the-mado-way.md +0 -82
- package/docs/uk/01-routing.md +0 -76
- package/docs/uk/02-project-layout.md +0 -73
- package/docs/uk/03-static-bake.md +0 -48
- package/docs/uk/04-ide-setup.md +0 -26
- package/docs/uk/05-why-mado.md +0 -34
- package/docs/uk/06-for-backenders.md +0 -55
- package/docs/uk/07-llm-pitfalls.md +0 -145
- package/docs/uk/08-llm-zero-history-test.md +0 -34
- package/docs/uk/09-shadow-vs-light-dom.md +0 -58
- package/docs/uk/10-app-architecture.md +0 -97
- package/docs/uk/11-layouts.md +0 -47
- package/docs/uk/12-auth-and-api.md +0 -70
- package/docs/uk/13-deployment.md +0 -40
- package/docs/uk/14-testing.md +0 -34
- package/docs/uk/15-error-handling.md +0 -32
- package/docs/uk/16-bake-cookbook.md +0 -36
- package/docs/uk/17-shadow-dom-forms.md +0 -193
- package/docs/uk/18-api-freeze-map.md +0 -61
- package/docs/uk/19-reactivity-ordering.md +0 -95
- package/docs/uk/20-v1-stability.md +0 -83
- package/docs/uk/README.md +0 -27
- package/starters/default/src/modules/home/not-found.page.ts +0 -11
- /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
- /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
- /package/starters/{default → modular}/.editorconfig +0 -0
- /package/starters/{default → modular}/eslint.config.mjs +0 -0
- /package/starters/{default → modular}/public/favicon.svg +0 -0
- /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
- /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
- /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { extname, join, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { injectSnapshotMode } from "./serialize.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalise a Vite-style base into the canonical Mado form: `"/"` for the
|
|
9
|
+
* root, otherwise `"/prefix/"` (leading and trailing slash, no doubles).
|
|
10
|
+
* Mirrors src/router/base.ts.normalizeBase so the capture pipeline never
|
|
11
|
+
* disagrees with the runtime router.
|
|
12
|
+
*/
|
|
13
|
+
function normalizeBase(raw) {
|
|
14
|
+
if (!raw) return "/";
|
|
15
|
+
let s = String(raw).trim();
|
|
16
|
+
if (!s || s === "/") return "/";
|
|
17
|
+
if (!s.startsWith("/")) s = "/" + s;
|
|
18
|
+
if (!s.endsWith("/")) s = s + "/";
|
|
19
|
+
return s.replace(/\/+/g, "/");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Strip the active base prefix off a request pathname so the capture
|
|
24
|
+
* server can look it up against route records (which are always
|
|
25
|
+
* registered without a base).
|
|
26
|
+
*/
|
|
27
|
+
function stripBase(pathname, base) {
|
|
28
|
+
const b = normalizeBase(base);
|
|
29
|
+
if (!pathname) return "/";
|
|
30
|
+
if (b === "/") return pathname.startsWith("/") ? pathname : "/" + pathname;
|
|
31
|
+
if (pathname === b || pathname === b.slice(0, -1)) return "/";
|
|
32
|
+
if (pathname.startsWith(b)) {
|
|
33
|
+
const rest = pathname.slice(b.length - 1);
|
|
34
|
+
return rest || "/";
|
|
35
|
+
}
|
|
36
|
+
return pathname;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MIME = {
|
|
40
|
+
".html": "text/html; charset=utf-8",
|
|
41
|
+
".js": "text/javascript; charset=utf-8",
|
|
42
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
43
|
+
".css": "text/css; charset=utf-8",
|
|
44
|
+
".json": "application/json; charset=utf-8",
|
|
45
|
+
".svg": "image/svg+xml",
|
|
46
|
+
".ico": "image/x-icon",
|
|
47
|
+
".png": "image/png",
|
|
48
|
+
".jpg": "image/jpeg",
|
|
49
|
+
".jpeg": "image/jpeg",
|
|
50
|
+
".webp": "image/webp",
|
|
51
|
+
".woff2": "font/woff2",
|
|
52
|
+
".xml": "application/xml; charset=utf-8",
|
|
53
|
+
".map": "application/json; charset=utf-8",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Internal HTTP server that hosts the Vite build output during snapshot
|
|
58
|
+
* capture. It speaks the real deployment URL shape (the active Vite
|
|
59
|
+
* `base` is honoured for both static routes and asset lookups), so the
|
|
60
|
+
* runtime router sees the same `location.pathname` as production.
|
|
61
|
+
*/
|
|
62
|
+
export async function createStaticCaptureServer({ outDir, shellHtml, records, base = "/" }) {
|
|
63
|
+
const normalizedBase = normalizeBase(base);
|
|
64
|
+
const routeRecords = new Map(records.map((record) => [record.pathname, record]));
|
|
65
|
+
|
|
66
|
+
const server = createServer(async (req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
69
|
+
const browserPath = normalizePathname(url.pathname);
|
|
70
|
+
// The router uses BASE-FREE route pathnames, but assets in
|
|
71
|
+
// `out/assets/...` are served from the same base-relative URLs the
|
|
72
|
+
// production CDN exposes. Strip the active base before any record
|
|
73
|
+
// lookup or file resolution so both halves use one consistent
|
|
74
|
+
// pathname space.
|
|
75
|
+
const routePath = stripBase(browserPath, normalizedBase);
|
|
76
|
+
|
|
77
|
+
const record = routeRecords.get(routePath);
|
|
78
|
+
if (record) {
|
|
79
|
+
sendHtml(res, injectSnapshotMode(shellHtml, record));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const file = await resolveStaticFile(outDir, routePath);
|
|
84
|
+
if (file) {
|
|
85
|
+
const data = await readFile(file);
|
|
86
|
+
res.writeHead(200, {
|
|
87
|
+
"content-type": MIME[extname(file).toLowerCase()] ?? "application/octet-stream",
|
|
88
|
+
"cache-control": "no-store",
|
|
89
|
+
});
|
|
90
|
+
res.end(data);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// SPA fallback: any non-asset path falls back to the snapshot shell
|
|
95
|
+
// so client-side routing handles it. We use the original browser
|
|
96
|
+
// pathname (not the stripped one) inside `injectSnapshotMode` so
|
|
97
|
+
// that the runtime `location.pathname` matches production.
|
|
98
|
+
if (!extname(routePath)) {
|
|
99
|
+
sendHtml(
|
|
100
|
+
res,
|
|
101
|
+
injectSnapshotMode(shellHtml, { pathname: routePath, params: {} }),
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
res.writeHead(404, { "cache-control": "no-store" });
|
|
107
|
+
res.end("not found");
|
|
108
|
+
} catch (err) {
|
|
109
|
+
res.writeHead(500, {
|
|
110
|
+
"content-type": "text/plain; charset=utf-8",
|
|
111
|
+
"cache-control": "no-store",
|
|
112
|
+
});
|
|
113
|
+
res.end(String(err?.stack ?? err));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
118
|
+
server.once("error", rejectListen);
|
|
119
|
+
server.listen(0, "127.0.0.1", () => {
|
|
120
|
+
server.off("error", rejectListen);
|
|
121
|
+
resolveListen();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const address = server.address();
|
|
126
|
+
if (!address || typeof address === "string") {
|
|
127
|
+
throw new Error("[mado:static] failed to bind internal capture server.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
origin: `http://127.0.0.1:${address.port}`,
|
|
132
|
+
close: () => new Promise((resolveClose) => server.close(resolveClose)),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function sendHtml(res, html) {
|
|
137
|
+
res.writeHead(200, {
|
|
138
|
+
"content-type": "text/html; charset=utf-8",
|
|
139
|
+
"cache-control": "no-store",
|
|
140
|
+
});
|
|
141
|
+
res.end(html);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function resolveStaticFile(outDir, pathname) {
|
|
145
|
+
const raw = pathname === "/" ? "/index.html" : pathname;
|
|
146
|
+
const candidate = resolve(join(outDir, decodeURIComponent(raw)));
|
|
147
|
+
if (!candidate.startsWith(resolve(outDir) + sep) && candidate !== resolve(outDir)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const s = await stat(candidate);
|
|
152
|
+
if (s.isFile()) return candidate;
|
|
153
|
+
if (s.isDirectory()) {
|
|
154
|
+
const index = join(candidate, "index.html");
|
|
155
|
+
if ((await stat(index)).isFile()) return index;
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
/* not found */
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizePathname(pathname) {
|
|
164
|
+
if (!pathname.startsWith("/")) return `/${pathname}`;
|
|
165
|
+
if (pathname.length > 1 && pathname.endsWith("/")) return pathname.slice(0, -1);
|
|
166
|
+
return pathname;
|
|
167
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, writeSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { parseFlags } from "./_config.mjs";
|
|
7
|
+
import { discoverStaticRoutes } from "./static/discover.mjs";
|
|
8
|
+
import { createStaticCaptureServer } from "./static/server.mjs";
|
|
9
|
+
import { captureStaticRoutes } from "./static/browser.mjs";
|
|
10
|
+
import {
|
|
11
|
+
cleanupTemp,
|
|
12
|
+
dropBuildBridge,
|
|
13
|
+
prepareStaticOutput,
|
|
14
|
+
promoteCapturedRoutes,
|
|
15
|
+
promoteSpaShell,
|
|
16
|
+
writeCapturedRoutes,
|
|
17
|
+
writeStaticDeploymentFiles,
|
|
18
|
+
} from "./static/output.mjs";
|
|
19
|
+
|
|
20
|
+
const { flags } = parseFlags(process.argv.slice(2));
|
|
21
|
+
const projectRoot = resolve(process.cwd());
|
|
22
|
+
const outDir = resolve(
|
|
23
|
+
projectRoot,
|
|
24
|
+
typeof flags.out === "string" ? flags.out : "out",
|
|
25
|
+
);
|
|
26
|
+
const timeout = Number(flags.timeout ?? 30_000);
|
|
27
|
+
|
|
28
|
+
let tempRootForCleanup = null;
|
|
29
|
+
try {
|
|
30
|
+
console.log(`[static] artifact: ${outDir}`);
|
|
31
|
+
|
|
32
|
+
// Source the public origin and Vite base from (in order):
|
|
33
|
+
// --base-url, --base / MADO_SITE env, _mado/build.json (the bridge
|
|
34
|
+
// emitted by the @madojs/mado/vite plugin). Static routes REQUIRE
|
|
35
|
+
// a non-localhost public origin; we never invent one.
|
|
36
|
+
const buildMeta = readBuildMeta(outDir);
|
|
37
|
+
const site = pickSite({ flags, buildMeta });
|
|
38
|
+
const base = pickBase({ flags, buildMeta });
|
|
39
|
+
validateSite(site, "site");
|
|
40
|
+
|
|
41
|
+
// Ordering matters: discover and validate the manifest BEFORE we
|
|
42
|
+
// touch the deployed artefact. A manifest that throws (missing
|
|
43
|
+
// params, duplicate URL, ...) used to leave a half-prepared
|
|
44
|
+
// staging directory behind because we provisioned the temp tree
|
|
45
|
+
// before discovery; now the temp tree is created only after we know
|
|
46
|
+
// the inputs are sound.
|
|
47
|
+
const { records } = await discoverStaticRoutes({
|
|
48
|
+
projectRoot,
|
|
49
|
+
entry: typeof flags.entry === "string" ? flags.entry : undefined,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log(`[static] discovered ${records.length} static route(s)`);
|
|
53
|
+
|
|
54
|
+
if (records.length > 0 && !site) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"[mado:static] missing public origin for static routes.\n" +
|
|
57
|
+
"Provide one of:\n" +
|
|
58
|
+
" mado static --base-url https://your.site\n" +
|
|
59
|
+
" MADO_SITE=https://your.site mado static\n" +
|
|
60
|
+
" mado({ site: \"https://your.site\" }) in vite.config.ts",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { shellHtml, tempRoot, routesDir, stagedSpaPath } =
|
|
65
|
+
await prepareStaticOutput(outDir);
|
|
66
|
+
tempRootForCleanup = tempRoot;
|
|
67
|
+
|
|
68
|
+
const publicOrigin = site ?? "";
|
|
69
|
+
const baseUrl = publicOrigin
|
|
70
|
+
? joinSite(publicOrigin, base)
|
|
71
|
+
: "/";
|
|
72
|
+
|
|
73
|
+
if (records.length > 0) {
|
|
74
|
+
const server = await createStaticCaptureServer({
|
|
75
|
+
outDir,
|
|
76
|
+
shellHtml,
|
|
77
|
+
records,
|
|
78
|
+
base,
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
const captured = await captureStaticRoutes({
|
|
82
|
+
records,
|
|
83
|
+
serverOrigin: server.origin,
|
|
84
|
+
baseUrl,
|
|
85
|
+
base,
|
|
86
|
+
site: publicOrigin,
|
|
87
|
+
timeout,
|
|
88
|
+
browserChannel:
|
|
89
|
+
typeof flags["browser-channel"] === "string"
|
|
90
|
+
? flags["browser-channel"]
|
|
91
|
+
: undefined,
|
|
92
|
+
browserPath:
|
|
93
|
+
typeof flags["browser-path"] === "string"
|
|
94
|
+
? flags["browser-path"]
|
|
95
|
+
: undefined,
|
|
96
|
+
});
|
|
97
|
+
// First write everything to the temp staging tree, only then
|
|
98
|
+
// promote into `out/`. A capture failure aborts before any of the
|
|
99
|
+
// existing deployment files are touched.
|
|
100
|
+
await writeCapturedRoutes(routesDir, captured);
|
|
101
|
+
await promoteCapturedRoutes({ outDir, routesDir, captured });
|
|
102
|
+
console.log(`[static] captured ${captured.length} route snapshot(s)`);
|
|
103
|
+
} finally {
|
|
104
|
+
await server.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// SPA shell is promoted only after every route survived capture and
|
|
109
|
+
// promotion. On a re-run with broken pages this guarantees that the
|
|
110
|
+
// previous (working) `_mado/spa.html` is preserved.
|
|
111
|
+
await promoteSpaShell({ outDir, stagedSpaPath });
|
|
112
|
+
|
|
113
|
+
await writeStaticDeploymentFiles({
|
|
114
|
+
outDir,
|
|
115
|
+
records,
|
|
116
|
+
baseUrl,
|
|
117
|
+
site: publicOrigin,
|
|
118
|
+
base,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Drop the internal build bridge so the production artifact does not
|
|
122
|
+
// ship Vite's resolved view of the project (site, base, assetsDir).
|
|
123
|
+
await dropBuildBridge(outDir);
|
|
124
|
+
console.log(`[static] done`);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
// Defer the actual termination: `process.exit()` is synchronous and
|
|
127
|
+
// would race the awaited cleanup in the `finally` block, leaving the
|
|
128
|
+
// OS to garbage-collect a half-promoted staging directory. Setting
|
|
129
|
+
// `exitCode` lets Node finish the microtask queue (including the
|
|
130
|
+
// cleanup below) and then exit with the failure status.
|
|
131
|
+
writeSync(2, `${err?.stack ?? err}\n`);
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
} finally {
|
|
134
|
+
if (tempRootForCleanup) {
|
|
135
|
+
try {
|
|
136
|
+
await cleanupTemp(tempRootForCleanup);
|
|
137
|
+
} catch {
|
|
138
|
+
/* best effort */
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readBuildMeta(outDir) {
|
|
144
|
+
const file = join(outDir, "_mado/build.json");
|
|
145
|
+
if (!existsSync(file)) return null;
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function pickSite({ flags, buildMeta }) {
|
|
154
|
+
const fromFlag = flags["base-url"] ?? flags.site;
|
|
155
|
+
if (typeof fromFlag === "string" && fromFlag.length > 0) return fromFlag;
|
|
156
|
+
if (typeof process.env.MADO_SITE === "string" && process.env.MADO_SITE.length > 0) {
|
|
157
|
+
return process.env.MADO_SITE;
|
|
158
|
+
}
|
|
159
|
+
if (buildMeta?.site) return buildMeta.site;
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function pickBase({ flags, buildMeta }) {
|
|
164
|
+
const fromFlag = flags.base;
|
|
165
|
+
if (typeof fromFlag === "string" && fromFlag.length > 0) return fromFlag;
|
|
166
|
+
if (buildMeta?.base) return buildMeta.base;
|
|
167
|
+
return "/";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validateSite(site, label) {
|
|
171
|
+
if (!site) return;
|
|
172
|
+
let u;
|
|
173
|
+
try {
|
|
174
|
+
u = new URL(site);
|
|
175
|
+
} catch {
|
|
176
|
+
throw new Error(`[mado:static] ${label} is not a valid URL: ${site}`);
|
|
177
|
+
}
|
|
178
|
+
if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1") {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`[mado:static] ${label} cannot point at localhost (${site}). ` +
|
|
181
|
+
"Set the public origin via --base-url or mado({ site }) in vite.config.ts.",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function joinSite(site, base) {
|
|
187
|
+
const left = site.replace(/\/+$/, "");
|
|
188
|
+
const right = (base ?? "/").startsWith("/") ? base : `/${base ?? "/"}`;
|
|
189
|
+
const merged = `${left}${right}`;
|
|
190
|
+
return merged.endsWith("/") ? merged.slice(0, -1) : merged;
|
|
191
|
+
}
|
|
@@ -1,74 +1,76 @@
|
|
|
1
|
-
# Mado
|
|
1
|
+
# Mado universal starter
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
A calm native-first web framework for sites and apps. This starter
|
|
4
|
+
proves the whole promise in ~15 files:
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
- one Shadow Component (`feature-card`) shared by the static landing
|
|
7
|
+
and the live SPA,
|
|
8
|
+
- one component model end-to-end,
|
|
9
|
+
- a build-time seed for a dynamic static route (`/guide/:slug`),
|
|
10
|
+
- a private SPA route (`/app`) that falls back through `_mado/spa.html`,
|
|
11
|
+
- per-route head metadata, sitemap, canonical and `og:url`
|
|
12
|
+
auto-fallback.
|
|
13
|
+
|
|
14
|
+
## Run it
|
|
7
15
|
|
|
8
16
|
```bash
|
|
9
17
|
npm install
|
|
10
|
-
npm run dev
|
|
11
|
-
npm run
|
|
18
|
+
npm run dev # Vite dev server
|
|
19
|
+
npm run build # Vite production SPA build
|
|
20
|
+
npm run release # vite build + browser-rendered snapshots → out/
|
|
21
|
+
npm run preview # serve out/ like a real static host
|
|
12
22
|
```
|
|
13
23
|
|
|
14
|
-
`
|
|
24
|
+
`mado release` requires a public origin so it can build absolute
|
|
25
|
+
canonical URLs. Set it once in `vite.config.ts`:
|
|
15
26
|
|
|
16
|
-
|
|
27
|
+
```ts
|
|
28
|
+
mado({ site: "https://your-app.example" })
|
|
29
|
+
```
|
|
17
30
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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/
|
|
31
|
+
…or override per environment:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
mado release --base-url https://staging.example
|
|
35
|
+
MADO_SITE=https://staging.example mado release
|
|
39
36
|
```
|
|
40
37
|
|
|
41
|
-
##
|
|
38
|
+
## File map
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
src/
|
|
42
|
+
main.ts # mounts the router into #app
|
|
43
|
+
app.routes.ts # the URL → page table
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
states.
|
|
49
|
-
- Reusable leaf components keep their own styles in ``css`...` `` inside
|
|
50
|
-
`component()` options.
|
|
45
|
+
pages/
|
|
46
|
+
home.page.ts # public, static
|
|
47
|
+
guide.page.ts # dynamic static (/guide/:slug)
|
|
48
|
+
app.page.ts # SPA-only interactive route
|
|
49
|
+
not-found.page.ts # 404 (SPA fallback)
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
components/
|
|
52
|
+
feature-card.component.ts # shared by landing + app
|
|
53
|
+
live-counter.component.ts # reactivity demo
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
content/
|
|
56
|
+
guides.ts # browser-safe content module
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
58
|
+
styles/
|
|
59
|
+
tokens.css # design tokens (custom properties)
|
|
60
|
+
reset.css # tiny modern reset
|
|
61
|
+
document.css # light-DOM document layout
|
|
66
62
|
```
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
module route map by hand.
|
|
64
|
+
## Going further
|
|
70
65
|
|
|
71
|
-
|
|
66
|
+
If you need module boundaries, layouts, guards, an auth shell, an HTTP
|
|
67
|
+
client and a Stripe-style billing example, scaffold the modular
|
|
68
|
+
starter instead:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
mado init my-app --starter modular
|
|
72
|
+
```
|
|
72
73
|
|
|
73
|
-
The
|
|
74
|
-
|
|
74
|
+
The modular starter is the reference architecture for long-lived
|
|
75
|
+
business apps. Both starters target the same Mado runtime — they only
|
|
76
|
+
differ in how much structure they pre-create for you.
|
|
@@ -5,25 +5,19 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "mado dev",
|
|
8
|
-
"build": "mado
|
|
8
|
+
"build": "mado build",
|
|
9
9
|
"typecheck": "mado typecheck",
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "mado typecheck",
|
|
11
11
|
"release": "mado release",
|
|
12
12
|
"preview": "mado preview",
|
|
13
|
-
"lint": "eslint \"src/**/*.ts\"",
|
|
14
|
-
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
15
13
|
"new": "mado new"
|
|
16
14
|
},
|
|
17
15
|
"dependencies": {
|
|
18
16
|
"@madojs/mado": "__MADOJS_VERSION__"
|
|
19
17
|
},
|
|
20
18
|
"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
19
|
"lightningcss": "^1.32.0",
|
|
20
|
+
"playwright-core": "^1.47.0",
|
|
27
21
|
"typescript": "^5.5.0",
|
|
28
22
|
"vite": "^8.0.16"
|
|
29
23
|
}
|
|
@@ -1,39 +1,26 @@
|
|
|
1
|
-
// Single source of truth for the
|
|
1
|
+
// Single source of truth for the app's URL → page mapping.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
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";
|
|
3
|
+
// Mado has no "file-based routes". You list pages explicitly, in order
|
|
4
|
+
// of specificity, and the router takes care of code-splitting via the
|
|
5
|
+
// dynamic imports below.
|
|
6
|
+
import { routes } from "@madojs/mado";
|
|
18
7
|
|
|
19
8
|
export const manifest = {
|
|
20
|
-
// Public landing
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"*": () => import("./modules/home/not-found.page"),
|
|
9
|
+
// Public landing — `static: true` makes `mado release` snapshot this
|
|
10
|
+
// route into `out/index.html` so search engines see a fully rendered
|
|
11
|
+
// document without running JS.
|
|
12
|
+
"/": () => import("./pages/home.page"),
|
|
13
|
+
// Dynamic static route: `mado static` calls `paths()` at build time,
|
|
14
|
+
// captures one HTML file per slug, and seeds `initialData` so the
|
|
15
|
+
// first client render does not re-fetch what the snapshot already
|
|
16
|
+
// shows.
|
|
17
|
+
"/guide/:slug": () => import("./pages/guide.page"),
|
|
18
|
+
// SPA-only route: deliberately non-static. Lives in `_mado/spa.html`
|
|
19
|
+
// at deploy time and is served whenever the CDN cannot find a static
|
|
20
|
+
// document.
|
|
21
|
+
"/app": () => import("./pages/app.page"),
|
|
22
|
+
// 404 — matched after every literal pattern.
|
|
23
|
+
"*": () => import("./pages/not-found.page"),
|
|
37
24
|
};
|
|
38
25
|
|
|
39
26
|
export default routes(manifest);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `feature-card` is a leaf component shared by the home page (in the
|
|
2
|
+
// static snapshot) and by `app.page.ts` (in the live SPA). The same
|
|
3
|
+
// open-shadow custom element runs in both contexts — that is the core
|
|
4
|
+
// promise of the universal model.
|
|
5
|
+
import { component, css, html } from "@madojs/mado";
|
|
6
|
+
|
|
7
|
+
component(
|
|
8
|
+
"feature-card",
|
|
9
|
+
(ctx) => {
|
|
10
|
+
const title = ctx.attr("title", "");
|
|
11
|
+
return () => html`
|
|
12
|
+
<article class="card">
|
|
13
|
+
<h3>${() => title()}</h3>
|
|
14
|
+
<p><slot></slot></p>
|
|
15
|
+
</article>
|
|
16
|
+
`;
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
styles: css`
|
|
20
|
+
:host {
|
|
21
|
+
display: block;
|
|
22
|
+
}
|
|
23
|
+
.card {
|
|
24
|
+
padding: 1.25rem 1.5rem;
|
|
25
|
+
border: 1px solid var(--mado-border);
|
|
26
|
+
border-radius: var(--mado-radius);
|
|
27
|
+
background: var(--mado-surface);
|
|
28
|
+
}
|
|
29
|
+
h3 {
|
|
30
|
+
margin: 0 0 0.5rem;
|
|
31
|
+
font-size: 1.1rem;
|
|
32
|
+
font-weight: 600;
|
|
33
|
+
}
|
|
34
|
+
p {
|
|
35
|
+
margin: 0;
|
|
36
|
+
color: var(--mado-fg-muted);
|
|
37
|
+
}
|
|
38
|
+
`,
|
|
39
|
+
},
|
|
40
|
+
);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// `live-counter` showcases reactivity: signal-driven text, a click
|
|
2
|
+
// handler that mutates the signal, and an open shadow root that
|
|
3
|
+
// participates in the static snapshot via Declarative Shadow DOM.
|
|
4
|
+
import { component, css, html, signal } from "@madojs/mado";
|
|
5
|
+
|
|
6
|
+
component(
|
|
7
|
+
"live-counter",
|
|
8
|
+
() => {
|
|
9
|
+
const count = signal(0);
|
|
10
|
+
return () => html`
|
|
11
|
+
<div class="counter">
|
|
12
|
+
<button
|
|
13
|
+
class="bump"
|
|
14
|
+
@click=${() => count.update((n) => n + 1)}
|
|
15
|
+
aria-label="Increment"
|
|
16
|
+
>
|
|
17
|
+
+
|
|
18
|
+
</button>
|
|
19
|
+
<span class="value">${() => count()}</span>
|
|
20
|
+
</div>
|
|
21
|
+
`;
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
styles: css`
|
|
25
|
+
:host {
|
|
26
|
+
display: inline-block;
|
|
27
|
+
}
|
|
28
|
+
.counter {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 0.75rem;
|
|
32
|
+
padding: 0.5rem 0.75rem;
|
|
33
|
+
border: 1px solid var(--mado-border);
|
|
34
|
+
border-radius: var(--mado-radius);
|
|
35
|
+
background: var(--mado-surface);
|
|
36
|
+
}
|
|
37
|
+
.bump {
|
|
38
|
+
font: inherit;
|
|
39
|
+
width: 2rem;
|
|
40
|
+
height: 2rem;
|
|
41
|
+
border-radius: 999px;
|
|
42
|
+
border: 1px solid var(--mado-border);
|
|
43
|
+
background: var(--mado-bg);
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
}
|
|
46
|
+
.bump:hover {
|
|
47
|
+
background: var(--mado-surface-hover);
|
|
48
|
+
}
|
|
49
|
+
.value {
|
|
50
|
+
font-variant-numeric: tabular-nums;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
min-width: 2ch;
|
|
53
|
+
text-align: right;
|
|
54
|
+
}
|
|
55
|
+
`,
|
|
56
|
+
},
|
|
57
|
+
);
|