@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.
Files changed (219) hide show
  1. package/AGENTS.md +24 -26
  2. package/CHANGELOG.md +98 -0
  3. package/README.md +18 -45
  4. package/TODO.md +52 -48
  5. package/dist/src/component.d.ts +2 -1
  6. package/dist/src/component.js +5 -2
  7. package/dist/src/component.js.map +1 -1
  8. package/dist/src/each.d.ts +1 -1
  9. package/dist/src/each.js +1 -1
  10. package/dist/src/each.js.map +1 -1
  11. package/dist/src/html/template.js +10 -0
  12. package/dist/src/html/template.js.map +1 -1
  13. package/dist/src/index.d.ts +11 -6
  14. package/dist/src/index.js +5 -3
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/lazy.d.ts +1 -1
  17. package/dist/src/lazy.js +1 -1
  18. package/dist/src/lazy.js.map +1 -1
  19. package/dist/src/page.d.ts +17 -21
  20. package/dist/src/page.js +7 -12
  21. package/dist/src/page.js.map +1 -1
  22. package/dist/src/router/manifest.d.ts +1 -1
  23. package/dist/src/router/manifest.js +21 -13
  24. package/dist/src/router/manifest.js.map +1 -1
  25. package/dist/src/router/match.d.ts +2 -2
  26. package/dist/src/router/match.js +3 -3
  27. package/dist/src/router/match.js.map +1 -1
  28. package/dist/src/router/navigation.js +1 -1
  29. package/dist/src/router/navigation.js.map +1 -1
  30. package/dist/src/vite/index.d.ts +10 -0
  31. package/dist/src/vite/index.js +33 -0
  32. package/dist/src/vite/index.js.map +1 -0
  33. package/docs/en/00-the-mado-way.md +25 -12
  34. package/docs/en/01-routing.md +90 -142
  35. package/docs/en/02-project-layout.md +59 -52
  36. package/docs/en/03-static-bake.md +5 -6
  37. package/docs/en/05-why-mado.md +6 -6
  38. package/docs/en/06-for-backenders.md +18 -22
  39. package/docs/en/08-llm-zero-history-test.md +9 -14
  40. package/docs/en/09-shadow-vs-light-dom.md +28 -36
  41. package/docs/en/10-app-architecture.md +158 -96
  42. package/docs/en/11-layouts.md +22 -24
  43. package/docs/en/12-auth-and-api.md +89 -182
  44. package/docs/en/13-deployment.md +25 -26
  45. package/docs/en/14-testing.md +4 -4
  46. package/docs/en/16-bake-cookbook.md +17 -10
  47. package/docs/en/18-api-freeze-map.md +6 -4
  48. package/docs/en/20-v1-stability.md +1 -1
  49. package/docs/fr/00-the-mado-way.md +55 -90
  50. package/docs/fr/01-routing.md +70 -152
  51. package/docs/fr/02-project-layout.md +74 -48
  52. package/docs/fr/03-static-bake.md +1 -1
  53. package/docs/fr/05-why-mado.md +6 -6
  54. package/docs/fr/06-for-backenders.md +7 -7
  55. package/docs/fr/08-llm-zero-history-test.md +21 -48
  56. package/docs/fr/09-shadow-vs-light-dom.md +43 -162
  57. package/docs/fr/10-app-architecture.md +110 -33
  58. package/docs/fr/11-layouts.md +24 -12
  59. package/docs/fr/12-auth-and-api.md +63 -22
  60. package/docs/fr/13-deployment.md +30 -12
  61. package/docs/fr/14-testing.md +1 -1
  62. package/docs/fr/16-bake-cookbook.md +57 -4
  63. package/docs/fr/18-api-freeze-map.md +1 -1
  64. package/docs/fr/20-v1-stability.md +1 -1
  65. package/docs/recipes/nginx/README.md +13 -0
  66. package/docs/ru/00-the-mado-way.md +53 -75
  67. package/docs/ru/01-routing.md +68 -143
  68. package/docs/ru/02-project-layout.md +75 -48
  69. package/docs/ru/03-static-bake.md +2 -2
  70. package/docs/ru/05-why-mado.md +6 -6
  71. package/docs/ru/06-for-backenders.md +7 -7
  72. package/docs/ru/08-llm-zero-history-test.md +9 -14
  73. package/docs/ru/09-shadow-vs-light-dom.md +43 -178
  74. package/docs/ru/10-app-architecture.md +115 -63
  75. package/docs/ru/11-layouts.md +24 -24
  76. package/docs/ru/12-auth-and-api.md +57 -35
  77. package/docs/ru/13-deployment.md +19 -13
  78. package/docs/ru/14-testing.md +1 -1
  79. package/docs/ru/16-bake-cookbook.md +48 -8
  80. package/docs/ru/18-api-freeze-map.md +5 -3
  81. package/docs/ru/20-v1-stability.md +1 -1
  82. package/docs/uk/00-the-mado-way.md +70 -44
  83. package/docs/uk/01-routing.md +41 -47
  84. package/docs/uk/02-project-layout.md +68 -41
  85. package/docs/uk/03-static-bake.md +1 -2
  86. package/docs/uk/06-for-backenders.md +3 -3
  87. package/docs/uk/08-llm-zero-history-test.md +22 -24
  88. package/docs/uk/09-shadow-vs-light-dom.md +37 -86
  89. package/docs/uk/10-app-architecture.md +72 -31
  90. package/docs/uk/11-layouts.md +25 -12
  91. package/docs/uk/12-auth-and-api.md +58 -22
  92. package/docs/uk/13-deployment.md +4 -3
  93. package/docs/uk/14-testing.md +1 -1
  94. package/docs/uk/18-api-freeze-map.md +1 -1
  95. package/docs/uk/20-v1-stability.md +1 -1
  96. package/llms.txt +14 -15
  97. package/package.json +18 -11
  98. package/scripts/_config.mjs +15 -161
  99. package/scripts/bake.mjs +71 -58
  100. package/scripts/cli/generate.mjs +348 -0
  101. package/scripts/cli/help.mjs +27 -0
  102. package/scripts/cli/index.mjs +79 -0
  103. package/scripts/cli/init.mjs +153 -0
  104. package/scripts/cli/release.mjs +152 -0
  105. package/scripts/cli/run.mjs +96 -0
  106. package/scripts/cli.mjs +2 -560
  107. package/scripts/package-smoke.mjs +4 -1
  108. package/scripts/preview.mjs +17 -61
  109. package/scripts/size-budget.mjs +5 -2
  110. package/scripts/vite.default.mjs +11 -0
  111. package/starters/default/.editorconfig +12 -0
  112. package/starters/default/README.md +74 -0
  113. package/starters/default/eslint.config.mjs +256 -0
  114. package/starters/default/index.html +13 -0
  115. package/starters/default/package.json +30 -0
  116. package/starters/default/public/favicon.svg +4 -0
  117. package/starters/default/src/app.routes.ts +39 -0
  118. package/starters/default/src/layouts/app-shell.layout.ts +35 -0
  119. package/starters/default/src/layouts/auth-shell.layout.ts +17 -0
  120. package/starters/default/src/main.ts +16 -0
  121. package/starters/default/src/modules/auth/_contracts/auth-api.types.ts +17 -0
  122. package/starters/default/src/modules/auth/auth.connector.ts +45 -0
  123. package/starters/default/src/modules/auth/auth.guard.ts +22 -0
  124. package/starters/default/src/modules/auth/auth.public.ts +9 -0
  125. package/starters/default/src/modules/auth/auth.routes.ts +8 -0
  126. package/starters/default/src/modules/auth/auth.service.ts +71 -0
  127. package/starters/default/src/modules/auth/auth.types.ts +15 -0
  128. package/starters/default/src/modules/auth/login.page.ts +62 -0
  129. package/starters/default/src/modules/billing/_contracts/stripe.types.ts +17 -0
  130. package/starters/default/src/modules/billing/api/stripe.connector.ts +71 -0
  131. package/starters/default/src/modules/billing/billing.public.ts +5 -0
  132. package/starters/default/src/modules/billing/billing.routes.ts +9 -0
  133. package/starters/default/src/modules/billing/billing.types.ts +15 -0
  134. package/starters/default/src/modules/billing/components/invoice-status-badge.component.ts +43 -0
  135. package/starters/default/src/modules/billing/data/invoices.resource.ts +35 -0
  136. package/starters/default/src/modules/billing/pages/invoice-detail.page.ts +70 -0
  137. package/starters/default/src/modules/billing/pages/invoices-list.page.ts +73 -0
  138. package/starters/default/src/modules/home/home.page.ts +34 -0
  139. package/starters/default/src/modules/home/not-found.page.ts +11 -0
  140. package/starters/default/src/shared/http/http-client.ts +86 -0
  141. package/starters/default/src/shared/http/http-error.ts +37 -0
  142. package/starters/default/src/shared/http/interceptors.ts +59 -0
  143. package/starters/default/src/shared/lib/format-date.ts +19 -0
  144. package/starters/default/src/shared/styles/content.css +70 -0
  145. package/starters/default/src/shared/styles/reset.css +32 -0
  146. package/starters/default/src/shared/styles/shell.css +57 -0
  147. package/starters/default/src/shared/styles/tokens.css +44 -0
  148. package/starters/default/src/shared/ui/x-button.component.ts +49 -0
  149. package/starters/default/src/shared/ui/x-spinner.component.ts +22 -0
  150. package/starters/default/src/styles.d.ts +1 -0
  151. package/starters/default/src/vite-env.d.ts +1 -0
  152. package/starters/default/tsconfig.json +24 -0
  153. package/starters/default/vite.config.ts +9 -0
  154. package/MADO_V1_PLAN.md +0 -179
  155. package/ROADMAP.md +0 -178
  156. package/dist/src/html.d.ts +0 -18
  157. package/dist/src/html.js +0 -17
  158. package/dist/src/html.js.map +0 -1
  159. package/dist/src/router.d.ts +0 -13
  160. package/dist/src/router.js +0 -13
  161. package/dist/src/router.js.map +0 -1
  162. package/scripts/bundle.mjs +0 -212
  163. package/scripts/llm-zero-history-smoke.mjs +0 -93
  164. package/scripts/new.mjs +0 -80
  165. package/scripts/showcase-regression.mjs +0 -392
  166. package/server/serve.mjs +0 -455
  167. package/starters/admin/README.md +0 -63
  168. package/starters/admin/index.html +0 -28
  169. package/starters/admin/mado.config.json +0 -22
  170. package/starters/admin/package.json +0 -24
  171. package/starters/admin/public/favicon.svg +0 -4
  172. package/starters/admin/src/components/x-button.ts +0 -82
  173. package/starters/admin/src/components/x-input.ts +0 -105
  174. package/starters/admin/src/layouts/app.ts +0 -101
  175. package/starters/admin/src/layouts/auth.ts +0 -41
  176. package/starters/admin/src/lib/api.ts +0 -184
  177. package/starters/admin/src/lib/auth.ts +0 -83
  178. package/starters/admin/src/main.ts +0 -15
  179. package/starters/admin/src/pages/admin/dashboard.ts +0 -48
  180. package/starters/admin/src/pages/admin/order-detail.ts +0 -80
  181. package/starters/admin/src/pages/admin/orders.ts +0 -117
  182. package/starters/admin/src/pages/home.ts +0 -34
  183. package/starters/admin/src/pages/login.ts +0 -70
  184. package/starters/admin/src/pages/not-found.ts +0 -12
  185. package/starters/admin/src/routes.ts +0 -40
  186. package/starters/admin/src/styles/global.ts +0 -86
  187. package/starters/admin/tsconfig.json +0 -15
  188. package/starters/crud/README.md +0 -33
  189. package/starters/crud/index.html +0 -28
  190. package/starters/crud/mado.config.json +0 -20
  191. package/starters/crud/package.json +0 -24
  192. package/starters/crud/src/components/app-shell.ts +0 -56
  193. package/starters/crud/src/components/ticket-detail.ts +0 -33
  194. package/starters/crud/src/components/ticket-form.ts +0 -69
  195. package/starters/crud/src/components/ticket-list.ts +0 -66
  196. package/starters/crud/src/lib/api.ts +0 -76
  197. package/starters/crud/src/main.ts +0 -9
  198. package/starters/crud/src/pages/home.ts +0 -34
  199. package/starters/crud/src/pages/not-found.ts +0 -12
  200. package/starters/crud/src/pages/ticket-detail.ts +0 -7
  201. package/starters/crud/src/pages/ticket-new.ts +0 -7
  202. package/starters/crud/src/pages/tickets.ts +0 -7
  203. package/starters/crud/src/routes.ts +0 -11
  204. package/starters/crud/src/styles/global.ts +0 -155
  205. package/starters/crud/tsconfig.json +0 -15
  206. package/starters/minimal/README.md +0 -21
  207. package/starters/minimal/index.html +0 -28
  208. package/starters/minimal/mado.config.json +0 -20
  209. package/starters/minimal/package.json +0 -24
  210. package/starters/minimal/src/components/app-counter.ts +0 -31
  211. package/starters/minimal/src/main.ts +0 -9
  212. package/starters/minimal/src/pages/home.ts +0 -35
  213. package/starters/minimal/src/pages/not-found.ts +0 -14
  214. package/starters/minimal/src/routes.ts +0 -8
  215. package/starters/minimal/src/styles/global.ts +0 -60
  216. package/starters/minimal/tsconfig.json +0 -15
  217. package/templates/page-detail.ts +0 -63
  218. package/templates/page-form.ts +0 -94
  219. package/templates/page-list.ts +0 -79
