@madojs/mado 0.5.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 (162) hide show
  1. package/AGENTS.md +291 -0
  2. package/CHANGELOG.md +23 -0
  3. package/LICENSE +21 -0
  4. package/README.md +371 -0
  5. package/ROADMAP.md +52 -0
  6. package/dist/src/component.d.ts +48 -0
  7. package/dist/src/component.js +140 -0
  8. package/dist/src/component.js.map +1 -0
  9. package/dist/src/context.d.ts +40 -0
  10. package/dist/src/context.js +67 -0
  11. package/dist/src/context.js.map +1 -0
  12. package/dist/src/css.d.ts +54 -0
  13. package/dist/src/css.js +137 -0
  14. package/dist/src/css.js.map +1 -0
  15. package/dist/src/devtools.d.ts +22 -0
  16. package/dist/src/devtools.js +63 -0
  17. package/dist/src/devtools.js.map +1 -0
  18. package/dist/src/diagnostics.d.ts +11 -0
  19. package/dist/src/diagnostics.js +28 -0
  20. package/dist/src/diagnostics.js.map +1 -0
  21. package/dist/src/each.d.ts +39 -0
  22. package/dist/src/each.js +35 -0
  23. package/dist/src/each.js.map +1 -0
  24. package/dist/src/forms.d.ts +71 -0
  25. package/dist/src/forms.js +161 -0
  26. package/dist/src/forms.js.map +1 -0
  27. package/dist/src/head.d.ts +19 -0
  28. package/dist/src/head.js +97 -0
  29. package/dist/src/head.js.map +1 -0
  30. package/dist/src/html/bindings.d.ts +78 -0
  31. package/dist/src/html/bindings.js +304 -0
  32. package/dist/src/html/bindings.js.map +1 -0
  33. package/dist/src/html/parser.d.ts +64 -0
  34. package/dist/src/html/parser.js +521 -0
  35. package/dist/src/html/parser.js.map +1 -0
  36. package/dist/src/html/template-types.d.ts +27 -0
  37. package/dist/src/html/template-types.js +8 -0
  38. package/dist/src/html/template-types.js.map +1 -0
  39. package/dist/src/html/template.d.ts +45 -0
  40. package/dist/src/html/template.js +119 -0
  41. package/dist/src/html/template.js.map +1 -0
  42. package/dist/src/html.d.ts +16 -0
  43. package/dist/src/html.js +16 -0
  44. package/dist/src/html.js.map +1 -0
  45. package/dist/src/index.d.ts +35 -0
  46. package/dist/src/index.js +39 -0
  47. package/dist/src/index.js.map +1 -0
  48. package/dist/src/lazy.d.ts +38 -0
  49. package/dist/src/lazy.js +73 -0
  50. package/dist/src/lazy.js.map +1 -0
  51. package/dist/src/lifecycle.d.ts +45 -0
  52. package/dist/src/lifecycle.js +66 -0
  53. package/dist/src/lifecycle.js.map +1 -0
  54. package/dist/src/page.d.ts +161 -0
  55. package/dist/src/page.js +38 -0
  56. package/dist/src/page.js.map +1 -0
  57. package/dist/src/persisted.d.ts +47 -0
  58. package/dist/src/persisted.js +119 -0
  59. package/dist/src/persisted.js.map +1 -0
  60. package/dist/src/resource.d.ts +120 -0
  61. package/dist/src/resource.js +275 -0
  62. package/dist/src/resource.js.map +1 -0
  63. package/dist/src/router/manifest.d.ts +56 -0
  64. package/dist/src/router/manifest.js +302 -0
  65. package/dist/src/router/manifest.js.map +1 -0
  66. package/dist/src/router/match.d.ts +62 -0
  67. package/dist/src/router/match.js +117 -0
  68. package/dist/src/router/match.js.map +1 -0
  69. package/dist/src/router/navigation.d.ts +89 -0
  70. package/dist/src/router/navigation.js +263 -0
  71. package/dist/src/router/navigation.js.map +1 -0
  72. package/dist/src/router.d.ts +13 -0
  73. package/dist/src/router.js +13 -0
  74. package/dist/src/router.js.map +1 -0
  75. package/dist/src/signal.d.ts +67 -0
  76. package/dist/src/signal.js +238 -0
  77. package/dist/src/signal.js.map +1 -0
  78. package/docs/README.md +12 -0
  79. package/docs/en/00-the-mado-way.md +106 -0
  80. package/docs/en/01-routing.md +204 -0
  81. package/docs/en/02-project-layout.md +58 -0
  82. package/docs/en/03-static-bake.md +251 -0
  83. package/docs/en/04-ide-setup.md +162 -0
  84. package/docs/en/05-why-mado.md +193 -0
  85. package/docs/en/06-for-backenders.md +422 -0
  86. package/docs/en/07-llm-pitfalls.md +486 -0
  87. package/docs/en/08-llm-zero-history-test.md +56 -0
  88. package/docs/en/09-shadow-vs-light-dom.md +122 -0
  89. package/docs/en/README.md +16 -0
  90. package/docs/fr/00-the-mado-way.md +108 -0
  91. package/docs/fr/01-routing.md +202 -0
  92. package/docs/fr/02-project-layout.md +58 -0
  93. package/docs/fr/03-static-bake.md +290 -0
  94. package/docs/fr/04-ide-setup.md +162 -0
  95. package/docs/fr/05-why-mado.md +193 -0
  96. package/docs/fr/06-for-backenders.md +432 -0
  97. package/docs/fr/07-llm-pitfalls.md +487 -0
  98. package/docs/fr/08-llm-zero-history-test.md +60 -0
  99. package/docs/fr/09-shadow-vs-light-dom.md +121 -0
  100. package/docs/fr/README.md +16 -0
  101. package/docs/ru/00-the-mado-way.md +93 -0
  102. package/docs/ru/01-routing.md +194 -0
  103. package/docs/ru/02-project-layout.md +57 -0
  104. package/docs/ru/03-static-bake.md +251 -0
  105. package/docs/ru/04-ide-setup.md +144 -0
  106. package/docs/ru/05-why-mado.md +193 -0
  107. package/docs/ru/06-for-backenders.md +422 -0
  108. package/docs/ru/07-llm-pitfalls.md +485 -0
  109. package/docs/ru/08-llm-zero-history-test.md +56 -0
  110. package/docs/ru/09-shadow-vs-light-dom.md +122 -0
  111. package/docs/ru/README.md +14 -0
  112. package/docs/uk/00-the-mado-way.md +54 -0
  113. package/docs/uk/01-routing.md +82 -0
  114. package/docs/uk/02-project-layout.md +46 -0
  115. package/docs/uk/03-static-bake.md +49 -0
  116. package/docs/uk/04-ide-setup.md +26 -0
  117. package/docs/uk/05-why-mado.md +34 -0
  118. package/docs/uk/06-for-backenders.md +50 -0
  119. package/docs/uk/07-llm-pitfalls.md +82 -0
  120. package/docs/uk/08-llm-zero-history-test.md +31 -0
  121. package/docs/uk/09-shadow-vs-light-dom.md +40 -0
  122. package/docs/uk/README.md +16 -0
  123. package/llms.txt +155 -0
  124. package/package.json +81 -0
  125. package/scripts/bake.mjs +406 -0
  126. package/scripts/bundle.mjs +146 -0
  127. package/scripts/cli.mjs +382 -0
  128. package/scripts/new.mjs +80 -0
  129. package/scripts/preview.mjs +176 -0
  130. package/scripts/release-notes.mjs +66 -0
  131. package/scripts/showcase-regression.mjs +392 -0
  132. package/server/serve.mjs +292 -0
  133. package/starters/crud/README.md +21 -0
  134. package/starters/crud/index.html +20 -0
  135. package/starters/crud/package.json +17 -0
  136. package/starters/crud/src/components/app-shell.ts +51 -0
  137. package/starters/crud/src/components/ticket-detail.ts +33 -0
  138. package/starters/crud/src/components/ticket-form.ts +69 -0
  139. package/starters/crud/src/components/ticket-list.ts +66 -0
  140. package/starters/crud/src/lib/api.ts +76 -0
  141. package/starters/crud/src/main.ts +12 -0
  142. package/starters/crud/src/pages/home.ts +18 -0
  143. package/starters/crud/src/pages/not-found.ts +12 -0
  144. package/starters/crud/src/pages/ticket-detail.ts +6 -0
  145. package/starters/crud/src/pages/ticket-new.ts +6 -0
  146. package/starters/crud/src/pages/tickets.ts +6 -0
  147. package/starters/crud/src/routes.ts +9 -0
  148. package/starters/crud/src/styles/global.ts +155 -0
  149. package/starters/crud/tsconfig.json +15 -0
  150. package/starters/minimal/README.md +19 -0
  151. package/starters/minimal/index.html +20 -0
  152. package/starters/minimal/package.json +17 -0
  153. package/starters/minimal/src/components/app-counter.ts +31 -0
  154. package/starters/minimal/src/main.ts +9 -0
  155. package/starters/minimal/src/pages/home.ts +18 -0
  156. package/starters/minimal/src/pages/not-found.ts +14 -0
  157. package/starters/minimal/src/routes.ts +6 -0
  158. package/starters/minimal/src/styles/global.ts +60 -0
  159. package/starters/minimal/tsconfig.json +15 -0
  160. package/templates/page-detail.ts +63 -0
  161. package/templates/page-form.ts +94 -0
  162. package/templates/page-list.ts +79 -0
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
5
+ import { copyFile, cp, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
6
+ import http from "node:http";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
+ const PROJECT_ROOT = resolve(process.cwd());
12
+ const PACKAGE_JSON = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
13
+ const [, , rawCommand, ...args] = process.argv;
14
+
15
+ const EXAMPLES = [
16
+ ["basic", "minimal API tour"],
17
+ ["tickets", "LLM zero-history CRUD validation"],
18
+ ["showcase", "flagship SaaS CRM pressure app"],
19
+ ["cloudflare", "Cloudflare Workers edge example"],
20
+ ];
21
+ const STARTERS = ["minimal", "crud"];
22
+
23
+ const command = rawCommand ?? "help";
24
+
25
+ switch (command) {
26
+ case "init":
27
+ await runInit(args);
28
+ break;
29
+ case "build":
30
+ await runNodeBin("typescript/bin/tsc", args);
31
+ break;
32
+ case "watch":
33
+ await runNodeBin("typescript/bin/tsc", ["-w", ...args]);
34
+ break;
35
+ case "typecheck":
36
+ await runNodeBin("typescript/bin/tsc", ["--noEmit", ...args]);
37
+ break;
38
+ case "test":
39
+ if (args[0] === "browser") {
40
+ await runNodeScript("scripts/showcase-regression.mjs", args.slice(1));
41
+ } else {
42
+ const files = await listTestFiles();
43
+ await run(process.execPath, ["--test", "--test-timeout=10000", ...files, ...args]);
44
+ }
45
+ break;
46
+ case "serve":
47
+ await runServe(args[0] ?? "");
48
+ break;
49
+ case "dev":
50
+ await runDev(args[0] ?? "");
51
+ break;
52
+ case "bake":
53
+ await runNodeScript("scripts/bake.mjs", args);
54
+ break;
55
+ case "bundle":
56
+ await runNodeScript("scripts/bundle.mjs", args);
57
+ break;
58
+ case "preview":
59
+ await runNodeScript("scripts/preview.mjs", args);
60
+ break;
61
+ case "new":
62
+ await runNodeScript("scripts/new.mjs", args);
63
+ break;
64
+ case "examples":
65
+ printExamples();
66
+ break;
67
+ case "help":
68
+ case "--help":
69
+ case "-h":
70
+ printHelp();
71
+ break;
72
+ default:
73
+ console.error(`[mado] unknown command: ${command}`);
74
+ printHelp();
75
+ process.exit(1);
76
+ }
77
+
78
+ async function runServe(example) {
79
+ if (!example && PROJECT_ROOT !== PACKAGE_ROOT) {
80
+ await serveStaticProject(PROJECT_ROOT);
81
+ return;
82
+ }
83
+ if (example) assertExample(example, { serveable: true });
84
+ await run(process.execPath, [join(PACKAGE_ROOT, "server/serve.mjs"), example].filter(Boolean), {
85
+ env: { ...process.env, EXAMPLE: example || process.env.EXAMPLE || "" },
86
+ });
87
+ }
88
+
89
+ async function runInit(rawArgs) {
90
+ const { flags, positional } = parseFlags(rawArgs);
91
+ const targetArg = positional[0];
92
+ if (!targetArg) {
93
+ console.error("[mado] usage: mado init <name> [--starter minimal|crud] [--force]");
94
+ process.exit(1);
95
+ }
96
+
97
+ const starter = String(flags.starter ?? "minimal");
98
+ if (!STARTERS.includes(starter)) {
99
+ console.error(`[mado] unknown starter: ${starter}`);
100
+ console.error(`[mado] available starters: ${STARTERS.join(", ")}`);
101
+ process.exit(1);
102
+ }
103
+
104
+ const target = resolve(PROJECT_ROOT, targetArg);
105
+ const source = join(PACKAGE_ROOT, "starters", starter);
106
+ if (!existsSync(source)) {
107
+ console.error(`[mado] missing starter template: ${starter}`);
108
+ process.exit(1);
109
+ }
110
+ if (existsSync(target) && statSync(target).isFile()) {
111
+ console.error(`[mado] target exists and is a file: ${target}`);
112
+ process.exit(1);
113
+ }
114
+ if (existsSync(target) && readdirSync(target).length > 0 && !flags.force) {
115
+ console.error(`[mado] target directory is not empty: ${target}`);
116
+ console.error("[mado] use --force to write into it");
117
+ process.exit(1);
118
+ }
119
+
120
+ await mkdir(target, { recursive: true });
121
+ await cp(source, target, { recursive: true, force: true });
122
+ await copyCanonicalAgentFiles(target);
123
+
124
+ const packageName = packageNameFromDir(target);
125
+ if (!isValidPackageName(packageName)) {
126
+ console.error(`[mado] invalid package name derived from target: ${packageName}`);
127
+ process.exit(1);
128
+ }
129
+
130
+ const replacements = {
131
+ __APP_NAME__: packageName,
132
+ __PACKAGE_NAME__: packageName,
133
+ __MADOJS_VERSION__: process.env.MADO_PACKAGE_SPEC || process.env.MADOJS_PACKAGE_SPEC || `^${PACKAGE_JSON.version}`,
134
+ __MADO_VERSION__: PACKAGE_JSON.version,
135
+ };
136
+
137
+ for (const file of await walkFiles(target)) {
138
+ const text = await readFile(file, "utf8").catch(() => null);
139
+ if (text === null) continue;
140
+ let next = text;
141
+ for (const [key, value] of Object.entries(replacements)) {
142
+ next = next.split(key).join(value);
143
+ }
144
+ if (next !== text) await writeFile(file, next);
145
+ }
146
+
147
+ console.log("");
148
+ console.log(`Created ${packageName} with the ${starter} starter.`);
149
+ console.log("");
150
+ console.log("Next steps:");
151
+ console.log(` cd ${relativePath(PROJECT_ROOT, target)}`);
152
+ console.log(" npm install");
153
+ console.log(" npm run build");
154
+ console.log(" npm run serve");
155
+ console.log("");
156
+ }
157
+
158
+ async function runDev(example) {
159
+ if (example) assertExample(example, { serveable: true });
160
+
161
+ const env = { ...process.env, EXAMPLE: example || process.env.EXAMPLE || "" };
162
+ const server = spawn(process.execPath, [join(PACKAGE_ROOT, "server/serve.mjs"), example].filter(Boolean), {
163
+ cwd: PROJECT_ROOT,
164
+ env,
165
+ stdio: "inherit",
166
+ });
167
+ const tsc = spawn(process.execPath, [resolveBin("typescript/bin/tsc"), "-w"], {
168
+ cwd: PROJECT_ROOT,
169
+ stdio: "inherit",
170
+ });
171
+
172
+ const children = [server, tsc];
173
+ let shuttingDown = false;
174
+
175
+ const shutdown = (code = 0) => {
176
+ if (shuttingDown) return;
177
+ shuttingDown = true;
178
+ for (const child of children) {
179
+ if (!child.killed) child.kill("SIGTERM");
180
+ }
181
+ setTimeout(() => process.exit(code), 80);
182
+ };
183
+
184
+ process.on("SIGINT", () => shutdown(0));
185
+ process.on("SIGTERM", () => shutdown(0));
186
+
187
+ await Promise.race(
188
+ children.map(
189
+ (child) =>
190
+ new Promise((resolveExit) => {
191
+ child.on("exit", (code, signal) => resolveExit({ code, signal }));
192
+ }),
193
+ ),
194
+ ).then(({ code, signal }) => {
195
+ if (signal) shutdown(1);
196
+ else shutdown(code ?? 0);
197
+ });
198
+ }
199
+
200
+ async function copyCanonicalAgentFiles(target) {
201
+ for (const file of ["AGENTS.md", "llms.txt"]) {
202
+ const source = join(PACKAGE_ROOT, file);
203
+ if (existsSync(source)) await copyFile(source, join(target, file));
204
+ }
205
+ }
206
+
207
+ async function runNodeBin(bin, args) {
208
+ await run(process.execPath, [resolveBin(bin), ...args]);
209
+ }
210
+
211
+ async function runNodeScript(script, args) {
212
+ await run(process.execPath, [join(PACKAGE_ROOT, script), ...args]);
213
+ }
214
+
215
+ async function run(cmd, args, options = {}) {
216
+ const child = spawn(cmd, args, {
217
+ cwd: PROJECT_ROOT,
218
+ stdio: "inherit",
219
+ env: options.env ?? process.env,
220
+ shell: options.shell ?? false,
221
+ });
222
+ const code = await new Promise((resolveExit) => {
223
+ child.on("exit", (status) => resolveExit(status ?? 1));
224
+ });
225
+ if (code !== 0) process.exit(code);
226
+ }
227
+
228
+ function resolveBin(bin) {
229
+ const projectPath = join(PROJECT_ROOT, "node_modules", bin);
230
+ if (existsSync(projectPath)) return projectPath;
231
+ const path = join(PACKAGE_ROOT, "node_modules", bin);
232
+ if (!existsSync(path)) {
233
+ console.error(`[mado] missing ${bin}. Run npm install first.`);
234
+ process.exit(1);
235
+ }
236
+ return path;
237
+ }
238
+
239
+ function assertExample(name, { serveable }) {
240
+ const dir = join(PROJECT_ROOT, "examples", name);
241
+ if (!existsSync(dir)) {
242
+ console.error(`[mado] unknown example: ${name}`);
243
+ printExamples();
244
+ process.exit(1);
245
+ }
246
+ if (serveable && !existsSync(join(dir, "index.html"))) {
247
+ console.error(`[mado] example "${name}" is not a browser SPA example.`);
248
+ process.exit(1);
249
+ }
250
+ }
251
+
252
+ function printExamples() {
253
+ console.log("Available examples:");
254
+ for (const [name, description] of EXAMPLES) {
255
+ const marker = existsSync(join(PROJECT_ROOT, "examples", name, "index.html")) ? "serve" : "docs";
256
+ console.log(` ${name.padEnd(10)} ${marker.padEnd(5)} ${description}`);
257
+ }
258
+ }
259
+
260
+ async function listTestFiles() {
261
+ const dir = join(PROJECT_ROOT, "test");
262
+ const files = await readdir(dir);
263
+ return files
264
+ .filter((file) => file.endsWith(".test.mjs"))
265
+ .sort()
266
+ .map((file) => join("test", file));
267
+ }
268
+
269
+ function printHelp() {
270
+ console.log(`mado commands:
271
+ mado init <name> [--starter minimal|crud] [--force]
272
+ mado build
273
+ mado watch
274
+ mado typecheck
275
+ mado test [browser]
276
+ mado serve [basic|tickets|showcase]
277
+ mado dev [basic|tickets|showcase]
278
+ mado bake
279
+ mado bundle
280
+ mado preview
281
+ mado new <list|form|detail> <name>
282
+ mado examples`);
283
+ }
284
+
285
+ function parseFlags(raw) {
286
+ const flags = {};
287
+ const positional = [];
288
+ for (let i = 0; i < raw.length; i++) {
289
+ const arg = raw[i];
290
+ if (arg === "--") continue;
291
+ if (arg.startsWith("--")) {
292
+ const [name, inline] = arg.slice(2).split("=");
293
+ if (inline !== undefined) flags[name] = inline;
294
+ else if (raw[i + 1] && !raw[i + 1].startsWith("-")) flags[name] = raw[++i];
295
+ else flags[name] = true;
296
+ } else {
297
+ positional.push(arg);
298
+ }
299
+ }
300
+ return { flags, positional };
301
+ }
302
+
303
+ function packageNameFromDir(target) {
304
+ return target
305
+ .split(/[\\/]/)
306
+ .filter(Boolean)
307
+ .at(-1)
308
+ ?.toLowerCase()
309
+ .replace(/[^a-z0-9._-]+/g, "-")
310
+ .replace(/^-+|-+$/g, "") || "mado-app";
311
+ }
312
+
313
+ function isValidPackageName(name) {
314
+ return /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(name)
315
+ && !name.includes("..")
316
+ && !name.startsWith(".")
317
+ && name.length <= 214;
318
+ }
319
+
320
+ async function walkFiles(dir) {
321
+ const out = [];
322
+ for (const entry of readdirSync(dir)) {
323
+ if (["node_modules", "dist", ".git"].includes(entry)) continue;
324
+ if (entry === "package-lock.json") continue;
325
+ const file = join(dir, entry);
326
+ const stat = statSync(file);
327
+ if (stat.isDirectory()) out.push(...await walkFiles(file));
328
+ else out.push(file);
329
+ }
330
+ return out;
331
+ }
332
+
333
+ function relativePath(from, to) {
334
+ const rel = to.startsWith(from) ? to.slice(from.length).replace(/^[/\\]/, "") : to;
335
+ return rel || ".";
336
+ }
337
+
338
+ function contentType(file) {
339
+ const ext = file.slice(file.lastIndexOf("."));
340
+ return {
341
+ ".html": "text/html; charset=utf-8",
342
+ ".js": "text/javascript; charset=utf-8",
343
+ ".mjs": "text/javascript; charset=utf-8",
344
+ ".css": "text/css; charset=utf-8",
345
+ ".json": "application/json; charset=utf-8",
346
+ ".svg": "image/svg+xml",
347
+ ".png": "image/png",
348
+ ".jpg": "image/jpeg",
349
+ ".jpeg": "image/jpeg",
350
+ ".webp": "image/webp",
351
+ ".ico": "image/x-icon",
352
+ }[ext] ?? "application/octet-stream";
353
+ }
354
+
355
+ async function serveStaticProject(rootDir) {
356
+ const port = Number(process.env.PORT || 5173);
357
+ const server = http.createServer((req, res) => {
358
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
359
+ const pathname = decodeURIComponent(url.pathname);
360
+ const normalized = pathname.replace(/^\/+/, "");
361
+ let file = resolve(rootDir, normalized);
362
+ if (pathname === "/" || !normalized.includes(".")) file = join(rootDir, "index.html");
363
+ if (!file.startsWith(rootDir) || !existsSync(file) || statSync(file).isDirectory()) {
364
+ file = join(rootDir, "index.html");
365
+ }
366
+
367
+ if (!existsSync(file)) {
368
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
369
+ res.end("Not found");
370
+ return;
371
+ }
372
+
373
+ res.writeHead(200, { "content-type": contentType(file) });
374
+ res.end(readFileSync(file));
375
+ });
376
+
377
+ await new Promise((resolveListen) => {
378
+ server.listen(port, resolveListen);
379
+ });
380
+ console.log(`[mado] serving ${rootDir}`);
381
+ console.log(`[mado] http://localhost:${port}`);
382
+ }
@@ -0,0 +1,80 @@
1
+ // Scaffold a new page from templates/.
2
+ //
3
+ // node scripts/new.mjs list users
4
+ // node scripts/new.mjs form sign-up
5
+ // node scripts/new.mjs detail post
6
+ //
7
+ // Result: examples/pages/<name>.ts (or src/pages/, when present)
8
+ // with __name__ / __Name__ placeholders replaced.
9
+ //
10
+ // Zero dependencies.
11
+
12
+ import { readFile, writeFile, mkdir, access } from "node:fs/promises";
13
+ import { dirname, join, resolve } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17
+ const PROJECT_ROOT = resolve(process.cwd());
18
+
19
+ const [, , kind, rawName] = process.argv;
20
+
21
+ if (!kind || !rawName) {
22
+ console.error("usage: node scripts/new.mjs <list|form|detail> <name>");
23
+ process.exit(1);
24
+ }
25
+
26
+ const templates = {
27
+ list: "templates/page-list.ts",
28
+ form: "templates/page-form.ts",
29
+ detail: "templates/page-detail.ts",
30
+ };
31
+
32
+ const tplPath = templates[kind];
33
+ if (!tplPath) {
34
+ console.error(`unknown template: ${kind} (available: list, form, detail)`);
35
+ process.exit(1);
36
+ }
37
+
38
+ // name → kebab-case (for file names and tags)
39
+ const kebab = rawName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
40
+ // Name → PascalCase for titles
41
+ const pascal = kebab
42
+ .split("-")
43
+ .filter(Boolean)
44
+ .map((p) => p[0].toUpperCase() + p.slice(1))
45
+ .join("");
46
+
47
+ const targetDir = (await exists("src/pages"))
48
+ ? "src/pages"
49
+ : (await exists("examples/basic/pages"))
50
+ ? "examples/basic/pages"
51
+ : "src/pages";
52
+ await mkdir(targetDir, { recursive: true });
53
+
54
+ const targetFile = join(
55
+ targetDir,
56
+ kind === "detail" ? `${kebab}-detail.ts` : `${kebab}.ts`,
57
+ );
58
+
59
+ if (await exists(targetFile)) {
60
+ console.error(`already exists: ${targetFile}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ const src = await readFile(join(PACKAGE_ROOT, tplPath), "utf8");
65
+ const out = src.replaceAll("__name__", kebab).replaceAll("__Name__", pascal);
66
+
67
+ await writeFile(targetFile, out);
68
+ console.log(`✓ created: ${targetFile}`);
69
+ console.log(` remember to add this to routes.ts:`);
70
+ const routePath = kind === "detail" ? `/${kebab}/:id` : `/${kebab}`;
71
+ console.log(` '${routePath}': () => import('./pages/${kebab}${kind === "detail" ? "-detail" : ""}.js'),`);
72
+
73
+ async function exists(p) {
74
+ try {
75
+ await access(join(PROJECT_ROOT, p));
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
@@ -0,0 +1,176 @@
1
+ // Preview: a tiny production-like server that emulates nginx.conf on node:http.
2
+ //
3
+ // npm run preview
4
+ //
5
+ // What it does:
6
+ // 1. npm run build (tsc)
7
+ // 2. node scripts/bake.mjs (generates SEO HTML when bake pages exist)
8
+ // 3. node scripts/bundle.mjs (esbuild splitting + .gz/.br)
9
+ // 4. Starts a static server on :4173 with:
10
+ // - immutable cache for hashed bundles;
11
+ // - SPA fallback to index.html;
12
+ // - baked HTML priority over index.html;
13
+ // - precompressed .gz / .br serving via Accept-Encoding.
14
+ //
15
+ // Goal: see production-like output locally without Docker/nginx.
16
+
17
+ import { createServer } from "node:http";
18
+ import { readFile, stat, access } from "node:fs/promises";
19
+ import { extname, join, resolve, sep } from "node:path";
20
+ import { spawnSync } from "node:child_process";
21
+
22
+ const ROOT = resolve(process.cwd());
23
+ const OUT = resolve(process.env.OUT_DIR ?? "out");
24
+ const PORT = Number(process.env.PORT ?? 4173);
25
+ const SKIP_BUILD = process.env.SKIP_BUILD === "1";
26
+
27
+ const MIME = {
28
+ ".html": "text/html; charset=utf-8",
29
+ ".js": "text/javascript; charset=utf-8",
30
+ ".mjs": "text/javascript; charset=utf-8",
31
+ ".css": "text/css; charset=utf-8",
32
+ ".json": "application/json; charset=utf-8",
33
+ ".svg": "image/svg+xml",
34
+ ".ico": "image/x-icon",
35
+ ".png": "image/png",
36
+ ".jpg": "image/jpeg",
37
+ ".jpeg": "image/jpeg",
38
+ ".webp": "image/webp",
39
+ ".woff2": "font/woff2",
40
+ ".xml": "application/xml; charset=utf-8",
41
+ ".map": "application/json; charset=utf-8",
42
+ };
43
+
44
+ // ---------- 1-3) Full build ----------
45
+
46
+ if (!SKIP_BUILD) {
47
+ console.log("[preview] step 1/3 — tsc");
48
+ run("npx", ["tsc"]);
49
+
50
+ // bake is optional (only when there are pages with bake config)
51
+ console.log("[preview] step 2/3 — bake (optional)");
52
+ run("node", ["scripts/bake.mjs"], { allowFail: true });
53
+
54
+ console.log("[preview] step 3/3 — bundle");
55
+ run("node", ["scripts/bundle.mjs"]);
56
+ }
57
+
58
+ if (!(await exists(OUT))) {
59
+ console.error(`[preview] missing ${OUT}/ — check the steps above`);
60
+ process.exit(1);
61
+ }
62
+
63
+ // ---------- 4) Server ----------
64
+
65
+ const isImmutable = (filename) =>
66
+ /^(main|chunk|asset)-[A-Z0-9]+\.js$/i.test(filename);
67
+
68
+ const server = createServer(async (req, res) => {
69
+ try {
70
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
71
+ const pathname = decodeURIComponent(url.pathname);
72
+ const accepts = (req.headers["accept-encoding"] ?? "").toString();
73
+
74
+ const target = await resolveTarget(pathname);
75
+ if (!target) {
76
+ res.writeHead(404).end("not found");
77
+ return;
78
+ }
79
+
80
+ // Choose encoding: br > gz > raw.
81
+ let { path: filePath, encoding } = await pickEncoding(target, accepts);
82
+ const data = await readFile(filePath);
83
+ const baseExt = extname(target).toLowerCase();
84
+ const type = MIME[baseExt] ?? "application/octet-stream";
85
+
86
+ const cache = isImmutable(basenameSafe(target))
87
+ ? "public, max-age=31536000, immutable"
88
+ : baseExt === ".html"
89
+ ? "no-cache, must-revalidate"
90
+ : "public, max-age=86400";
91
+
92
+ const headers = {
93
+ "content-type": type,
94
+ "cache-control": cache,
95
+ vary: "Accept-Encoding",
96
+ };
97
+ if (encoding) headers["content-encoding"] = encoding;
98
+
99
+ res.writeHead(200, headers);
100
+ res.end(data);
101
+ } catch (err) {
102
+ console.error("[preview] error:", err);
103
+ res.writeHead(500).end(String(err));
104
+ }
105
+ });
106
+
107
+ server.listen(PORT, () => {
108
+ console.log(`\n[preview] http://localhost:${PORT}/ (Ctrl-C — stop)\n`);
109
+ });
110
+
111
+ // ---------- helpers ----------
112
+
113
+ function run(cmd, args, { allowFail = false } = {}) {
114
+ const r = spawnSync(cmd, args, { stdio: "inherit" });
115
+ if (r.status !== 0 && !allowFail) {
116
+ console.error(`[preview] command "${cmd} ${args.join(" ")}" exited with ${r.status}`);
117
+ process.exit(r.status ?? 1);
118
+ }
119
+ }
120
+
121
+ async function exists(p) {
122
+ try {
123
+ await access(p);
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ function basenameSafe(p) {
131
+ const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf(sep));
132
+ return i >= 0 ? p.slice(i + 1) : p;
133
+ }
134
+
135
+ async function resolveTarget(pathname) {
136
+ if (pathname === "/") pathname = "/index.html";
137
+
138
+ const candidate = resolve(join(OUT, pathname));
139
+ if (!candidate.startsWith(OUT + sep) && candidate !== OUT) return null;
140
+
141
+ // 1) Exact match
142
+ if (await exists(candidate)) {
143
+ const s = await stat(candidate);
144
+ if (s.isDirectory()) {
145
+ // Baked priority: /product/foo/ → /product/foo/index.html
146
+ const idx = join(candidate, "index.html");
147
+ if (await exists(idx)) return idx;
148
+ } else {
149
+ return candidate;
150
+ }
151
+ }
152
+
153
+ // 2) /foo → /foo/index.html (for baked pages without trailing slash)
154
+ if (!extname(pathname)) {
155
+ const asDir = join(OUT, pathname, "index.html");
156
+ if (await exists(asDir)) return asDir;
157
+ }
158
+
159
+ // 3) SPA-fallback
160
+ const spa = join(OUT, "index.html");
161
+ if (await exists(spa)) return spa;
162
+
163
+ return null;
164
+ }
165
+
166
+ async function pickEncoding(file, accepts) {
167
+ if (file.endsWith(".js") || file.endsWith(".css") || file.endsWith(".html")) {
168
+ if (accepts.includes("br") && (await exists(`${file}.br`))) {
169
+ return { path: `${file}.br`, encoding: "br" };
170
+ }
171
+ if (accepts.includes("gzip") && (await exists(`${file}.gz`))) {
172
+ return { path: `${file}.gz`, encoding: "gzip" };
173
+ }
174
+ }
175
+ return { path: file, encoding: null };
176
+ }
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from "node:child_process";
3
+
4
+ function sh(command) {
5
+ return execSync(command, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
6
+ }
7
+
8
+ function safe(command) {
9
+ try {
10
+ return sh(command);
11
+ } catch {
12
+ return "";
13
+ }
14
+ }
15
+
16
+ const tag = process.env.GITHUB_REF_NAME || safe("git describe --tags --exact-match") || "HEAD";
17
+ const previous = safe(`git describe --tags --abbrev=0 ${tag}^`);
18
+ const range = previous ? `${previous}..${tag}` : tag;
19
+ const log = safe(`git log --format=%s ${range}`);
20
+
21
+ const groups = [
22
+ ["Features", /^feat(?:\(.+\))?:\s+(.+)/],
23
+ ["Fixes", /^fix(?:\(.+\))?:\s+(.+)/],
24
+ ["Documentation", /^docs(?:\(.+\))?:\s+(.+)/],
25
+ ["Tests", /^test(?:\(.+\))?:\s+(.+)/],
26
+ ["CI", /^ci(?:\(.+\))?:\s+(.+)/],
27
+ ["Maintenance", /^(?:chore|refactor)(?:\(.+\))?:\s+(.+)/],
28
+ ];
29
+
30
+ const notes = new Map(groups.map(([name]) => [name, []]));
31
+ const other = [];
32
+
33
+ for (const line of log.split("\n").map((item) => item.trim()).filter(Boolean)) {
34
+ let matched = false;
35
+ for (const [name, pattern] of groups) {
36
+ const match = line.match(pattern);
37
+ if (match) {
38
+ notes.get(name).push(match[1]);
39
+ matched = true;
40
+ break;
41
+ }
42
+ }
43
+ if (!matched) other.push(line);
44
+ }
45
+
46
+ console.log(`# ${tag}`);
47
+ console.log("");
48
+ if (previous) console.log(`Changes since ${previous}.`);
49
+ else console.log("First public release.");
50
+ console.log("");
51
+
52
+ for (const [name] of groups) {
53
+ const items = notes.get(name);
54
+ if (!items.length) continue;
55
+ console.log(`## ${name}`);
56
+ console.log("");
57
+ for (const item of items) console.log(`- ${item}`);
58
+ console.log("");
59
+ }
60
+
61
+ if (other.length) {
62
+ console.log("## Other");
63
+ console.log("");
64
+ for (const item of other) console.log(`- ${item}`);
65
+ console.log("");
66
+ }