@madojs/mado 0.5.0 → 0.6.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 +49 -1
- package/CHANGELOG.md +188 -0
- package/MADO_V1_PLAN.md +179 -0
- package/README.md +53 -14
- package/ROADMAP.md +36 -5
- package/TODO.md +72 -0
- package/dist/src/forms.d.ts +41 -7
- package/dist/src/forms.js +334 -59
- 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/router/manifest.d.ts +16 -1
- package/dist/src/router/manifest.js +181 -38
- 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 +73 -12
- 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/05-why-mado.md +1 -1
- package/docs/en/06-for-backenders.md +1 -1
- package/docs/en/07-llm-pitfalls.md +1 -1
- package/docs/en/09-shadow-vs-light-dom.md +60 -0
- 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/05-why-mado.md +1 -1
- package/docs/fr/06-for-backenders.md +1 -1
- package/docs/fr/07-llm-pitfalls.md +1 -1
- package/docs/fr/09-shadow-vs-light-dom.md +63 -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/05-why-mado.md +2 -2
- package/docs/ru/06-for-backenders.md +1 -1
- package/docs/ru/09-shadow-vs-light-dom.md +60 -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/06-for-backenders.md +2 -2
- package/docs/uk/09-shadow-vs-light-dom.md +91 -24
- 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 +24 -1
- package/package.json +3 -1
- package/scripts/_config.mjs +224 -0
- package/scripts/bake.mjs +217 -120
- package/scripts/bundle.mjs +110 -67
- package/scripts/cli.mjs +127 -16
- package/scripts/preview.mjs +22 -12
- package/server/serve.mjs +101 -11
- package/starters/admin/README.md +63 -0
- package/starters/admin/index.html +21 -0
- package/starters/admin/mado.config.json +22 -0
- package/starters/admin/package.json +22 -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 +78 -0
- package/starters/admin/src/pages/admin/orders.ts +117 -0
- package/starters/admin/src/pages/home.ts +25 -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/README.md +14 -2
- package/starters/crud/mado.config.json +20 -0
- package/starters/crud/package.json +9 -4
- package/starters/crud/src/components/app-shell.ts +13 -8
- package/starters/crud/src/main.ts +1 -4
- package/starters/crud/src/pages/ticket-detail.ts +1 -0
- package/starters/crud/src/pages/ticket-new.ts +1 -0
- package/starters/crud/src/pages/tickets.ts +1 -0
- package/starters/crud/src/routes.ts +4 -2
- package/starters/minimal/README.md +4 -2
- package/starters/minimal/mado.config.json +20 -0
- package/starters/minimal/package.json +8 -3
- package/starters/minimal/src/components/app-counter.ts +1 -1
- package/starters/minimal/src/routes.ts +4 -2
package/scripts/bundle.mjs
CHANGED
|
@@ -1,40 +1,86 @@
|
|
|
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 } 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/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({});
|
|
34
38
|
|
|
35
|
-
|
|
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
|
+
await mkdir(ASSETS_DIR, { recursive: true });
|
|
78
|
+
|
|
79
|
+
console.log(`[bundle] entry: ${ENTRY}`);
|
|
80
|
+
console.log(`[bundle] html: ${HTML}`);
|
|
81
|
+
console.log(`[bundle] out: ${OUT_DIR}`);
|
|
82
|
+
if (ASSETS_REL) console.log(`[bundle] assets: ${ASSETS_DIR}`);
|
|
36
83
|
|
|
37
|
-
// 1) esbuild with code splitting
|
|
38
84
|
const result = await build({
|
|
39
85
|
entryPoints: [ENTRY],
|
|
40
86
|
bundle: true,
|
|
@@ -43,7 +89,7 @@ const result = await build({
|
|
|
43
89
|
format: "esm",
|
|
44
90
|
target: "es2022",
|
|
45
91
|
splitting: true,
|
|
46
|
-
outdir:
|
|
92
|
+
outdir: ASSETS_DIR,
|
|
47
93
|
entryNames: "main-[hash]",
|
|
48
94
|
chunkNames: "chunk-[hash]",
|
|
49
95
|
assetNames: "asset-[hash]",
|
|
@@ -51,79 +97,76 @@ const result = await build({
|
|
|
51
97
|
legalComments: "none",
|
|
52
98
|
});
|
|
53
99
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!entryFile) {
|
|
100
|
+
const entryOutput = Object.entries(result.metafile.outputs).find(
|
|
101
|
+
([name, info]) => info.entryPoint && name.endsWith(".js"),
|
|
102
|
+
);
|
|
103
|
+
if (!entryOutput) {
|
|
59
104
|
console.error("[bundle] entry not found in outputs");
|
|
60
105
|
process.exit(1);
|
|
61
106
|
}
|
|
62
|
-
const mainBundle = basename(
|
|
107
|
+
const mainBundle = basename(entryOutput[0]);
|
|
63
108
|
|
|
64
|
-
//
|
|
65
|
-
const allJs = (await readdir(
|
|
109
|
+
// Collect all js chunks in the assets dir.
|
|
110
|
+
const allJs = (await readdir(ASSETS_DIR)).filter((f) => f.endsWith(".js"));
|
|
66
111
|
|
|
67
|
-
//
|
|
112
|
+
// Pre-compress every .js into .gz and .br.
|
|
68
113
|
let totalRaw = 0;
|
|
69
114
|
let totalGz = 0;
|
|
70
115
|
let totalBr = 0;
|
|
71
|
-
|
|
72
116
|
for (const f of allJs) {
|
|
73
|
-
const p = join(
|
|
117
|
+
const p = join(ASSETS_DIR, f);
|
|
74
118
|
const buf = await readFile(p);
|
|
75
119
|
totalRaw += buf.length;
|
|
76
|
-
|
|
77
120
|
const gz = gzipSync(buf, { level: 9 });
|
|
78
121
|
await writeFile(`${p}.gz`, gz);
|
|
79
122
|
totalGz += gz.length;
|
|
80
|
-
|
|
81
|
-
const br = brotliCompressSync(buf, {
|
|
82
|
-
params: { [zlibConst.BROTLI_PARAM_QUALITY]: 11 },
|
|
83
|
-
});
|
|
123
|
+
const br = brotliCompressSync(buf, { params: { [zlibConst.BROTLI_PARAM_QUALITY]: 11 } });
|
|
84
124
|
await writeFile(`${p}.br`, br);
|
|
85
125
|
totalBr += br.length;
|
|
86
126
|
}
|
|
87
127
|
|
|
88
|
-
//
|
|
89
|
-
const mainBuf = await readFile(join(
|
|
128
|
+
// SRI for the main bundle.
|
|
129
|
+
const mainBuf = await readFile(join(ASSETS_DIR, mainBundle));
|
|
90
130
|
const sri = "sha384-" + createHash("sha384").update(mainBuf).digest("base64");
|
|
91
131
|
|
|
92
|
-
//
|
|
132
|
+
// Rewrite HTML: drop dev importmap, swap the <script src>, add preloads.
|
|
133
|
+
const urlPrefix = ASSETS_REL ? `/${ASSETS_REL}/` : "/";
|
|
93
134
|
let html = await readFile(HTML, "utf8");
|
|
94
135
|
|
|
95
|
-
|
|
96
|
-
|
|
136
|
+
html = html.replace(/<script type="importmap">[\s\S]*?<\/script>/, "");
|
|
137
|
+
|
|
97
138
|
const preloads = allJs
|
|
98
139
|
.map(
|
|
99
140
|
(f) =>
|
|
100
|
-
` <link rel="modulepreload" href="
|
|
141
|
+
` <link rel="modulepreload" href="${urlPrefix}${f}"${
|
|
101
142
|
f === mainBundle ? ` integrity="${sri}" crossorigin="anonymous"` : ""
|
|
102
143
|
} />`,
|
|
103
144
|
)
|
|
104
145
|
.join("\n");
|
|
105
146
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
147
|
+
const scriptTag = `<script type="module" src="${urlPrefix}${mainBundle}" integrity="${sri}" crossorigin="anonymous"></script>`;
|
|
148
|
+
if (/<script\s+type="module"\s+src="[^"]+"[^>]*><\/script>/.test(html)) {
|
|
149
|
+
html = html.replace(
|
|
150
|
+
/<script\s+type="module"\s+src="[^"]+"[^>]*><\/script>/,
|
|
151
|
+
scriptTag,
|
|
152
|
+
);
|
|
153
|
+
} else {
|
|
154
|
+
// No matching dev <script> in the template: inject one before </body>.
|
|
155
|
+
html = html.replace(/<\/body>/i, ` ${scriptTag}\n </body>`);
|
|
156
|
+
}
|
|
114
157
|
|
|
115
|
-
|
|
116
|
-
html = html.replace(
|
|
117
|
-
/<\/head>/,
|
|
118
|
-
`${preloads}\n </head>`,
|
|
119
|
-
);
|
|
158
|
+
html = html.replace(/<\/head>/, `${preloads}\n </head>`);
|
|
120
159
|
|
|
160
|
+
await mkdir(OUT_DIR, { recursive: true });
|
|
121
161
|
await writeFile(join(OUT_DIR, "index.html"), html);
|
|
122
162
|
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
163
|
+
// Backwards-compatible asset copy (repo-mode only). In app-mode the
|
|
164
|
+
// `mado release` command copies the entire `public/` tree, which is the
|
|
165
|
+
// recommended path for new apps.
|
|
166
|
+
if (cfg.context === "repo") {
|
|
167
|
+
for (const name of ["favicon.ico", "favicon.svg", "assets"]) {
|
|
168
|
+
const src = join(cfg.projectRoot, "examples", name);
|
|
169
|
+
if (!existsSync(src)) continue;
|
|
127
170
|
const s = await stat(src);
|
|
128
171
|
if (s.isDirectory()) {
|
|
129
172
|
await cp(src, join(OUT_DIR, name), { recursive: true });
|
|
@@ -133,14 +176,14 @@ for (const name of ["favicon.ico", "favicon.svg", "assets"]) {
|
|
|
133
176
|
}
|
|
134
177
|
}
|
|
135
178
|
|
|
136
|
-
//
|
|
179
|
+
// Stats
|
|
137
180
|
const kib = (n) => (n / 1024).toFixed(1);
|
|
138
181
|
console.log(`[bundle] chunks: ${allJs.length}`);
|
|
139
182
|
for (const f of allJs.sort()) {
|
|
140
|
-
const sz = (await stat(join(
|
|
141
|
-
const gz = (await stat(join(
|
|
183
|
+
const sz = (await stat(join(ASSETS_DIR, f))).size;
|
|
184
|
+
const gz = (await stat(join(ASSETS_DIR, `${f}.gz`))).size;
|
|
142
185
|
const star = f === mainBundle ? " *" : "";
|
|
143
186
|
console.log(` ${f.padEnd(24)} ${kib(sz).padStart(6)} KB raw, ${kib(gz).padStart(5)} KB gz${star}`);
|
|
144
187
|
}
|
|
145
188
|
console.log(`[bundle] total: ${kib(totalRaw)} KB raw / ${kib(totalGz)} KB gz / ${kib(totalBr)} KB br`);
|
|
146
|
-
console.log(`[bundle] entry SRI: ${sri}`);
|
|
189
|
+
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,8 +46,10 @@ 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
|
-
await run(process.execPath, ["--test", "--test-timeout=
|
|
52
|
+
await run(process.execPath, ["--test", "--test-timeout=20000", ...files, ...args]);
|
|
44
53
|
}
|
|
45
54
|
break;
|
|
46
55
|
case "serve":
|
|
@@ -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;
|
|
@@ -120,6 +132,7 @@ async function runInit(rawArgs) {
|
|
|
120
132
|
await mkdir(target, { recursive: true });
|
|
121
133
|
await cp(source, target, { recursive: true, force: true });
|
|
122
134
|
await copyCanonicalAgentFiles(target);
|
|
135
|
+
await ensureStarterGitignore(target);
|
|
123
136
|
|
|
124
137
|
const packageName = packageNameFromDir(target);
|
|
125
138
|
if (!isValidPackageName(packageName)) {
|
|
@@ -197,6 +210,79 @@ async function runDev(example) {
|
|
|
197
210
|
});
|
|
198
211
|
}
|
|
199
212
|
|
|
213
|
+
async function runRelease(rawArgs) {
|
|
214
|
+
// Single "ship it" command. Composes the smaller steps so the user does not
|
|
215
|
+
// have to remember the order, and so the deploy artifact (out/) is always
|
|
216
|
+
// assembled the same way.
|
|
217
|
+
//
|
|
218
|
+
// mado release
|
|
219
|
+
// → mado typecheck
|
|
220
|
+
// → mado build (tsc → dist/)
|
|
221
|
+
// → mado bundle (esbuild → out/assets/, also copies index.html)
|
|
222
|
+
// → mado bake (HTML → out/baked/)
|
|
223
|
+
// → copy public/* → out/
|
|
224
|
+
//
|
|
225
|
+
// Flags are forwarded to bake/bundle.
|
|
226
|
+
const cfg = loadConfig({ projectRoot: PROJECT_ROOT });
|
|
227
|
+
const outDir = resolve(cfg.projectRoot, cfg.build.out ?? "out");
|
|
228
|
+
const publicDir = resolve(cfg.projectRoot, cfg.build.publicDir ?? "public");
|
|
229
|
+
|
|
230
|
+
console.log(`[release] context: ${cfg.context}`);
|
|
231
|
+
console.log(`[release] artifact: ${outDir}`);
|
|
232
|
+
console.log("");
|
|
233
|
+
|
|
234
|
+
console.log("[release] step 1/5 typecheck");
|
|
235
|
+
await runNodeBin("typescript/bin/tsc", ["--noEmit"]);
|
|
236
|
+
|
|
237
|
+
console.log("[release] step 2/5 build (tsc → dist/)");
|
|
238
|
+
await runNodeBin("typescript/bin/tsc", []);
|
|
239
|
+
|
|
240
|
+
console.log("[release] step 3/5 bundle (esbuild → out/assets/)");
|
|
241
|
+
await runNodeScript("scripts/bundle.mjs", rawArgs);
|
|
242
|
+
|
|
243
|
+
console.log("[release] step 4/5 bake (out/baked/)");
|
|
244
|
+
await runNodeScript("scripts/bake.mjs", rawArgs);
|
|
245
|
+
|
|
246
|
+
console.log("[release] step 5/5 copy public/ → out/");
|
|
247
|
+
if (existsSync(publicDir)) {
|
|
248
|
+
await mkdir(outDir, { recursive: true });
|
|
249
|
+
await cp(publicDir, outDir, { recursive: true });
|
|
250
|
+
console.log(`[release] copied ${publicDir} → ${outDir}`);
|
|
251
|
+
} else {
|
|
252
|
+
console.log(`[release] no ${publicDir}, skipping`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Optional CDN config files. Generated only when not already provided.
|
|
256
|
+
await writeIfMissing(
|
|
257
|
+
join(outDir, "_redirects"),
|
|
258
|
+
// Cloudflare Pages / Netlify: SPA fallback so deep links work after a
|
|
259
|
+
// hard refresh. Baked HTML files are matched first because of
|
|
260
|
+
// `force: false` / static-priority rules on these hosts.
|
|
261
|
+
"/* /index.html 200\n",
|
|
262
|
+
);
|
|
263
|
+
await writeIfMissing(
|
|
264
|
+
join(outDir, "_headers"),
|
|
265
|
+
[
|
|
266
|
+
"/assets/*",
|
|
267
|
+
" Cache-Control: public, max-age=31536000, immutable",
|
|
268
|
+
"",
|
|
269
|
+
"/*.html",
|
|
270
|
+
" Cache-Control: no-cache, must-revalidate",
|
|
271
|
+
"",
|
|
272
|
+
].join("\n"),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
console.log("");
|
|
276
|
+
console.log(`[release] done. Deploy artifact: ${outDir}`);
|
|
277
|
+
console.log("[release] try: mado preview");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function writeIfMissing(path, content) {
|
|
281
|
+
if (existsSync(path)) return;
|
|
282
|
+
await writeFile(path, content);
|
|
283
|
+
console.log(`[release] wrote ${path}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
200
286
|
async function copyCanonicalAgentFiles(target) {
|
|
201
287
|
for (const file of ["AGENTS.md", "llms.txt"]) {
|
|
202
288
|
const source = join(PACKAGE_ROOT, file);
|
|
@@ -204,6 +290,12 @@ async function copyCanonicalAgentFiles(target) {
|
|
|
204
290
|
}
|
|
205
291
|
}
|
|
206
292
|
|
|
293
|
+
async function ensureStarterGitignore(target) {
|
|
294
|
+
const file = join(target, ".gitignore");
|
|
295
|
+
if (existsSync(file)) return;
|
|
296
|
+
await writeFile(file, "node_modules\ndist\nout\n.DS_Store\n*.log\n");
|
|
297
|
+
}
|
|
298
|
+
|
|
207
299
|
async function runNodeBin(bin, args) {
|
|
208
300
|
await run(process.execPath, [resolveBin(bin), ...args]);
|
|
209
301
|
}
|
|
@@ -267,19 +359,38 @@ async function listTestFiles() {
|
|
|
267
359
|
}
|
|
268
360
|
|
|
269
361
|
function printHelp() {
|
|
270
|
-
|
|
271
|
-
mado
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
362
|
+
const ctx = IS_REPO ? "repo-mode (framework repository)" : "app-mode";
|
|
363
|
+
console.log(`mado commands (${ctx}):
|
|
364
|
+
|
|
365
|
+
Project lifecycle:
|
|
366
|
+
mado init <name> [--starter minimal|crud|admin] [--force]
|
|
367
|
+
scaffold a new app
|
|
368
|
+
mado dev tsc -w + dev server with HMR
|
|
369
|
+
mado build tsc (writes dist/)
|
|
370
|
+
mado typecheck tsc --noEmit
|
|
371
|
+
mado test [browser] run unit tests (or browser regression)
|
|
372
|
+
|
|
373
|
+
Production:
|
|
374
|
+
mado bundle esbuild → out/assets/ (hashed bundles)
|
|
375
|
+
mado bake [--entry <file>] [--template <html>] [--out <dir>] [--base-url <url>]
|
|
376
|
+
prerender baked routes → out/baked/
|
|
377
|
+
mado release typecheck + build + bundle + bake + copy public/ → out/
|
|
378
|
+
← the one command for "ship it"
|
|
379
|
+
mado preview serve exactly out/ locally (production rehearsal)
|
|
380
|
+
mado serve [example] simple static server (also runs in repo-mode for examples)
|
|
381
|
+
|
|
382
|
+
Generators:
|
|
383
|
+
mado new <list|form|detail> <name>
|
|
384
|
+
|
|
385
|
+
Misc:
|
|
386
|
+
mado examples list bundled examples
|
|
387
|
+
mado help this screen
|
|
388
|
+
|
|
389
|
+
Configuration:
|
|
390
|
+
mado reads ./mado.config.json (dev.proxy, build.out, bake.entry/template/baseUrl, …)
|
|
391
|
+
CLI flags > mado.config.json > built-in defaults.
|
|
392
|
+
|
|
393
|
+
See MADO_V1_PLAN.md for the road to v1.`);
|
|
283
394
|
}
|
|
284
395
|
|
|
285
396
|
function parseFlags(raw) {
|
package/scripts/preview.mjs
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
|
-
// Preview: a tiny production-like server that
|
|
1
|
+
// Preview: a tiny production-like server that serves exactly out/ on node:http.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// mado preview
|
|
4
4
|
//
|
|
5
5
|
// What it does:
|
|
6
|
-
// 1.
|
|
7
|
-
// 2.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// 1. Reads `mado.config.json` to discover OUT (default `out/`) and PORT.
|
|
7
|
+
// 2. If `out/` is missing AND we are in a project root, refuses to run and
|
|
8
|
+
// points the user at `mado release`. (Old auto-build behavior is opt-in
|
|
9
|
+
// via PREVIEW_AUTOBUILD=1 to stay backward-compatible for the framework
|
|
10
|
+
// repo.)
|
|
11
|
+
// 3. Starts a static server with:
|
|
10
12
|
// - immutable cache for hashed bundles;
|
|
11
13
|
// - SPA fallback to index.html;
|
|
12
|
-
// - baked HTML priority over
|
|
14
|
+
// - baked HTML priority over the SPA shell;
|
|
13
15
|
// - precompressed .gz / .br serving via Accept-Encoding.
|
|
14
16
|
//
|
|
15
|
-
// Goal: see production-like output locally without Docker/nginx
|
|
17
|
+
// Goal: see production-like output locally without Docker/nginx, identical to
|
|
18
|
+
// what a static host (nginx / Cloudflare Pages / S3) would serve.
|
|
16
19
|
|
|
17
20
|
import { createServer } from "node:http";
|
|
18
21
|
import { readFile, stat, access } from "node:fs/promises";
|
|
19
22
|
import { extname, join, resolve, sep } from "node:path";
|
|
20
23
|
import { spawnSync } from "node:child_process";
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
25
|
+
import { loadConfig } from "./_config.mjs";
|
|
26
|
+
|
|
27
|
+
const cfg = loadConfig({});
|
|
28
|
+
const ROOT = cfg.projectRoot;
|
|
29
|
+
const OUT = resolve(
|
|
30
|
+
ROOT,
|
|
31
|
+
process.env.OUT_DIR ?? cfg.build.out ?? "out",
|
|
32
|
+
);
|
|
33
|
+
const PORT = Number(process.env.PORT ?? cfg.dev?.port ?? 4173);
|
|
34
|
+
const AUTOBUILD = process.env.PREVIEW_AUTOBUILD === "1";
|
|
35
|
+
const SKIP_BUILD = process.env.SKIP_BUILD === "1" || !AUTOBUILD;
|
|
26
36
|
|
|
27
37
|
const MIME = {
|
|
28
38
|
".html": "text/html; charset=utf-8",
|