package/server/serve.mjs DELETED
@@ -1,455 +0,0 @@
1
- // Tiny static server on node:http with dev features (ETag + HMR through SSE).
2
- // No dependencies.
3
- //
4
- // PORT=5173 node server/serve.mjs
5
- // NO_HMR=1 node server/serve.mjs # disable HMR
6
- // node server/serve.mjs basic # mount examples/basic/ at /
7
- // EXAMPLE=showcase node server/serve.mjs # mount examples/showcase/ at /
8
- //
9
- // Without EXAMPLE, / serves examples/index.html when running inside the Mado
10
- // repository, or ./index.html when running inside a generated app.
11
- // With EXAMPLE, all extensionless and /index.html requests fall back to
12
- // examples/<EXAMPLE>/index.html so the client router works from root, just
13
- // like a production SPA deploy.
14
-
15
- import { createServer, request as httpRequest } from "node:http";
16
- import { request as httpsRequest } from "node:https";
17
- import { readFile, readdir, readFile as readFileAsync, stat } from "node:fs/promises";
18
- import { watch, existsSync, readFileSync } from "node:fs";
19
- import { extname, join, resolve, sep } from "node:path";
20
- import { createHash } from "node:crypto";
21
-
22
- const ROOT = resolve(process.cwd());
23
-
24
- // Optional mado.config.json — used for dev.proxy and dev.port. Read with a
25
- // hand-rolled JSON parse to avoid a circular dep with scripts/_config.mjs
26
- // (this server is launched from cli.mjs and runs in its own Node process).
27
- const CONFIG = (() => {
28
- try {
29
- const file = join(ROOT, "mado.config.json");
30
- if (!existsSync(file)) return {};
31
- return JSON.parse(readFileSync(file, "utf8")) ?? {};
32
- } catch {
33
- return {};
34
- }
35
- })();
36
- const PROXY_RULES = Object.entries(CONFIG.dev?.proxy ?? {}); // [["/api", "http://localhost:3000"], ...]
37
-
38
- // Tiny argv parser. Supports --flag, --flag=value, --flag value. The first
39
- // non-flag positional is the EXAMPLE name (legacy behavior). Anything that
40
- // looks like a flag is consumed before we pick the positional, so calls like
41
- // `mado dev -- --host 127.0.0.1` work as documented.
42
- function parseArgs(argv) {
43
- const flags = {};
44
- const positional = [];
45
- for (let i = 0; i < argv.length; i++) {
46
- const a = argv[i];
47
- if (a === "--") continue;
48
- if (a.startsWith("--")) {
49
- const eq = a.indexOf("=");
50
- if (eq >= 0) {
51
- flags[a.slice(2, eq)] = a.slice(eq + 1);
52
- } else {
53
- const name = a.slice(2);
54
- const next = argv[i + 1];
55
- if (next !== undefined && !next.startsWith("-")) {
56
- flags[name] = next;
57
- i++;
58
- } else {
59
- flags[name] = true;
60
- }
61
- }
62
- } else {
63
- positional.push(a);
64
- }
65
- }
66
- return { flags, positional };
67
- }
68
-
69
- const { flags: CLI_FLAGS, positional: CLI_POSITIONAL } = parseArgs(
70
- process.argv.slice(2),
71
- );
72
-
73
- const PORT = Number(CLI_FLAGS.port ?? process.env.PORT ?? CONFIG.dev?.port ?? 5173);
74
- // HOST is opt-in. Default to "localhost" (loopback) which is friendlier in
75
- // sandboxes that disallow binding 0.0.0.0 (EPERM on listen). Users can opt
76
- // into LAN exposure with `mado dev --host 0.0.0.0` or HOST=0.0.0.0.
77
- const HOST = String(CLI_FLAGS.host ?? process.env.HOST ?? CONFIG.dev?.host ?? "localhost");
78
- const HMR = CLI_FLAGS.hmr !== false && process.env.NO_HMR !== "1";
79
-
80
- const EXAMPLE = CLI_POSITIONAL[0] ?? process.env.MADO_EXAMPLE ?? process.env.EXAMPLE ?? "";
81
- const EXAMPLE_DIR = EXAMPLE
82
- ? resolve(join(ROOT, "examples", EXAMPLE))
83
- : "";
84
- const EXAMPLE_INDEX = EXAMPLE ? join(EXAMPLE_DIR, "index.html") : "";
85
- const EXAMPLES_INDEX = join(ROOT, "examples", "index.html");
86
- const APP_INDEX = join(ROOT, "index.html");
87
- const DEFAULT_INDEX = existsSync(EXAMPLES_INDEX) ? EXAMPLES_INDEX : APP_INDEX;
88
-
89
- if (EXAMPLE) {
90
- if (!existsSync(EXAMPLE_INDEX)) {
91
- console.error(
92
- `[serve] EXAMPLE=${EXAMPLE}: file not found: ${EXAMPLE_INDEX}`,
93
- );
94
- process.exit(1);
95
- }
96
- }
97
-
98
- const MIME = {
99
- ".html": "text/html; charset=utf-8",
100
- ".js": "text/javascript; charset=utf-8",
101
- ".mjs": "text/javascript; charset=utf-8",
102
- ".css": "text/css; charset=utf-8",
103
- ".json": "application/json; charset=utf-8",
104
- ".svg": "image/svg+xml",
105
- ".ico": "image/x-icon",
106
- ".map": "application/json; charset=utf-8",
107
- };
108
-
109
- // ---------- HMR through Server-Sent Events ----------
110
- //
111
- // Open SSE connections live in a Set. Any file change broadcasts "reload".
112
- // The client reloads the page. This is deliberately full reload rather than
113
- // true HMR: we do not need preserved state, and the behavior stays simple.
114
-
115
- const sseClients = new Set();
116
-
117
- function broadcast(event, data) {
118
- for (const res of sseClients) {
119
- res.write(`event: ${event}\ndata: ${data}\n\n`);
120
- }
121
- }
122
-
123
- if (HMR) {
124
- // Debounce reload: tsc -w often changes several files in a row.
125
- let timer = null;
126
- const trigger = () => {
127
- clearTimeout(timer);
128
- timer = setTimeout(() => {
129
- console.log(`[hmr] reload ${sseClients.size} client(s)`);
130
- broadcast("reload", Date.now());
131
- }, 80);
132
- };
133
-
134
- for (const dir of ["dist", "examples"]) {
135
- try {
136
- watch(join(ROOT, dir), { recursive: true }, trigger);
137
- } catch {
138
- /* dir may not exist on startup */
139
- }
140
- }
141
- }
142
-
143
- const HMR_CLIENT = `
144
- // Mado HMR client (auto-injected by serve.mjs)
145
- (() => {
146
- if (window.__madoHmr) return;
147
- window.__madoHmr = true;
148
- const es = new EventSource('/__hmr');
149
- es.addEventListener('reload', () => location.reload());
150
- es.addEventListener('error', () => {
151
- // The server may be gone; try reconnecting after 1s.
152
- setTimeout(() => location.reload(), 1000);
153
- });
154
- })();
155
- `.trim();
156
-
157
- // ---------- Server ----------
158
-
159
- const server = createServer(async (req, res) => {
160
- const started = Date.now();
161
- let pathname = "/";
162
- let reason = "";
163
- res.on("finish", () => {
164
- const ms = Date.now() - started;
165
- const type = res.getHeader("content-type") ?? "-";
166
- const suffix = reason ? ` ${reason}` : "";
167
- console.log(
168
- `[serve] ${req.method ?? "GET"} ${pathname} ${res.statusCode} ${ms}ms ${type}${suffix}`,
169
- );
170
- });
171
-
172
- try {
173
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
174
- pathname = decodeURIComponent(url.pathname);
175
-
176
- // Dev proxy: forward matching prefixes to an upstream backend, so the
177
- // browser can reach the SPA and the API on a single origin without CORS.
178
- const proxyRule = PROXY_RULES.find(([prefix]) => pathname.startsWith(prefix));
179
- if (proxyRule) {
180
- const [prefix, upstream] = proxyRule;
181
- await proxyForward({ req, res, prefix, upstream, pathname, search: url.search });
182
- reason = `proxy → ${upstream}`;
183
- return;
184
- }
185
-
186
- // SSE endpoint for HMR.
187
- if (pathname === "/__hmr") {
188
- res.writeHead(200, {
189
- "content-type": "text/event-stream",
190
- "cache-control": "no-cache",
191
- connection: "keep-alive",
192
- });
193
- res.write("retry: 1000\n\n");
194
- sseClients.add(res);
195
- console.log(`[hmr] client connected (${sseClients.size})`);
196
- req.on("close", () => {
197
- sseClients.delete(res);
198
- console.log(`[hmr] client disconnected (${sseClients.size})`);
199
- });
200
- return;
201
- }
202
-
203
- // A mounted example owns root and SPA fallback. Otherwise serve the
204
- // examples index page.
205
- const fallbackIndex = EXAMPLE ? EXAMPLE_INDEX : DEFAULT_INDEX;
206
-
207
- if (pathname === "/") {
208
- // Resolved through fallback below.
209
- }
210
-
211
- const filePath =
212
- pathname === "/" ? fallbackIndex : resolve(join(ROOT, pathname));
213
-
214
- if (filePath !== fallbackIndex) {
215
- if (!filePath.startsWith(ROOT + sep) && filePath !== ROOT) {
216
- reason = "forbidden path";
217
- res.writeHead(403).end("forbidden");
218
- return;
219
- }
220
- }
221
-
222
- let target = filePath;
223
- try {
224
- const s = await stat(target);
225
- if (s.isDirectory()) target = join(target, "index.html");
226
- } catch {
227
- // Public assets: in production `mado release` copies public/* into out/.
228
- // In dev we surface them from public/ directly so favicon.svg, robots.txt,
229
- // og-image.png, etc. don't 404 (and so dev/prod behavior matches).
230
- const publicCandidate = resolve(join(ROOT, "public", pathname));
231
- if (
232
- publicCandidate.startsWith(resolve(join(ROOT, "public")) + sep) &&
233
- existsSync(publicCandidate)
234
- ) {
235
- try {
236
- const ps = await stat(publicCandidate);
237
- if (!ps.isDirectory()) {
238
- target = publicCandidate;
239
- reason = "public/";
240
- } else if (!extname(pathname)) {
241
- target = fallbackIndex;
242
- } else {
243
- reason = "file not found";
244
- res.writeHead(404).end("not found");
245
- return;
246
- }
247
- } catch {
248
- target = fallbackIndex;
249
- }
250
- } else if (!extname(pathname)) {
251
- target = fallbackIndex;
252
- } else {
253
- reason = "file not found";
254
- res.writeHead(404).end("not found");
255
- return;
256
- }
257
- }
258
-
259
- let data = await readFile(target);
260
-
261
- // ETag: content hash. If-None-Match → 304.
262
- const etag = `"${createHash("sha1").update(data).digest("base64url")}"`;
263
- if (req.headers["if-none-match"] === etag) {
264
- res.writeHead(304, { etag });
265
- res.end();
266
- return;
267
- }
268
-
269
- // HMR injector: add the client script before </body>.
270
- const type =
271
- MIME[extname(target).toLowerCase()] ?? "application/octet-stream";
272
-
273
- if (type.startsWith("text/html")) {
274
- let text = data.toString("utf8");
275
- // modulepreload hints: tell the browser to fetch the framework core and
276
- // the mounted example's pages while HTML is still being parsed.
277
- const preload = await buildPreloadHints();
278
- if (preload) {
279
- text = text.replace(/<\/head>/i, `${preload}\n </head>`);
280
- }
281
- if (HMR) {
282
- text = text.replace(
283
- /<\/body>/i,
284
- `<script>${HMR_CLIENT}</script>\n </body>`,
285
- );
286
- }
287
- data = Buffer.from(text);
288
- }
289
-
290
- res.writeHead(200, {
291
- "content-type": type,
292
- etag,
293
- "cache-control": "no-cache",
294
- });
295
- res.end(data);
296
- } catch (err) {
297
- reason = "unhandled error";
298
- console.error("[serve] error:", err);
299
- res.writeHead(500).end(String(err));
300
- }
301
- });
302
-
303
- // ---------- modulepreload hints ----------
304
- //
305
- // Simple heuristic, no AST parsing:
306
- // 1. /dist/src/index.js — framework core, always needed
307
- // 2. /dist/examples/<EXAMPLE>/main.js — app entry
308
- // 3. /dist/examples/<EXAMPLE>/routes.js — route manifest
309
- // 4. /dist/examples/<EXAMPLE>/pages/*.js — all pages
310
- // (without them the first router click waterfalls)
311
- //
312
- // Disable with PRELOAD=0. Limit to the core with PRELOAD=core.
313
- // Default is full (all pages).
314
-
315
- const PRELOAD = process.env.PRELOAD ?? "full";
316
-
317
- let cachedPreloadHints = null;
318
- let cachedPreloadAt = 0;
319
- const PRELOAD_CACHE_MS = HMR ? 1000 : 60_000;
320
-
321
- async function buildPreloadHints() {
322
- if (PRELOAD === "0" || PRELOAD === "off" || PRELOAD === "false") return "";
323
- const now = Date.now();
324
- if (cachedPreloadHints !== null && now - cachedPreloadAt < PRELOAD_CACHE_MS) {
325
- return cachedPreloadHints;
326
- }
327
- const hrefs = [];
328
- // core
329
- if (existsSync(join(ROOT, "dist/src/index.js"))) {
330
- hrefs.push("/dist/src/index.js");
331
- }
332
- if (EXAMPLE) {
333
- const exampleDist = join(ROOT, "dist", "examples", EXAMPLE);
334
- for (const f of ["main.js", "routes.js"]) {
335
- if (existsSync(join(exampleDist, f))) {
336
- hrefs.push(`/dist/examples/${EXAMPLE}/${f}`);
337
- }
338
- }
339
- if (PRELOAD === "full") {
340
- const pagesDir = join(exampleDist, "pages");
341
- if (existsSync(pagesDir)) {
342
- try {
343
- for (const file of await readdir(pagesDir)) {
344
- if (file.endsWith(".js")) {
345
- hrefs.push(`/dist/examples/${EXAMPLE}/pages/${file}`);
346
- }
347
- }
348
- } catch {
349
- /* ignore */
350
- }
351
- }
352
- }
353
- } else if (!existsSync(EXAMPLES_INDEX)) {
354
- if (existsSync(join(ROOT, "dist/main.js"))) {
355
- hrefs.push("/dist/main.js");
356
- }
357
- }
358
- cachedPreloadHints = hrefs
359
- .map((h) => ` <link rel="modulepreload" href="${h}">`)
360
- .join("\n");
361
- cachedPreloadAt = now;
362
- return cachedPreloadHints;
363
- }
364
-
365
- server.on("error", (err) => {
366
- if (err.code === "EPERM" || err.code === "EACCES") {
367
- console.error(
368
- `[serve] failed to bind ${HOST}:${PORT}: ${err.message}\n` +
369
- `[serve] tip: this sandbox may disallow binding "${HOST}".\n` +
370
- `[serve] try: mado dev --host 127.0.0.1 (or HOST=127.0.0.1)`,
371
- );
372
- } else {
373
- console.error(`[serve] failed to listen on ${HOST}:${PORT}: ${err.message}`);
374
- }
375
- process.exit(1);
376
- });
377
-
378
- async function proxyForward({ req, res, prefix, upstream, pathname, search }) {
379
- // Strip the prefix only if the upstream URL itself ends with `/`; otherwise
380
- // forward the full pathname so the backend sees /api/...
381
- let upstreamUrl;
382
- try {
383
- upstreamUrl = new URL(upstream);
384
- } catch {
385
- res.writeHead(502).end(`bad upstream: ${upstream}`);
386
- return;
387
- }
388
- const target = new URL(upstream);
389
- // Compose path: <upstream.pathname rstrip "/"> + <pathname> + <search>
390
- const tail = pathname; // keep the original /api/... so backends route normally
391
- target.pathname = (target.pathname.replace(/\/$/, "")) + tail;
392
- target.search = search;
393
-
394
- const lib = target.protocol === "https:" ? httpsRequest : httpRequest;
395
- const upstreamReq = lib(
396
- target,
397
- {
398
- method: req.method,
399
- headers: {
400
- ...req.headers,
401
- host: target.host,
402
- },
403
- },
404
- (upstreamRes) => {
405
- // Forward status and headers, then pipe the body.
406
- res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
407
- upstreamRes.pipe(res);
408
- },
409
- );
410
- upstreamReq.on("error", (err) => {
411
- console.error(`[serve] proxy error for ${pathname} → ${target.href}:`, err.message);
412
- if (!res.headersSent) {
413
- res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
414
- res.end(`proxy upstream unavailable: ${target.host}\n${err.message}`);
415
- } else {
416
- res.end();
417
- }
418
- });
419
- req.pipe(upstreamReq);
420
- // Reference unused arg so lint is happy.
421
- void prefix;
422
- }
423
-
424
- server.listen(PORT, HOST, () => {
425
- const distReady = existsSync(join(ROOT, "dist/src/index.js"))
426
- || existsSync(join(ROOT, "dist/main.js"));
427
- const mount = EXAMPLE
428
- ? `examples/${EXAMPLE}/ -> /`
429
- : existsSync(EXAMPLES_INDEX)
430
- ? "examples/index.html landing"
431
- : "index.html app";
432
- // Show "localhost" in the URL when bound to 0.0.0.0 — easier to click.
433
- const urlHost = HOST === "0.0.0.0" || HOST === "::" ? "localhost" : HOST;
434
- console.log("");
435
- console.log("Mado dev server");
436
- console.log(` url: http://${urlHost}:${PORT}/`);
437
- console.log(` host: ${HOST}`);
438
- console.log(` root: ${ROOT}`);
439
- console.log(` mount: ${mount}`);
440
- console.log(` hmr: ${HMR ? "on" : "off"}`);
441
- console.log(` preload: ${PRELOAD}`);
442
- console.log(` dist: ${distReady ? "ready" : "missing (run mado build)"}`);
443
- if (PROXY_RULES.length > 0) {
444
- console.log(" proxy:");
445
- for (const [prefix, upstream] of PROXY_RULES) {
446
- console.log(` ${prefix.padEnd(10)} → ${upstream}`);
447
- }
448
- }
449
- if (!EXAMPLE && existsSync(EXAMPLES_INDEX)) {
450
- console.log(" try: mado serve basic");
451
- console.log(" mado serve showcase");
452
- console.log(" mado serve tickets");
453
- }
454
- console.log("");
455
- });
@@ -1,63 +0,0 @@
1
- # __APP_NAME__
2
-
3
- A starter Mado admin app: nested routes, a guarded admin shell, a blessed API
4
- client, and a one-shot release pipeline.
5
-
6
- ## What you get
7
-
8
- - `src/main.ts` — 8 lines: mount the router into `#app`. Layouts are NOT
9
- declared here, only in `src/routes.ts`.
10
- - `src/routes.ts` — nested manifest with three groups:
11
- - `/` → public landing (bakeable),
12
- - `/login` → centered `auth` layout,
13
- - `/admin` → `app` layout, **guarded** by `requireAuth`.
14
- - `src/layouts/app.ts` — admin shell (top bar + sidebar + content slot).
15
- - `src/layouts/auth.ts` — centered card for sign-in.
16
- - `src/lib/api.ts` — `createApiClient(baseUrl)` with bearer token, 401-refresh
17
- retry, JSON in/out and a typed `ApiError`.
18
- - `src/lib/auth.ts` — memory-only `accessToken`, `restoreSession()` from an
19
- HttpOnly refresh cookie, and the `requireAuth` guard.
20
- - `src/components/` — tiny `x-button` and `x-input` Web Components.
21
- - `mado.config.json` — one config file. Includes a `dev.proxy` for `/api`.
22
-
23
- ## Commands
24
-
25
- ```bash
26
- npm run dev # tsc -w + dev server on http://localhost:5173, HMR on
27
- npm run build # tsc → dist/
28
- npm run typecheck # tsc --noEmit
29
- npm run bundle # esbuild → out/assets/
30
- npm run bake # prerender baked routes → out/baked/
31
- npm run release # typecheck + build + bundle + bake + copy public/ → out/
32
- npm run preview # serve out/ locally (production rehearsal)
33
- ```
34
-
35
- To deploy, run `npm run release` and upload the entire `out/` directory
36
- anywhere static (nginx, Cloudflare Pages, S3, Netlify, GitHub Pages, …).
37
-
38
- ## Backend expectations
39
-
40
- The blessed `api` client speaks JSON. The auth recipe expects:
41
-
42
- - `POST /api/auth/login` → `{ accessToken: string }` (sets refresh cookie)
43
- - `POST /api/auth/refresh` → `{ accessToken: string }` (reads refresh cookie)
44
- - `POST /api/auth/logout` → 204 (clears refresh cookie)
45
-
46
- Change `mado.config.json#dev.proxy` to point at your backend in development.
47
-
48
- ## Where things live
49
-
50
- | What | Where |
51
- |---------------------|--------------------------------------|
52
- | New URL | `src/pages/*.ts` + add to `routes.ts`|
53
- | New protected URL | inside the `/admin` layout block |
54
- | New layout | `src/layouts/*.ts` |
55
- | New reusable widget | `src/components/x-*.ts` |
56
- | New API call | `src/lib/api.ts` (add a method) |
57
- | New global signal | `src/lib/<name>.ts` |
58
- | Static image | `public/<file>` |
59
-
60
- See the framework docs:
61
- [`docs/en/11-layouts.md`](https://github.com/madojs/mado/blob/main/docs/en/11-layouts.md),
62
- [`docs/en/12-auth-and-api.md`](https://github.com/madojs/mado/blob/main/docs/en/12-auth-and-api.md),
63
- [`docs/en/13-deployment.md`](https://github.com/madojs/mado/blob/main/docs/en/13-deployment.md).
@@ -1,28 +0,0 @@
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">
6
- <title>__APP_NAME__</title>
7
- <link rel="icon" type="image/svg+xml" href="/favicon.svg">
8
- <!--
9
- Paths below MUST be root-absolute (start with "/"), not "./...".
10
- Mado is an SPA: hard-refreshing /admin/orders/42 still serves this same
11
- index.html, and the browser resolves "./dist/main.js" against the
12
- current URL → /admin/orders/dist/main.js → 404 → blank page.
13
- Root-absolute paths always resolve to /dist/main.js regardless of route.
14
- -->
15
- <script type="importmap">
16
- {
17
- "imports": {
18
- "@madojs/mado": "/node_modules/@madojs/mado/dist/src/index.js",
19
- "@madojs/mado/": "/node_modules/@madojs/mado/dist/src/"
20
- }
21
- }
22
- </script>
23
- </head>
24
- <body>
25
- <div id="app"></div>
26
- <script type="module" src="/dist/main.js"></script>
27
- </body>
28
- </html>
@@ -1,22 +0,0 @@
1
- {
2
- "dev": {
3
- "port": 5173,
4
- "proxy": {
5
- "/api": "http://localhost:3000"
6
- }
7
- },
8
- "build": {
9
- "out": "out",
10
- "dist": "dist",
11
- "publicDir": "public"
12
- },
13
- "bake": {
14
- "entry": "src/routes.ts",
15
- "template": "index.html",
16
- "baseUrl": "https://example.com"
17
- },
18
- "bundle": {
19
- "splitting": true,
20
- "compress": ["gz", "br"]
21
- }
22
- }
@@ -1,24 +0,0 @@
1
- {
2
- "name": "__PACKAGE_NAME__",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "build": "mado build",
8
- "typecheck": "mado typecheck",
9
- "dev": "mado dev",
10
- "serve": "mado serve",
11
- "bundle": "mado bundle",
12
- "bake": "mado bake",
13
- "release": "mado release",
14
- "preview": "mado preview"
15
- },
16
- "dependencies": {
17
- "@madojs/mado": "__MADOJS_VERSION__"
18
- },
19
- "devDependencies": {
20
- "esbuild": "^0.28.0",
21
- "linkedom": "^0.18.12",
22
- "typescript": "^6.0.3"
23
- }
24
- }
@@ -1,4 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
- <rect width="32" height="32" rx="6" fill="#1f6feb"/>
3
- <text x="16" y="22" text-anchor="middle" font-family="ui-sans-serif, system-ui, sans-serif" font-weight="700" font-size="18" fill="white">M</text>
4
- </svg>
@@ -1,82 +0,0 @@
1
- // <x-button variant="primary|ghost|danger" ?disabled>
2
- //
3
- // Wraps a native <button> so it can be slotted with text/icon and styled
4
- // consistently across the app.
5
- //
6
- // Handles two Shadow DOM gotchas out of the box:
7
- // 1. Reactive attributes via ctx.attr() — external ?disabled changes
8
- // re-render the inner button automatically.
9
- // 2. Form submit — a <button type="submit"> inside Shadow DOM cannot
10
- // trigger <form> submit in Light DOM (spec limitation). We call
11
- // form.requestSubmit() from a click handler to bridge this gap.
12
-
13
- import { component, css, html } from "@madojs/mado";
14
-
15
- component(
16
- "x-button",
17
- ({ host, attr }) => {
18
- const variant = attr("variant", "primary");
19
- const disabled = attr("disabled");
20
-
21
- const handleClick = () => {
22
- const typeAttr = host.getAttribute("type");
23
- if (typeAttr === "button" || typeAttr === "reset") return;
24
- const form = host.closest("form");
25
- if (form && !host.hasAttribute("disabled")) form.requestSubmit();
26
- };
27
-
28
- return () => html`
29
- <button
30
- data-variant=${variant()}
31
- ?disabled=${() => disabled() !== ""}
32
- @click=${handleClick}
33
- >
34
- <slot></slot>
35
- </button>
36
- `;
37
- },
38
- {
39
- styles: css`
40
- :host {
41
- display: inline-flex;
42
- }
43
- button {
44
- display: inline-flex;
45
- align-items: center;
46
- gap: var(--space-2);
47
- padding: 8px 14px;
48
- border-radius: var(--radius-sm);
49
- border: 1px solid transparent;
50
- font: inherit;
51
- cursor: pointer;
52
- background: var(--accent);
53
- color: var(--accent-fg);
54
- transition: filter 0.12s ease;
55
- }
56
- button:hover:not(:disabled) {
57
- filter: brightness(1.07);
58
- }
59
- button:active:not(:disabled) {
60
- filter: brightness(0.95);
61
- }
62
- button:disabled {
63
- opacity: 0.55;
64
- cursor: not-allowed;
65
- }
66
-
67
- button[data-variant="ghost"] {
68
- background: transparent;
69
- color: var(--fg);
70
- border-color: var(--border);
71
- }
72
- button[data-variant="ghost"]:hover:not(:disabled) {
73
- background: var(--bg-elevated);
74
- }
75
-
76
- button[data-variant="danger"] {
77
- background: var(--danger);
78
- color: white;
79
- }
80
- `,
81
- },
82
- );