@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.
- package/AGENTS.md +291 -0
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +371 -0
- package/ROADMAP.md +52 -0
- package/dist/src/component.d.ts +48 -0
- package/dist/src/component.js +140 -0
- package/dist/src/component.js.map +1 -0
- package/dist/src/context.d.ts +40 -0
- package/dist/src/context.js +67 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/css.d.ts +54 -0
- package/dist/src/css.js +137 -0
- package/dist/src/css.js.map +1 -0
- package/dist/src/devtools.d.ts +22 -0
- package/dist/src/devtools.js +63 -0
- package/dist/src/devtools.js.map +1 -0
- package/dist/src/diagnostics.d.ts +11 -0
- package/dist/src/diagnostics.js +28 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/each.d.ts +39 -0
- package/dist/src/each.js +35 -0
- package/dist/src/each.js.map +1 -0
- package/dist/src/forms.d.ts +71 -0
- package/dist/src/forms.js +161 -0
- package/dist/src/forms.js.map +1 -0
- package/dist/src/head.d.ts +19 -0
- package/dist/src/head.js +97 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/html/bindings.d.ts +78 -0
- package/dist/src/html/bindings.js +304 -0
- package/dist/src/html/bindings.js.map +1 -0
- package/dist/src/html/parser.d.ts +64 -0
- package/dist/src/html/parser.js +521 -0
- package/dist/src/html/parser.js.map +1 -0
- package/dist/src/html/template-types.d.ts +27 -0
- package/dist/src/html/template-types.js +8 -0
- package/dist/src/html/template-types.js.map +1 -0
- package/dist/src/html/template.d.ts +45 -0
- package/dist/src/html/template.js +119 -0
- package/dist/src/html/template.js.map +1 -0
- package/dist/src/html.d.ts +16 -0
- package/dist/src/html.js +16 -0
- package/dist/src/html.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lazy.d.ts +38 -0
- package/dist/src/lazy.js +73 -0
- package/dist/src/lazy.js.map +1 -0
- package/dist/src/lifecycle.d.ts +45 -0
- package/dist/src/lifecycle.js +66 -0
- package/dist/src/lifecycle.js.map +1 -0
- package/dist/src/page.d.ts +161 -0
- package/dist/src/page.js +38 -0
- package/dist/src/page.js.map +1 -0
- package/dist/src/persisted.d.ts +47 -0
- package/dist/src/persisted.js +119 -0
- package/dist/src/persisted.js.map +1 -0
- package/dist/src/resource.d.ts +120 -0
- package/dist/src/resource.js +275 -0
- package/dist/src/resource.js.map +1 -0
- package/dist/src/router/manifest.d.ts +56 -0
- package/dist/src/router/manifest.js +302 -0
- package/dist/src/router/manifest.js.map +1 -0
- package/dist/src/router/match.d.ts +62 -0
- package/dist/src/router/match.js +117 -0
- package/dist/src/router/match.js.map +1 -0
- package/dist/src/router/navigation.d.ts +89 -0
- package/dist/src/router/navigation.js +263 -0
- package/dist/src/router/navigation.js.map +1 -0
- package/dist/src/router.d.ts +13 -0
- package/dist/src/router.js +13 -0
- package/dist/src/router.js.map +1 -0
- package/dist/src/signal.d.ts +67 -0
- package/dist/src/signal.js +238 -0
- package/dist/src/signal.js.map +1 -0
- package/docs/README.md +12 -0
- package/docs/en/00-the-mado-way.md +106 -0
- package/docs/en/01-routing.md +204 -0
- package/docs/en/02-project-layout.md +58 -0
- package/docs/en/03-static-bake.md +251 -0
- package/docs/en/04-ide-setup.md +162 -0
- package/docs/en/05-why-mado.md +193 -0
- package/docs/en/06-for-backenders.md +422 -0
- package/docs/en/07-llm-pitfalls.md +486 -0
- package/docs/en/08-llm-zero-history-test.md +56 -0
- package/docs/en/09-shadow-vs-light-dom.md +122 -0
- package/docs/en/README.md +16 -0
- package/docs/fr/00-the-mado-way.md +108 -0
- package/docs/fr/01-routing.md +202 -0
- package/docs/fr/02-project-layout.md +58 -0
- package/docs/fr/03-static-bake.md +290 -0
- package/docs/fr/04-ide-setup.md +162 -0
- package/docs/fr/05-why-mado.md +193 -0
- package/docs/fr/06-for-backenders.md +432 -0
- package/docs/fr/07-llm-pitfalls.md +487 -0
- package/docs/fr/08-llm-zero-history-test.md +60 -0
- package/docs/fr/09-shadow-vs-light-dom.md +121 -0
- package/docs/fr/README.md +16 -0
- package/docs/ru/00-the-mado-way.md +93 -0
- package/docs/ru/01-routing.md +194 -0
- package/docs/ru/02-project-layout.md +57 -0
- package/docs/ru/03-static-bake.md +251 -0
- package/docs/ru/04-ide-setup.md +144 -0
- package/docs/ru/05-why-mado.md +193 -0
- package/docs/ru/06-for-backenders.md +422 -0
- package/docs/ru/07-llm-pitfalls.md +485 -0
- package/docs/ru/08-llm-zero-history-test.md +56 -0
- package/docs/ru/09-shadow-vs-light-dom.md +122 -0
- package/docs/ru/README.md +14 -0
- package/docs/uk/00-the-mado-way.md +54 -0
- package/docs/uk/01-routing.md +82 -0
- package/docs/uk/02-project-layout.md +46 -0
- package/docs/uk/03-static-bake.md +49 -0
- package/docs/uk/04-ide-setup.md +26 -0
- package/docs/uk/05-why-mado.md +34 -0
- package/docs/uk/06-for-backenders.md +50 -0
- package/docs/uk/07-llm-pitfalls.md +82 -0
- package/docs/uk/08-llm-zero-history-test.md +31 -0
- package/docs/uk/09-shadow-vs-light-dom.md +40 -0
- package/docs/uk/README.md +16 -0
- package/llms.txt +155 -0
- package/package.json +81 -0
- package/scripts/bake.mjs +406 -0
- package/scripts/bundle.mjs +146 -0
- package/scripts/cli.mjs +382 -0
- package/scripts/new.mjs +80 -0
- package/scripts/preview.mjs +176 -0
- package/scripts/release-notes.mjs +66 -0
- package/scripts/showcase-regression.mjs +392 -0
- package/server/serve.mjs +292 -0
- package/starters/crud/README.md +21 -0
- package/starters/crud/index.html +20 -0
- package/starters/crud/package.json +17 -0
- package/starters/crud/src/components/app-shell.ts +51 -0
- package/starters/crud/src/components/ticket-detail.ts +33 -0
- package/starters/crud/src/components/ticket-form.ts +69 -0
- package/starters/crud/src/components/ticket-list.ts +66 -0
- package/starters/crud/src/lib/api.ts +76 -0
- package/starters/crud/src/main.ts +12 -0
- package/starters/crud/src/pages/home.ts +18 -0
- package/starters/crud/src/pages/not-found.ts +12 -0
- package/starters/crud/src/pages/ticket-detail.ts +6 -0
- package/starters/crud/src/pages/ticket-new.ts +6 -0
- package/starters/crud/src/pages/tickets.ts +6 -0
- package/starters/crud/src/routes.ts +9 -0
- package/starters/crud/src/styles/global.ts +155 -0
- package/starters/crud/tsconfig.json +15 -0
- package/starters/minimal/README.md +19 -0
- package/starters/minimal/index.html +20 -0
- package/starters/minimal/package.json +17 -0
- package/starters/minimal/src/components/app-counter.ts +31 -0
- package/starters/minimal/src/main.ts +9 -0
- package/starters/minimal/src/pages/home.ts +18 -0
- package/starters/minimal/src/pages/not-found.ts +14 -0
- package/starters/minimal/src/routes.ts +6 -0
- package/starters/minimal/src/styles/global.ts +60 -0
- package/starters/minimal/tsconfig.json +15 -0
- package/templates/page-detail.ts +63 -0
- package/templates/page-form.ts +94 -0
- package/templates/page-list.ts +79 -0
package/scripts/cli.mjs
ADDED
|
@@ -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
|
+
}
|
package/scripts/new.mjs
ADDED
|
@@ -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
|
+
}
|