@madojs/mado 0.5.1 → 0.6.1
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 +26 -0
- package/CHANGELOG.md +265 -0
- package/MADO_V1_PLAN.md +179 -0
- package/README.md +31 -13
- package/ROADMAP.md +28 -7
- package/TODO.md +72 -0
- package/dist/src/forms.d.ts +37 -4
- package/dist/src/forms.js +331 -57
- package/dist/src/forms.js.map +1 -1
- package/dist/src/html/bindings.d.ts +41 -0
- package/dist/src/html/bindings.js +163 -6
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/html.d.ts +2 -0
- package/dist/src/html.js +1 -0
- package/dist/src/html.js.map +1 -1
- package/dist/src/index.d.ts +6 -6
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/page.d.ts +56 -0
- package/dist/src/page.js +17 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/resource.js +11 -0
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/manifest.d.ts +16 -1
- package/dist/src/router/manifest.js +210 -40
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +7 -2
- package/dist/src/router/match.js +14 -4
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.d.ts +10 -0
- package/dist/src/router/navigation.js +71 -3
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/signal.d.ts +15 -1
- package/dist/src/signal.js +112 -16
- package/dist/src/signal.js.map +1 -1
- package/docs/en/02-project-layout.md +99 -40
- package/docs/en/10-app-architecture.md +141 -0
- package/docs/en/11-layouts.md +115 -0
- package/docs/en/12-auth-and-api.md +217 -0
- package/docs/en/13-deployment.md +192 -0
- package/docs/en/14-testing.md +82 -0
- package/docs/en/15-error-handling.md +100 -0
- package/docs/en/16-bake-cookbook.md +93 -0
- package/docs/en/README.md +7 -0
- package/docs/fr/10-app-architecture.md +61 -0
- package/docs/fr/11-layouts.md +35 -0
- package/docs/fr/12-auth-and-api.md +35 -0
- package/docs/fr/13-deployment.md +39 -0
- package/docs/fr/14-testing.md +41 -0
- package/docs/fr/15-error-handling.md +50 -0
- package/docs/fr/16-bake-cookbook.md +35 -0
- package/docs/fr/README.md +7 -0
- package/docs/ru/10-app-architecture.md +100 -0
- package/docs/ru/11-layouts.md +47 -0
- package/docs/ru/12-auth-and-api.md +53 -0
- package/docs/ru/13-deployment.md +60 -0
- package/docs/ru/14-testing.md +50 -0
- package/docs/ru/15-error-handling.md +56 -0
- package/docs/ru/16-bake-cookbook.md +55 -0
- package/docs/ru/README.md +7 -0
- package/docs/uk/10-app-architecture.md +56 -0
- package/docs/uk/11-layouts.md +34 -0
- package/docs/uk/12-auth-and-api.md +34 -0
- package/docs/uk/13-deployment.md +39 -0
- package/docs/uk/14-testing.md +34 -0
- package/docs/uk/15-error-handling.md +32 -0
- package/docs/uk/16-bake-cookbook.md +36 -0
- package/docs/uk/README.md +7 -0
- package/llms.txt +9 -1
- package/package.json +3 -1
- package/scripts/_config.mjs +224 -0
- package/scripts/bake.mjs +266 -121
- package/scripts/bundle.mjs +133 -67
- package/scripts/cli.mjs +195 -27
- package/scripts/preview.mjs +125 -21
- package/server/serve.mjs +161 -10
- package/starters/admin/README.md +63 -0
- package/starters/admin/index.html +28 -0
- package/starters/admin/mado.config.json +22 -0
- package/starters/admin/package.json +24 -0
- package/starters/admin/public/favicon.svg +4 -0
- package/starters/admin/src/components/x-button.ts +55 -0
- package/starters/admin/src/components/x-input.ts +74 -0
- package/starters/admin/src/layouts/app.ts +101 -0
- package/starters/admin/src/layouts/auth.ts +41 -0
- package/starters/admin/src/lib/api.ts +133 -0
- package/starters/admin/src/lib/auth.ts +83 -0
- package/starters/admin/src/main.ts +15 -0
- package/starters/admin/src/pages/admin/dashboard.ts +48 -0
- package/starters/admin/src/pages/admin/order-detail.ts +80 -0
- package/starters/admin/src/pages/admin/orders.ts +117 -0
- package/starters/admin/src/pages/home.ts +34 -0
- package/starters/admin/src/pages/login.ts +70 -0
- package/starters/admin/src/pages/not-found.ts +12 -0
- package/starters/admin/src/routes.ts +40 -0
- package/starters/admin/src/styles/global.ts +86 -0
- package/starters/admin/tsconfig.json +15 -0
- package/starters/crud/index.html +12 -4
- package/starters/crud/mado.config.json +20 -0
- package/starters/crud/package.json +9 -3
- package/starters/crud/src/pages/home.ts +16 -0
- package/starters/crud/src/routes.ts +4 -2
- package/starters/minimal/index.html +12 -4
- package/starters/minimal/mado.config.json +20 -0
- package/starters/minimal/package.json +9 -3
- package/starters/minimal/src/pages/home.ts +17 -0
- package/starters/minimal/src/routes.ts +4 -2
package/scripts/bundle.mjs
CHANGED
|
@@ -1,40 +1,109 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Production bundle through esbuild. No build config files.
|
|
2
2
|
//
|
|
3
3
|
// Usage:
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// mado bundle
|
|
5
|
+
// mado bundle --entry src/main.ts --html index.html --out out
|
|
6
|
+
//
|
|
7
|
+
// Configuration precedence: built-in defaults < mado.config.json < CLI flags
|
|
8
|
+
// < legacy env vars (ENTRY, HTML, OUT_DIR).
|
|
6
9
|
//
|
|
7
10
|
// What it does:
|
|
8
|
-
// 1. Bundles entry with code splitting (each dynamic import →
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
11
|
+
// 1. Bundles `entry` with code splitting (each dynamic import → chunk),
|
|
12
|
+
// writing hashed `main-<hash>.js` and `chunk-<hash>.js` into <out>/assets/.
|
|
13
|
+
// 2. Computes SRI for the entry bundle.
|
|
14
|
+
// 3. Rewrites `html` so its <script type=module> points at the hashed entry,
|
|
15
|
+
// removes the dev importmap, and adds <link rel=modulepreload> for the
|
|
16
|
+
// entry and all chunks. Writes the result to <out>/index.html.
|
|
17
|
+
// 4. Pre-compresses every .js into .gz and .br for nginx_gzip_static and
|
|
18
|
+
// Cloudflare/Netlify Accept-Encoding.
|
|
19
|
+
// 5. Copies optional `favicon.ico`/`favicon.svg`/`assets/` from the project
|
|
20
|
+
// root if they exist (kept for backwards compatibility; new apps should
|
|
21
|
+
// put public assets in `public/` so `mado release` copies them).
|
|
12
22
|
//
|
|
13
|
-
//
|
|
23
|
+
// In repo-mode (the framework repo itself) the defaults still point at
|
|
24
|
+
// examples/showcase so the framework can dogfood its bundle pipeline against
|
|
25
|
+
// its biggest example.
|
|
14
26
|
|
|
15
27
|
import { build } from "esbuild";
|
|
16
|
-
import {
|
|
17
|
-
readFile,
|
|
18
|
-
writeFile,
|
|
19
|
-
mkdir,
|
|
20
|
-
cp,
|
|
21
|
-
stat,
|
|
22
|
-
readdir,
|
|
23
|
-
} from "node:fs/promises";
|
|
28
|
+
import { readFile, writeFile, mkdir, cp, stat, readdir, rm } from "node:fs/promises";
|
|
24
29
|
import { createHash } from "node:crypto";
|
|
25
30
|
import { gzipSync, brotliCompressSync, constants as zlibConst } from "node:zlib";
|
|
26
|
-
import { join, basename } from "node:path";
|
|
31
|
+
import { join, basename, resolve, dirname } from "node:path";
|
|
27
32
|
import { existsSync } from "node:fs";
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
const OUT_DIR = process.env.OUT_DIR ?? "out";
|
|
31
|
-
const HTML = process.env.HTML ?? "examples/showcase/index.html";
|
|
34
|
+
import { loadConfig, parseFlags, resolveProjectPath } from "./_config.mjs";
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
const { flags } = parseFlags(process.argv.slice(2));
|
|
37
|
+
const cfg = loadConfig({});
|
|
38
|
+
|
|
39
|
+
// Defaults are context-aware: in repo-mode they continue to bundle the
|
|
40
|
+
// showcase example; in app-mode they assume the canonical layout.
|
|
41
|
+
const defaultEntry = cfg.context === "repo"
|
|
42
|
+
? "examples/showcase/main.ts"
|
|
43
|
+
: "src/main.ts";
|
|
44
|
+
const defaultHtml = cfg.context === "repo"
|
|
45
|
+
? "examples/showcase/index.html"
|
|
46
|
+
: "index.html";
|
|
47
|
+
|
|
48
|
+
const ENTRY = resolveProjectPath(
|
|
49
|
+
cfg,
|
|
50
|
+
typeof flags.entry === "string" ? flags.entry : process.env.ENTRY ?? defaultEntry,
|
|
51
|
+
);
|
|
52
|
+
const HTML = resolveProjectPath(
|
|
53
|
+
cfg,
|
|
54
|
+
typeof flags.html === "string" ? flags.html : process.env.HTML ?? defaultHtml,
|
|
55
|
+
);
|
|
56
|
+
const OUT_DIR = resolveProjectPath(
|
|
57
|
+
cfg,
|
|
58
|
+
typeof flags.out === "string" ? flags.out : process.env.OUT_DIR ?? cfg.build.out ?? "out",
|
|
59
|
+
);
|
|
60
|
+
// Where the hashed bundles land. Apps want them under /assets/* to match
|
|
61
|
+
// nginx.conf and _headers; in repo-mode we keep the historical out/main-*.js
|
|
62
|
+
// layout so existing showcase pages continue to work.
|
|
63
|
+
const ASSETS_REL = cfg.context === "repo" ? "" : "assets";
|
|
64
|
+
const ASSETS_DIR = ASSETS_REL ? join(OUT_DIR, ASSETS_REL) : OUT_DIR;
|
|
65
|
+
|
|
66
|
+
if (!existsSync(ENTRY)) {
|
|
67
|
+
console.error(`[bundle] entry not found: ${ENTRY}`);
|
|
68
|
+
console.error("[bundle] set bundle entry in mado.config.json or pass --entry <file>");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
if (!existsSync(HTML)) {
|
|
72
|
+
console.error(`[bundle] html template not found: ${HTML}`);
|
|
73
|
+
console.error("[bundle] pass --html <file> or place index.html at the project root");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Clean stale assets from previous bundles.
|
|
78
|
+
//
|
|
79
|
+
// Without this step, hashed chunks from prior runs (main-<oldhash>.js,
|
|
80
|
+
// chunk-<oldhash>.js) accumulate in ASSETS_DIR. We later list ASSETS_DIR
|
|
81
|
+
// (via readdir below) and emit <link rel="modulepreload"> for every
|
|
82
|
+
// .js file we find — so stale chunks would be preloaded as if they were
|
|
83
|
+
// still part of the app, polluting the production HTML and shipping dead
|
|
84
|
+
// code over the wire. SRI is also only computed for the fresh entry, so
|
|
85
|
+
// stale preloads would lack integrity checks.
|
|
86
|
+
//
|
|
87
|
+
// In app-mode the entire <out>/assets/ folder is owned by the bundler,
|
|
88
|
+
// so wiping it is safe. In repo-mode the historical layout drops assets
|
|
89
|
+
// directly into <out>/ alongside non-bundle artifacts, so we only remove
|
|
90
|
+
// the recognisable hashed files there.
|
|
91
|
+
if (ASSETS_REL) {
|
|
92
|
+
await rm(ASSETS_DIR, { recursive: true, force: true });
|
|
93
|
+
} else if (existsSync(ASSETS_DIR)) {
|
|
94
|
+
for (const f of await readdir(ASSETS_DIR)) {
|
|
95
|
+
if (/^(main|chunk|asset)-[A-Z0-9]+\.(js|css)(\.map|\.gz|\.br)?$/i.test(f)) {
|
|
96
|
+
await rm(join(ASSETS_DIR, f), { force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
await mkdir(ASSETS_DIR, { recursive: true });
|
|
34
101
|
|
|
35
|
-
console.log(`[bundle] entry:
|
|
102
|
+
console.log(`[bundle] entry: ${ENTRY}`);
|
|
103
|
+
console.log(`[bundle] html: ${HTML}`);
|
|
104
|
+
console.log(`[bundle] out: ${OUT_DIR}`);
|
|
105
|
+
if (ASSETS_REL) console.log(`[bundle] assets: ${ASSETS_DIR}`);
|
|
36
106
|
|
|
37
|
-
// 1) esbuild with code splitting
|
|
38
107
|
const result = await build({
|
|
39
108
|
entryPoints: [ENTRY],
|
|
40
109
|
bundle: true,
|
|
@@ -43,7 +112,7 @@ const result = await build({
|
|
|
43
112
|
format: "esm",
|
|
44
113
|
target: "es2022",
|
|
45
114
|
splitting: true,
|
|
46
|
-
outdir:
|
|
115
|
+
outdir: ASSETS_DIR,
|
|
47
116
|
entryNames: "main-[hash]",
|
|
48
117
|
chunkNames: "chunk-[hash]",
|
|
49
118
|
assetNames: "asset-[hash]",
|
|
@@ -51,79 +120,76 @@ const result = await build({
|
|
|
51
120
|
legalComments: "none",
|
|
52
121
|
});
|
|
53
122
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!entryFile) {
|
|
123
|
+
const entryOutput = Object.entries(result.metafile.outputs).find(
|
|
124
|
+
([name, info]) => info.entryPoint && name.endsWith(".js"),
|
|
125
|
+
);
|
|
126
|
+
if (!entryOutput) {
|
|
59
127
|
console.error("[bundle] entry not found in outputs");
|
|
60
128
|
process.exit(1);
|
|
61
129
|
}
|
|
62
|
-
const mainBundle = basename(
|
|
130
|
+
const mainBundle = basename(entryOutput[0]);
|
|
63
131
|
|
|
64
|
-
//
|
|
65
|
-
const allJs = (await readdir(
|
|
132
|
+
// Collect all js chunks in the assets dir.
|
|
133
|
+
const allJs = (await readdir(ASSETS_DIR)).filter((f) => f.endsWith(".js"));
|
|
66
134
|
|
|
67
|
-
//
|
|
135
|
+
// Pre-compress every .js into .gz and .br.
|
|
68
136
|
let totalRaw = 0;
|
|
69
137
|
let totalGz = 0;
|
|
70
138
|
let totalBr = 0;
|
|
71
|
-
|
|
72
139
|
for (const f of allJs) {
|
|
73
|
-
const p = join(
|
|
140
|
+
const p = join(ASSETS_DIR, f);
|
|
74
141
|
const buf = await readFile(p);
|
|
75
142
|
totalRaw += buf.length;
|
|
76
|
-
|
|
77
143
|
const gz = gzipSync(buf, { level: 9 });
|
|
78
144
|
await writeFile(`${p}.gz`, gz);
|
|
79
145
|
totalGz += gz.length;
|
|
80
|
-
|
|
81
|
-
const br = brotliCompressSync(buf, {
|
|
82
|
-
params: { [zlibConst.BROTLI_PARAM_QUALITY]: 11 },
|
|
83
|
-
});
|
|
146
|
+
const br = brotliCompressSync(buf, { params: { [zlibConst.BROTLI_PARAM_QUALITY]: 11 } });
|
|
84
147
|
await writeFile(`${p}.br`, br);
|
|
85
148
|
totalBr += br.length;
|
|
86
149
|
}
|
|
87
150
|
|
|
88
|
-
//
|
|
89
|
-
const mainBuf = await readFile(join(
|
|
151
|
+
// SRI for the main bundle.
|
|
152
|
+
const mainBuf = await readFile(join(ASSETS_DIR, mainBundle));
|
|
90
153
|
const sri = "sha384-" + createHash("sha384").update(mainBuf).digest("base64");
|
|
91
154
|
|
|
92
|
-
//
|
|
155
|
+
// Rewrite HTML: drop dev importmap, swap the <script src>, add preloads.
|
|
156
|
+
const urlPrefix = ASSETS_REL ? `/${ASSETS_REL}/` : "/";
|
|
93
157
|
let html = await readFile(HTML, "utf8");
|
|
94
158
|
|
|
95
|
-
|
|
96
|
-
|
|
159
|
+
html = html.replace(/<script type="importmap">[\s\S]*?<\/script>/, "");
|
|
160
|
+
|
|
97
161
|
const preloads = allJs
|
|
98
162
|
.map(
|
|
99
163
|
(f) =>
|
|
100
|
-
` <link rel="modulepreload" href="
|
|
164
|
+
` <link rel="modulepreload" href="${urlPrefix}${f}"${
|
|
101
165
|
f === mainBundle ? ` integrity="${sri}" crossorigin="anonymous"` : ""
|
|
102
166
|
} />`,
|
|
103
167
|
)
|
|
104
168
|
.join("\n");
|
|
105
169
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
170
|
+
const scriptTag = `<script type="module" src="${urlPrefix}${mainBundle}" integrity="${sri}" crossorigin="anonymous"></script>`;
|
|
171
|
+
if (/<script\s+type="module"\s+src="[^"]+"[^>]*><\/script>/.test(html)) {
|
|
172
|
+
html = html.replace(
|
|
173
|
+
/<script\s+type="module"\s+src="[^"]+"[^>]*><\/script>/,
|
|
174
|
+
scriptTag,
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
// No matching dev <script> in the template: inject one before </body>.
|
|
178
|
+
html = html.replace(/<\/body>/i, ` ${scriptTag}\n </body>`);
|
|
179
|
+
}
|
|
114
180
|
|
|
115
|
-
|
|
116
|
-
html = html.replace(
|
|
117
|
-
/<\/head>/,
|
|
118
|
-
`${preloads}\n </head>`,
|
|
119
|
-
);
|
|
181
|
+
html = html.replace(/<\/head>/, `${preloads}\n </head>`);
|
|
120
182
|
|
|
183
|
+
await mkdir(OUT_DIR, { recursive: true });
|
|
121
184
|
await writeFile(join(OUT_DIR, "index.html"), html);
|
|
122
185
|
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
186
|
+
// Backwards-compatible asset copy (repo-mode only). In app-mode the
|
|
187
|
+
// `mado release` command copies the entire `public/` tree, which is the
|
|
188
|
+
// recommended path for new apps.
|
|
189
|
+
if (cfg.context === "repo") {
|
|
190
|
+
for (const name of ["favicon.ico", "favicon.svg", "assets"]) {
|
|
191
|
+
const src = join(cfg.projectRoot, "examples", name);
|
|
192
|
+
if (!existsSync(src)) continue;
|
|
127
193
|
const s = await stat(src);
|
|
128
194
|
if (s.isDirectory()) {
|
|
129
195
|
await cp(src, join(OUT_DIR, name), { recursive: true });
|
|
@@ -133,14 +199,14 @@ for (const name of ["favicon.ico", "favicon.svg", "assets"]) {
|
|
|
133
199
|
}
|
|
134
200
|
}
|
|
135
201
|
|
|
136
|
-
//
|
|
202
|
+
// Stats
|
|
137
203
|
const kib = (n) => (n / 1024).toFixed(1);
|
|
138
204
|
console.log(`[bundle] chunks: ${allJs.length}`);
|
|
139
205
|
for (const f of allJs.sort()) {
|
|
140
|
-
const sz = (await stat(join(
|
|
141
|
-
const gz = (await stat(join(
|
|
206
|
+
const sz = (await stat(join(ASSETS_DIR, f))).size;
|
|
207
|
+
const gz = (await stat(join(ASSETS_DIR, `${f}.gz`))).size;
|
|
142
208
|
const star = f === mainBundle ? " *" : "";
|
|
143
209
|
console.log(` ${f.padEnd(24)} ${kib(sz).padStart(6)} KB raw, ${kib(gz).padStart(5)} KB gz${star}`);
|
|
144
210
|
}
|
|
145
211
|
console.log(`[bundle] total: ${kib(totalRaw)} KB raw / ${kib(totalGz)} KB gz / ${kib(totalBr)} KB br`);
|
|
146
|
-
console.log(`[bundle] entry SRI: ${sri}`);
|
|
212
|
+
console.log(`[bundle] entry SRI: ${sri}`);
|
package/scripts/cli.mjs
CHANGED
|
@@ -2,23 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
5
|
-
import { copyFile, cp, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { copyFile, cp, mkdir, readdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
6
6
|
import http from "node:http";
|
|
7
7
|
import { dirname, join, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
|
+
import { detectContext, loadConfig } from "./_config.mjs";
|
|
11
|
+
|
|
10
12
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
13
|
const PROJECT_ROOT = resolve(process.cwd());
|
|
12
14
|
const PACKAGE_JSON = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
13
15
|
const [, , rawCommand, ...args] = process.argv;
|
|
14
16
|
|
|
17
|
+
// Context detection lives in _config.mjs so every script agrees on what
|
|
18
|
+
// "repo" vs "app" means. CLI uses it to pick safer defaults.
|
|
19
|
+
const CONTEXT = detectContext(PROJECT_ROOT);
|
|
20
|
+
const IS_REPO = CONTEXT === "repo";
|
|
21
|
+
|
|
15
22
|
const EXAMPLES = [
|
|
16
23
|
["basic", "minimal API tour"],
|
|
17
24
|
["tickets", "LLM zero-history CRUD validation"],
|
|
18
25
|
["showcase", "flagship SaaS CRM pressure app"],
|
|
19
26
|
["cloudflare", "Cloudflare Workers edge example"],
|
|
20
27
|
];
|
|
21
|
-
const STARTERS = ["minimal", "crud"];
|
|
28
|
+
const STARTERS = ["minimal", "crud", "admin"];
|
|
22
29
|
|
|
23
30
|
const command = rawCommand ?? "help";
|
|
24
31
|
|
|
@@ -39,15 +46,17 @@ switch (command) {
|
|
|
39
46
|
if (args[0] === "browser") {
|
|
40
47
|
await runNodeScript("scripts/showcase-regression.mjs", args.slice(1));
|
|
41
48
|
} else {
|
|
49
|
+
// Ensure dist/ is fresh so tests that import from ../dist/ work.
|
|
50
|
+
await runNodeBin("typescript/bin/tsc", []);
|
|
42
51
|
const files = await listTestFiles();
|
|
43
52
|
await run(process.execPath, ["--test", "--test-timeout=20000", ...files, ...args]);
|
|
44
53
|
}
|
|
45
54
|
break;
|
|
46
55
|
case "serve":
|
|
47
|
-
await runServe(args
|
|
56
|
+
await runServe(args);
|
|
48
57
|
break;
|
|
49
58
|
case "dev":
|
|
50
|
-
await runDev(args
|
|
59
|
+
await runDev(args);
|
|
51
60
|
break;
|
|
52
61
|
case "bake":
|
|
53
62
|
await runNodeScript("scripts/bake.mjs", args);
|
|
@@ -58,6 +67,9 @@ switch (command) {
|
|
|
58
67
|
case "preview":
|
|
59
68
|
await runNodeScript("scripts/preview.mjs", args);
|
|
60
69
|
break;
|
|
70
|
+
case "release":
|
|
71
|
+
await runRelease(args);
|
|
72
|
+
break;
|
|
61
73
|
case "new":
|
|
62
74
|
await runNodeScript("scripts/new.mjs", args);
|
|
63
75
|
break;
|
|
@@ -75,15 +87,25 @@ switch (command) {
|
|
|
75
87
|
process.exit(1);
|
|
76
88
|
}
|
|
77
89
|
|
|
78
|
-
async function runServe(
|
|
90
|
+
async function runServe(rawArgs) {
|
|
91
|
+
// Split args into [example?, ...flags]. The first non-flag positional is the
|
|
92
|
+
// example name; everything else (including `--host`, `--port`, etc.) is
|
|
93
|
+
// forwarded verbatim to server/serve.mjs.
|
|
94
|
+
const { example, forwarded } = splitDevArgs(rawArgs);
|
|
79
95
|
if (!example && PROJECT_ROOT !== PACKAGE_ROOT) {
|
|
80
96
|
await serveStaticProject(PROJECT_ROOT);
|
|
81
97
|
return;
|
|
82
98
|
}
|
|
83
99
|
if (example) assertExample(example, { serveable: true });
|
|
84
|
-
await run(
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
await run(
|
|
101
|
+
process.execPath,
|
|
102
|
+
[join(PACKAGE_ROOT, "server/serve.mjs"), example, ...forwarded].filter(
|
|
103
|
+
Boolean,
|
|
104
|
+
),
|
|
105
|
+
{
|
|
106
|
+
env: { ...process.env, EXAMPLE: example || process.env.EXAMPLE || "" },
|
|
107
|
+
},
|
|
108
|
+
);
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
async function runInit(rawArgs) {
|
|
@@ -156,15 +178,25 @@ async function runInit(rawArgs) {
|
|
|
156
178
|
console.log("");
|
|
157
179
|
}
|
|
158
180
|
|
|
159
|
-
async function runDev(
|
|
181
|
+
async function runDev(rawArgs) {
|
|
182
|
+
// Forward unknown flags (e.g. --host, --port) to server/serve.mjs so callers
|
|
183
|
+
// can write `mado dev --host 127.0.0.1` without the CLI mistaking `--host`
|
|
184
|
+
// for an example name.
|
|
185
|
+
const { example, forwarded } = splitDevArgs(rawArgs);
|
|
160
186
|
if (example) assertExample(example, { serveable: true });
|
|
161
187
|
|
|
162
188
|
const env = { ...process.env, EXAMPLE: example || process.env.EXAMPLE || "" };
|
|
163
|
-
const server = spawn(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
const server = spawn(
|
|
190
|
+
process.execPath,
|
|
191
|
+
[join(PACKAGE_ROOT, "server/serve.mjs"), example, ...forwarded].filter(
|
|
192
|
+
Boolean,
|
|
193
|
+
),
|
|
194
|
+
{
|
|
195
|
+
cwd: PROJECT_ROOT,
|
|
196
|
+
env,
|
|
197
|
+
stdio: "inherit",
|
|
198
|
+
},
|
|
199
|
+
);
|
|
168
200
|
const tsc = spawn(process.execPath, [resolveBin("typescript/bin/tsc"), "-w"], {
|
|
169
201
|
cwd: PROJECT_ROOT,
|
|
170
202
|
stdio: "inherit",
|
|
@@ -198,6 +230,79 @@ async function runDev(example) {
|
|
|
198
230
|
});
|
|
199
231
|
}
|
|
200
232
|
|
|
233
|
+
async function runRelease(rawArgs) {
|
|
234
|
+
// Single "ship it" command. Composes the smaller steps so the user does not
|
|
235
|
+
// have to remember the order, and so the deploy artifact (out/) is always
|
|
236
|
+
// assembled the same way.
|
|
237
|
+
//
|
|
238
|
+
// mado release
|
|
239
|
+
// → mado typecheck
|
|
240
|
+
// → mado build (tsc → dist/)
|
|
241
|
+
// → mado bundle (esbuild → out/assets/, also copies index.html)
|
|
242
|
+
// → mado bake (HTML → out/baked/)
|
|
243
|
+
// → copy public/* → out/
|
|
244
|
+
//
|
|
245
|
+
// Flags are forwarded to bake/bundle.
|
|
246
|
+
const cfg = loadConfig({ projectRoot: PROJECT_ROOT });
|
|
247
|
+
const outDir = resolve(cfg.projectRoot, cfg.build.out ?? "out");
|
|
248
|
+
const publicDir = resolve(cfg.projectRoot, cfg.build.publicDir ?? "public");
|
|
249
|
+
|
|
250
|
+
console.log(`[release] context: ${cfg.context}`);
|
|
251
|
+
console.log(`[release] artifact: ${outDir}`);
|
|
252
|
+
console.log("");
|
|
253
|
+
|
|
254
|
+
console.log("[release] step 1/5 typecheck");
|
|
255
|
+
await runNodeBin("typescript/bin/tsc", ["--noEmit"]);
|
|
256
|
+
|
|
257
|
+
console.log("[release] step 2/5 build (tsc → dist/)");
|
|
258
|
+
await runNodeBin("typescript/bin/tsc", []);
|
|
259
|
+
|
|
260
|
+
console.log("[release] step 3/5 bundle (esbuild → out/assets/)");
|
|
261
|
+
await runNodeScript("scripts/bundle.mjs", rawArgs);
|
|
262
|
+
|
|
263
|
+
console.log("[release] step 4/5 bake (out/baked/)");
|
|
264
|
+
await runNodeScript("scripts/bake.mjs", rawArgs);
|
|
265
|
+
|
|
266
|
+
console.log("[release] step 5/5 copy public/ → out/");
|
|
267
|
+
if (existsSync(publicDir)) {
|
|
268
|
+
await mkdir(outDir, { recursive: true });
|
|
269
|
+
await cp(publicDir, outDir, { recursive: true });
|
|
270
|
+
console.log(`[release] copied ${publicDir} → ${outDir}`);
|
|
271
|
+
} else {
|
|
272
|
+
console.log(`[release] no ${publicDir}, skipping`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Optional CDN config files. Generated only when not already provided.
|
|
276
|
+
await writeIfMissing(
|
|
277
|
+
join(outDir, "_redirects"),
|
|
278
|
+
// Cloudflare Pages / Netlify: SPA fallback so deep links work after a
|
|
279
|
+
// hard refresh. Baked HTML files are matched first because of
|
|
280
|
+
// `force: false` / static-priority rules on these hosts.
|
|
281
|
+
"/* /index.html 200\n",
|
|
282
|
+
);
|
|
283
|
+
await writeIfMissing(
|
|
284
|
+
join(outDir, "_headers"),
|
|
285
|
+
[
|
|
286
|
+
"/assets/*",
|
|
287
|
+
" Cache-Control: public, max-age=31536000, immutable",
|
|
288
|
+
"",
|
|
289
|
+
"/*.html",
|
|
290
|
+
" Cache-Control: no-cache, must-revalidate",
|
|
291
|
+
"",
|
|
292
|
+
].join("\n"),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
console.log("");
|
|
296
|
+
console.log(`[release] done. Deploy artifact: ${outDir}`);
|
|
297
|
+
console.log("[release] try: mado preview");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function writeIfMissing(path, content) {
|
|
301
|
+
if (existsSync(path)) return;
|
|
302
|
+
await writeFile(path, content);
|
|
303
|
+
console.log(`[release] wrote ${path}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
201
306
|
async function copyCanonicalAgentFiles(target) {
|
|
202
307
|
for (const file of ["AGENTS.md", "llms.txt"]) {
|
|
203
308
|
const source = join(PACKAGE_ROOT, file);
|
|
@@ -274,19 +379,38 @@ async function listTestFiles() {
|
|
|
274
379
|
}
|
|
275
380
|
|
|
276
381
|
function printHelp() {
|
|
277
|
-
|
|
278
|
-
mado
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
382
|
+
const ctx = IS_REPO ? "repo-mode (framework repository)" : "app-mode";
|
|
383
|
+
console.log(`mado commands (${ctx}):
|
|
384
|
+
|
|
385
|
+
Project lifecycle:
|
|
386
|
+
mado init <name> [--starter minimal|crud|admin] [--force]
|
|
387
|
+
scaffold a new app
|
|
388
|
+
mado dev tsc -w + dev server with HMR
|
|
389
|
+
mado build tsc (writes dist/)
|
|
390
|
+
mado typecheck tsc --noEmit
|
|
391
|
+
mado test [browser] run unit tests (or browser regression)
|
|
392
|
+
|
|
393
|
+
Production:
|
|
394
|
+
mado bundle esbuild → out/assets/ (hashed bundles)
|
|
395
|
+
mado bake [--entry <file>] [--template <html>] [--out <dir>] [--base-url <url>]
|
|
396
|
+
prerender baked routes → out/baked/
|
|
397
|
+
mado release typecheck + build + bundle + bake + copy public/ → out/
|
|
398
|
+
← the one command for "ship it"
|
|
399
|
+
mado preview serve exactly out/ locally (production rehearsal)
|
|
400
|
+
mado serve [example] simple static server (also runs in repo-mode for examples)
|
|
401
|
+
|
|
402
|
+
Generators:
|
|
403
|
+
mado new <list|form|detail> <name>
|
|
404
|
+
|
|
405
|
+
Misc:
|
|
406
|
+
mado examples list bundled examples
|
|
407
|
+
mado help this screen
|
|
408
|
+
|
|
409
|
+
Configuration:
|
|
410
|
+
mado reads ./mado.config.json (dev.proxy, build.out, bake.entry/template/baseUrl, …)
|
|
411
|
+
CLI flags > mado.config.json > built-in defaults.
|
|
412
|
+
|
|
413
|
+
See MADO_V1_PLAN.md for the road to v1.`);
|
|
290
414
|
}
|
|
291
415
|
|
|
292
416
|
function parseFlags(raw) {
|
|
@@ -307,6 +431,50 @@ function parseFlags(raw) {
|
|
|
307
431
|
return { flags, positional };
|
|
308
432
|
}
|
|
309
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Split args for `mado dev` / `mado serve` into:
|
|
436
|
+
* - example: the first non-flag positional (or undefined)
|
|
437
|
+
* - forwarded: every remaining token (flags, their values, leftover
|
|
438
|
+
* positionals), preserved in order so server/serve.mjs sees them
|
|
439
|
+
* unchanged.
|
|
440
|
+
*
|
|
441
|
+
* This is what lets `mado dev -- --host 127.0.0.1` and
|
|
442
|
+
* `mado dev showcase --port 6000` both work without the CLI confusing
|
|
443
|
+
* `--host` for an example name.
|
|
444
|
+
*/
|
|
445
|
+
function splitDevArgs(raw) {
|
|
446
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
447
|
+
return { example: "", forwarded: [] };
|
|
448
|
+
}
|
|
449
|
+
let example = "";
|
|
450
|
+
const forwarded = [];
|
|
451
|
+
let pickedExample = false;
|
|
452
|
+
for (let i = 0; i < raw.length; i++) {
|
|
453
|
+
const a = raw[i];
|
|
454
|
+
if (a === "--") {
|
|
455
|
+
forwarded.push(...raw.slice(i + 1));
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
if (a.startsWith("-")) {
|
|
459
|
+
forwarded.push(a);
|
|
460
|
+
// Lookahead: if the next token is the flag's VALUE (does not start with
|
|
461
|
+
// "-"), forward it too — but only when the flag is in inline form
|
|
462
|
+
// (--flag value), not --flag=value.
|
|
463
|
+
if (!a.includes("=") && raw[i + 1] !== undefined && !raw[i + 1].startsWith("-")) {
|
|
464
|
+
forwarded.push(raw[++i]);
|
|
465
|
+
}
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (!pickedExample) {
|
|
469
|
+
example = a;
|
|
470
|
+
pickedExample = true;
|
|
471
|
+
} else {
|
|
472
|
+
forwarded.push(a);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return { example, forwarded };
|
|
476
|
+
}
|
|
477
|
+
|
|
310
478
|
function packageNameFromDir(target) {
|
|
311
479
|
return target
|
|
312
480
|
.split(/[\\/]/)
|