@madojs/mado 0.6.0 → 0.7.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 +82 -30
- package/CHANGELOG.md +208 -1
- package/dist/src/component.d.ts +17 -4
- package/dist/src/component.js +26 -4
- package/dist/src/component.js.map +1 -1
- package/dist/src/resource.js +11 -0
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/manifest.js +29 -2
- package/dist/src/router/manifest.js.map +1 -1
- package/docs/en/07-llm-pitfalls.md +197 -60
- package/docs/en/08-llm-zero-history-test.md +1 -1
- package/docs/en/17-shadow-dom-forms.md +192 -0
- package/docs/en/README.md +20 -19
- package/docs/fr/07-llm-pitfalls.md +196 -60
- package/docs/fr/17-shadow-dom-forms.md +196 -0
- package/docs/fr/README.md +20 -19
- package/docs/ru/07-llm-pitfalls.md +198 -61
- package/docs/ru/08-llm-zero-history-test.md +39 -38
- package/docs/ru/09-shadow-vs-light-dom.md +97 -81
- package/docs/ru/17-shadow-dom-forms.md +193 -0
- package/docs/ru/README.md +20 -19
- package/docs/uk/07-llm-pitfalls.md +64 -3
- package/docs/uk/17-shadow-dom-forms.md +193 -0
- package/docs/uk/README.md +20 -19
- package/llms.txt +50 -1
- package/package.json +2 -2
- package/scripts/bake.mjs +76 -22
- package/scripts/bundle.mjs +24 -1
- package/scripts/cli.mjs +98 -45
- package/scripts/preview.mjs +104 -10
- package/server/serve.mjs +80 -7
- package/starters/admin/index.html +10 -3
- package/starters/admin/package.json +3 -1
- package/starters/admin/src/components/x-button.ts +40 -13
- package/starters/admin/src/components/x-input.ts +50 -19
- package/starters/admin/src/lib/api.ts +55 -4
- package/starters/admin/src/pages/admin/order-detail.ts +4 -2
- package/starters/admin/src/pages/home.ts +10 -1
- package/starters/crud/index.html +12 -4
- package/starters/crud/package.json +3 -1
- package/starters/crud/src/pages/home.ts +16 -0
- package/starters/minimal/index.html +12 -4
- package/starters/minimal/package.json +2 -0
- package/starters/minimal/src/pages/home.ts +17 -0
package/scripts/cli.mjs
CHANGED
|
@@ -53,10 +53,10 @@ switch (command) {
|
|
|
53
53
|
}
|
|
54
54
|
break;
|
|
55
55
|
case "serve":
|
|
56
|
-
await runServe(args
|
|
56
|
+
await runServe(args);
|
|
57
57
|
break;
|
|
58
58
|
case "dev":
|
|
59
|
-
await runDev(args
|
|
59
|
+
await runDev(args);
|
|
60
60
|
break;
|
|
61
61
|
case "bake":
|
|
62
62
|
await runNodeScript("scripts/bake.mjs", args);
|
|
@@ -87,15 +87,26 @@ switch (command) {
|
|
|
87
87
|
process.exit(1);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
async function runServe(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
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);
|
|
95
95
|
if (example) assertExample(example, { serveable: true });
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
|
|
97
|
+
// In app-mode (generated project, no example argument) we also go through
|
|
98
|
+
// server/serve.mjs to get config support (--host, --port, mado.config.json
|
|
99
|
+
// dev.proxy, HMR, etc.) — previously this fell back to serveStaticProject()
|
|
100
|
+
// which only read PORT from env and had no proxy/config/HMR.
|
|
101
|
+
await run(
|
|
102
|
+
process.execPath,
|
|
103
|
+
[join(PACKAGE_ROOT, "server/serve.mjs"), example, ...forwarded].filter(
|
|
104
|
+
Boolean,
|
|
105
|
+
),
|
|
106
|
+
{
|
|
107
|
+
env: { ...process.env, EXAMPLE: example || process.env.EXAMPLE || "" },
|
|
108
|
+
},
|
|
109
|
+
);
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
async function runInit(rawArgs) {
|
|
@@ -168,15 +179,25 @@ async function runInit(rawArgs) {
|
|
|
168
179
|
console.log("");
|
|
169
180
|
}
|
|
170
181
|
|
|
171
|
-
async function runDev(
|
|
182
|
+
async function runDev(rawArgs) {
|
|
183
|
+
// Forward unknown flags (e.g. --host, --port) to server/serve.mjs so callers
|
|
184
|
+
// can write `mado dev --host 127.0.0.1` without the CLI mistaking `--host`
|
|
185
|
+
// for an example name.
|
|
186
|
+
const { example, forwarded } = splitDevArgs(rawArgs);
|
|
172
187
|
if (example) assertExample(example, { serveable: true });
|
|
173
188
|
|
|
174
189
|
const env = { ...process.env, EXAMPLE: example || process.env.EXAMPLE || "" };
|
|
175
|
-
const server = spawn(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
const server = spawn(
|
|
191
|
+
process.execPath,
|
|
192
|
+
[join(PACKAGE_ROOT, "server/serve.mjs"), example, ...forwarded].filter(
|
|
193
|
+
Boolean,
|
|
194
|
+
),
|
|
195
|
+
{
|
|
196
|
+
cwd: PROJECT_ROOT,
|
|
197
|
+
env,
|
|
198
|
+
stdio: "inherit",
|
|
199
|
+
},
|
|
200
|
+
);
|
|
180
201
|
const tsc = spawn(process.execPath, [resolveBin("typescript/bin/tsc"), "-w"], {
|
|
181
202
|
cwd: PROJECT_ROOT,
|
|
182
203
|
stdio: "inherit",
|
|
@@ -215,7 +236,8 @@ async function runRelease(rawArgs) {
|
|
|
215
236
|
// have to remember the order, and so the deploy artifact (out/) is always
|
|
216
237
|
// assembled the same way.
|
|
217
238
|
//
|
|
218
|
-
// mado release
|
|
239
|
+
// mado release [--no-clean]
|
|
240
|
+
// → rm -rf out/ (unless --no-clean)
|
|
219
241
|
// → mado typecheck
|
|
220
242
|
// → mado build (tsc → dist/)
|
|
221
243
|
// → mado bundle (esbuild → out/assets/, also copies index.html)
|
|
@@ -223,6 +245,7 @@ async function runRelease(rawArgs) {
|
|
|
223
245
|
// → copy public/* → out/
|
|
224
246
|
//
|
|
225
247
|
// Flags are forwarded to bake/bundle.
|
|
248
|
+
const { flags: releaseFlags } = parseFlags(rawArgs);
|
|
226
249
|
const cfg = loadConfig({ projectRoot: PROJECT_ROOT });
|
|
227
250
|
const outDir = resolve(cfg.projectRoot, cfg.build.out ?? "out");
|
|
228
251
|
const publicDir = resolve(cfg.projectRoot, cfg.build.publicDir ?? "public");
|
|
@@ -231,6 +254,18 @@ async function runRelease(rawArgs) {
|
|
|
231
254
|
console.log(`[release] artifact: ${outDir}`);
|
|
232
255
|
console.log("");
|
|
233
256
|
|
|
257
|
+
// Deterministic builds: remove the entire output directory so stale assets,
|
|
258
|
+
// removed bake routes, and deleted public files don't linger in the deploy
|
|
259
|
+
// artifact. Use --no-clean to opt out (e.g. incremental CI workflows).
|
|
260
|
+
if (!releaseFlags["no-clean"]) {
|
|
261
|
+
if (existsSync(outDir)) {
|
|
262
|
+
await rm(outDir, { recursive: true, force: true });
|
|
263
|
+
console.log(`[release] cleaned ${outDir}`);
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
console.log("[release] --no-clean: keeping existing out/");
|
|
267
|
+
}
|
|
268
|
+
|
|
234
269
|
console.log("[release] step 1/5 typecheck");
|
|
235
270
|
await runNodeBin("typescript/bin/tsc", ["--noEmit"]);
|
|
236
271
|
|
|
@@ -411,6 +446,50 @@ function parseFlags(raw) {
|
|
|
411
446
|
return { flags, positional };
|
|
412
447
|
}
|
|
413
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Split args for `mado dev` / `mado serve` into:
|
|
451
|
+
* - example: the first non-flag positional (or undefined)
|
|
452
|
+
* - forwarded: every remaining token (flags, their values, leftover
|
|
453
|
+
* positionals), preserved in order so server/serve.mjs sees them
|
|
454
|
+
* unchanged.
|
|
455
|
+
*
|
|
456
|
+
* This is what lets `mado dev -- --host 127.0.0.1` and
|
|
457
|
+
* `mado dev showcase --port 6000` both work without the CLI confusing
|
|
458
|
+
* `--host` for an example name.
|
|
459
|
+
*/
|
|
460
|
+
function splitDevArgs(raw) {
|
|
461
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
462
|
+
return { example: "", forwarded: [] };
|
|
463
|
+
}
|
|
464
|
+
let example = "";
|
|
465
|
+
const forwarded = [];
|
|
466
|
+
let pickedExample = false;
|
|
467
|
+
for (let i = 0; i < raw.length; i++) {
|
|
468
|
+
const a = raw[i];
|
|
469
|
+
if (a === "--") {
|
|
470
|
+
forwarded.push(...raw.slice(i + 1));
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
if (a.startsWith("-")) {
|
|
474
|
+
forwarded.push(a);
|
|
475
|
+
// Lookahead: if the next token is the flag's VALUE (does not start with
|
|
476
|
+
// "-"), forward it too — but only when the flag is in inline form
|
|
477
|
+
// (--flag value), not --flag=value.
|
|
478
|
+
if (!a.includes("=") && raw[i + 1] !== undefined && !raw[i + 1].startsWith("-")) {
|
|
479
|
+
forwarded.push(raw[++i]);
|
|
480
|
+
}
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (!pickedExample) {
|
|
484
|
+
example = a;
|
|
485
|
+
pickedExample = true;
|
|
486
|
+
} else {
|
|
487
|
+
forwarded.push(a);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return { example, forwarded };
|
|
491
|
+
}
|
|
492
|
+
|
|
414
493
|
function packageNameFromDir(target) {
|
|
415
494
|
return target
|
|
416
495
|
.split(/[\\/]/)
|
|
@@ -463,31 +542,5 @@ function contentType(file) {
|
|
|
463
542
|
}[ext] ?? "application/octet-stream";
|
|
464
543
|
}
|
|
465
544
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const server = http.createServer((req, res) => {
|
|
469
|
-
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
470
|
-
const pathname = decodeURIComponent(url.pathname);
|
|
471
|
-
const normalized = pathname.replace(/^\/+/, "");
|
|
472
|
-
let file = resolve(rootDir, normalized);
|
|
473
|
-
if (pathname === "/" || !normalized.includes(".")) file = join(rootDir, "index.html");
|
|
474
|
-
if (!file.startsWith(rootDir) || !existsSync(file) || statSync(file).isDirectory()) {
|
|
475
|
-
file = join(rootDir, "index.html");
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (!existsSync(file)) {
|
|
479
|
-
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
480
|
-
res.end("Not found");
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
res.writeHead(200, { "content-type": contentType(file) });
|
|
485
|
-
res.end(readFileSync(file));
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
await new Promise((resolveListen) => {
|
|
489
|
-
server.listen(port, resolveListen);
|
|
490
|
-
});
|
|
491
|
-
console.log(`[mado] serving ${rootDir}`);
|
|
492
|
-
console.log(`[mado] http://localhost:${port}`);
|
|
493
|
-
}
|
|
545
|
+
// serveStaticProject removed in v0.7 — mado serve now always goes through
|
|
546
|
+
// server/serve.mjs to get --host, --port, dev.proxy, and HMR support.
|
package/scripts/preview.mjs
CHANGED
|
@@ -24,13 +24,49 @@ import { spawnSync } from "node:child_process";
|
|
|
24
24
|
|
|
25
25
|
import { loadConfig } from "./_config.mjs";
|
|
26
26
|
|
|
27
|
+
// Tiny argv parser. Supports --flag, --flag=value, --flag value.
|
|
28
|
+
function parsePreviewArgs(argv) {
|
|
29
|
+
const flags = {};
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const a = argv[i];
|
|
32
|
+
if (a === "--") continue;
|
|
33
|
+
if (a.startsWith("--")) {
|
|
34
|
+
const eq = a.indexOf("=");
|
|
35
|
+
if (eq >= 0) {
|
|
36
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
37
|
+
} else {
|
|
38
|
+
const name = a.slice(2);
|
|
39
|
+
const next = argv[i + 1];
|
|
40
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
41
|
+
flags[name] = next;
|
|
42
|
+
i++;
|
|
43
|
+
} else {
|
|
44
|
+
flags[name] = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return flags;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const PREVIEW_FLAGS = parsePreviewArgs(process.argv.slice(2));
|
|
53
|
+
|
|
27
54
|
const cfg = loadConfig({});
|
|
28
55
|
const ROOT = cfg.projectRoot;
|
|
29
56
|
const OUT = resolve(
|
|
30
57
|
ROOT,
|
|
31
58
|
process.env.OUT_DIR ?? cfg.build.out ?? "out",
|
|
32
59
|
);
|
|
33
|
-
|
|
60
|
+
// Baked HTML lives in <out>/baked/ by default (see scripts/bake.mjs and
|
|
61
|
+
// mado.config.json bake.outDir). Preview serves it with priority over the
|
|
62
|
+
// SPA shell so URLs that have a prerendered HTML page render real markup
|
|
63
|
+
// instead of an empty <div id="app"></div>.
|
|
64
|
+
const BAKED = resolve(
|
|
65
|
+
ROOT,
|
|
66
|
+
process.env.BAKED_DIR ?? cfg.bake?.outDir ?? join(cfg.build.out ?? "out", "baked"),
|
|
67
|
+
);
|
|
68
|
+
const PORT = Number(PREVIEW_FLAGS.port ?? process.env.PORT ?? cfg.dev?.port ?? 4173);
|
|
69
|
+
const HOST = String(PREVIEW_FLAGS.host ?? process.env.HOST ?? cfg.dev?.host ?? "localhost");
|
|
34
70
|
const AUTOBUILD = process.env.PREVIEW_AUTOBUILD === "1";
|
|
35
71
|
const SKIP_BUILD = process.env.SKIP_BUILD === "1" || !AUTOBUILD;
|
|
36
72
|
|
|
@@ -66,7 +102,18 @@ if (!SKIP_BUILD) {
|
|
|
66
102
|
}
|
|
67
103
|
|
|
68
104
|
if (!(await exists(OUT))) {
|
|
69
|
-
console.error(
|
|
105
|
+
console.error(
|
|
106
|
+
`[preview] missing ${OUT}/ — run \`mado release\` (or \`mado bundle\`) first.`,
|
|
107
|
+
);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const spaShell = join(OUT, "index.html");
|
|
112
|
+
if (!(await exists(spaShell))) {
|
|
113
|
+
console.error(
|
|
114
|
+
`[preview] missing ${spaShell} — \`mado bundle\` did not produce an HTML entry.\n` +
|
|
115
|
+
`[preview] Without it any non-baked route will 404 instead of falling back to the SPA.`,
|
|
116
|
+
);
|
|
70
117
|
process.exit(1);
|
|
71
118
|
}
|
|
72
119
|
|
|
@@ -114,8 +161,31 @@ const server = createServer(async (req, res) => {
|
|
|
114
161
|
}
|
|
115
162
|
});
|
|
116
163
|
|
|
117
|
-
server.
|
|
118
|
-
|
|
164
|
+
server.on("error", (err) => {
|
|
165
|
+
if (err.code === "EPERM" || err.code === "EACCES") {
|
|
166
|
+
console.error(
|
|
167
|
+
`[preview] failed to bind ${HOST}:${PORT}: ${err.message}\n` +
|
|
168
|
+
`[preview] tip: this sandbox may disallow binding "${HOST}".\n` +
|
|
169
|
+
`[preview] try: mado preview --host 127.0.0.1`,
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
console.error(
|
|
173
|
+
`[preview] failed to listen on ${HOST}:${PORT}: ${err.message}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
server.listen(PORT, HOST, async () => {
|
|
180
|
+
const urlHost = HOST === "0.0.0.0" || HOST === "::" ? "localhost" : HOST;
|
|
181
|
+
const bakedReady = await exists(BAKED);
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log("Mado preview (production-like)");
|
|
184
|
+
console.log(` url: http://${urlHost}:${PORT}/`);
|
|
185
|
+
console.log(` out: ${OUT}`);
|
|
186
|
+
console.log(` baked: ${bakedReady ? BAKED : "(none — SPA-only)"}`);
|
|
187
|
+
console.log(" (Ctrl-C to stop)");
|
|
188
|
+
console.log("");
|
|
119
189
|
});
|
|
120
190
|
|
|
121
191
|
// ---------- helpers ----------
|
|
@@ -145,14 +215,33 @@ function basenameSafe(p) {
|
|
|
145
215
|
async function resolveTarget(pathname) {
|
|
146
216
|
if (pathname === "/") pathname = "/index.html";
|
|
147
217
|
|
|
218
|
+
// 1) Baked HTML wins. `mado bake` writes prerendered pages into
|
|
219
|
+
// <out>/baked/<path>/index.html. Serve them with priority over the
|
|
220
|
+
// SPA shell so search engines AND human users hitting a prerendered
|
|
221
|
+
// URL see real content immediately. Without this branch preview
|
|
222
|
+
// served the empty SPA shell for every URL, which looked like a
|
|
223
|
+
// "blank page" bug even when bake had succeeded.
|
|
224
|
+
if (await exists(BAKED)) {
|
|
225
|
+
if (!extname(pathname) || pathname.endsWith("/index.html")) {
|
|
226
|
+
const bakedDir = join(BAKED, pathname.replace(/\/index\.html$/, ""));
|
|
227
|
+
const bakedIdx = join(bakedDir, "index.html");
|
|
228
|
+
if (await exists(bakedIdx)) return bakedIdx;
|
|
229
|
+
}
|
|
230
|
+
// Direct file (sitemap.xml etc.) from the baked dir.
|
|
231
|
+
const bakedFile = resolve(join(BAKED, pathname));
|
|
232
|
+
if (bakedFile.startsWith(BAKED + sep) && (await exists(bakedFile))) {
|
|
233
|
+
const s = await stat(bakedFile);
|
|
234
|
+
if (!s.isDirectory()) return bakedFile;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
148
238
|
const candidate = resolve(join(OUT, pathname));
|
|
149
239
|
if (!candidate.startsWith(OUT + sep) && candidate !== OUT) return null;
|
|
150
240
|
|
|
151
|
-
//
|
|
241
|
+
// 2) Exact match inside out/.
|
|
152
242
|
if (await exists(candidate)) {
|
|
153
243
|
const s = await stat(candidate);
|
|
154
244
|
if (s.isDirectory()) {
|
|
155
|
-
// Baked priority: /product/foo/ → /product/foo/index.html
|
|
156
245
|
const idx = join(candidate, "index.html");
|
|
157
246
|
if (await exists(idx)) return idx;
|
|
158
247
|
} else {
|
|
@@ -160,15 +249,20 @@ async function resolveTarget(pathname) {
|
|
|
160
249
|
}
|
|
161
250
|
}
|
|
162
251
|
|
|
163
|
-
//
|
|
252
|
+
// 3) /foo → /foo/index.html (for sub-folders without trailing slash).
|
|
164
253
|
if (!extname(pathname)) {
|
|
165
254
|
const asDir = join(OUT, pathname, "index.html");
|
|
166
255
|
if (await exists(asDir)) return asDir;
|
|
167
256
|
}
|
|
168
257
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
258
|
+
// 4) SPA-fallback: any non-asset path falls back to the SPA shell so
|
|
259
|
+
// client-side routing handles it. Asset-looking paths (with an
|
|
260
|
+
// extension) deliberately 404 instead — otherwise a 200 on
|
|
261
|
+
// /missing.png would mask real bugs.
|
|
262
|
+
if (!extname(pathname)) {
|
|
263
|
+
const spa = join(OUT, "index.html");
|
|
264
|
+
if (await exists(spa)) return spa;
|
|
265
|
+
}
|
|
172
266
|
|
|
173
267
|
return null;
|
|
174
268
|
}
|
package/server/serve.mjs
CHANGED
|
@@ -35,10 +35,49 @@ const CONFIG = (() => {
|
|
|
35
35
|
})();
|
|
36
36
|
const PROXY_RULES = Object.entries(CONFIG.dev?.proxy ?? {}); // [["/api", "http://localhost:3000"], ...]
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// Tiny argv parser. Supports --flag, --flag=value, --flag value. The first
|
|
39
|
+
// non-flag positional is the EXAMPLE name (legacy behavior). Anything that
|
|
40
|
+
// looks like a flag is consumed before we pick the positional, so calls like
|
|
41
|
+
// `mado dev -- --host 127.0.0.1` work as documented.
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const flags = {};
|
|
44
|
+
const positional = [];
|
|
45
|
+
for (let i = 0; i < argv.length; i++) {
|
|
46
|
+
const a = argv[i];
|
|
47
|
+
if (a === "--") continue;
|
|
48
|
+
if (a.startsWith("--")) {
|
|
49
|
+
const eq = a.indexOf("=");
|
|
50
|
+
if (eq >= 0) {
|
|
51
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
52
|
+
} else {
|
|
53
|
+
const name = a.slice(2);
|
|
54
|
+
const next = argv[i + 1];
|
|
55
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
56
|
+
flags[name] = next;
|
|
57
|
+
i++;
|
|
58
|
+
} else {
|
|
59
|
+
flags[name] = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
positional.push(a);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { flags, positional };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { flags: CLI_FLAGS, positional: CLI_POSITIONAL } = parseArgs(
|
|
70
|
+
process.argv.slice(2),
|
|
71
|
+
);
|
|
40
72
|
|
|
41
|
-
const
|
|
73
|
+
const PORT = Number(CLI_FLAGS.port ?? process.env.PORT ?? CONFIG.dev?.port ?? 5173);
|
|
74
|
+
// HOST is opt-in. Default to "localhost" (loopback) which is friendlier in
|
|
75
|
+
// sandboxes that disallow binding 0.0.0.0 (EPERM on listen). Users can opt
|
|
76
|
+
// into LAN exposure with `mado dev --host 0.0.0.0` or HOST=0.0.0.0.
|
|
77
|
+
const HOST = String(CLI_FLAGS.host ?? process.env.HOST ?? CONFIG.dev?.host ?? "localhost");
|
|
78
|
+
const HMR = CLI_FLAGS.hmr !== false && process.env.NO_HMR !== "1";
|
|
79
|
+
|
|
80
|
+
const EXAMPLE = CLI_POSITIONAL[0] ?? process.env.MADO_EXAMPLE ?? process.env.EXAMPLE ?? "";
|
|
42
81
|
const EXAMPLE_DIR = EXAMPLE
|
|
43
82
|
? resolve(join(ROOT, "examples", EXAMPLE))
|
|
44
83
|
: "";
|
|
@@ -185,7 +224,30 @@ const server = createServer(async (req, res) => {
|
|
|
185
224
|
const s = await stat(target);
|
|
186
225
|
if (s.isDirectory()) target = join(target, "index.html");
|
|
187
226
|
} catch {
|
|
188
|
-
|
|
227
|
+
// Public assets: in production `mado release` copies public/* into out/.
|
|
228
|
+
// In dev we surface them from public/ directly so favicon.svg, robots.txt,
|
|
229
|
+
// og-image.png, etc. don't 404 (and so dev/prod behavior matches).
|
|
230
|
+
const publicCandidate = resolve(join(ROOT, "public", pathname));
|
|
231
|
+
if (
|
|
232
|
+
publicCandidate.startsWith(resolve(join(ROOT, "public")) + sep) &&
|
|
233
|
+
existsSync(publicCandidate)
|
|
234
|
+
) {
|
|
235
|
+
try {
|
|
236
|
+
const ps = await stat(publicCandidate);
|
|
237
|
+
if (!ps.isDirectory()) {
|
|
238
|
+
target = publicCandidate;
|
|
239
|
+
reason = "public/";
|
|
240
|
+
} else if (!extname(pathname)) {
|
|
241
|
+
target = fallbackIndex;
|
|
242
|
+
} else {
|
|
243
|
+
reason = "file not found";
|
|
244
|
+
res.writeHead(404).end("not found");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
target = fallbackIndex;
|
|
249
|
+
}
|
|
250
|
+
} else if (!extname(pathname)) {
|
|
189
251
|
target = fallbackIndex;
|
|
190
252
|
} else {
|
|
191
253
|
reason = "file not found";
|
|
@@ -301,7 +363,15 @@ async function buildPreloadHints() {
|
|
|
301
363
|
}
|
|
302
364
|
|
|
303
365
|
server.on("error", (err) => {
|
|
304
|
-
|
|
366
|
+
if (err.code === "EPERM" || err.code === "EACCES") {
|
|
367
|
+
console.error(
|
|
368
|
+
`[serve] failed to bind ${HOST}:${PORT}: ${err.message}\n` +
|
|
369
|
+
`[serve] tip: this sandbox may disallow binding "${HOST}".\n` +
|
|
370
|
+
`[serve] try: mado dev --host 127.0.0.1 (or HOST=127.0.0.1)`,
|
|
371
|
+
);
|
|
372
|
+
} else {
|
|
373
|
+
console.error(`[serve] failed to listen on ${HOST}:${PORT}: ${err.message}`);
|
|
374
|
+
}
|
|
305
375
|
process.exit(1);
|
|
306
376
|
});
|
|
307
377
|
|
|
@@ -351,7 +421,7 @@ async function proxyForward({ req, res, prefix, upstream, pathname, search }) {
|
|
|
351
421
|
void prefix;
|
|
352
422
|
}
|
|
353
423
|
|
|
354
|
-
server.listen(PORT, () => {
|
|
424
|
+
server.listen(PORT, HOST, () => {
|
|
355
425
|
const distReady = existsSync(join(ROOT, "dist/src/index.js"))
|
|
356
426
|
|| existsSync(join(ROOT, "dist/main.js"));
|
|
357
427
|
const mount = EXAMPLE
|
|
@@ -359,9 +429,12 @@ server.listen(PORT, () => {
|
|
|
359
429
|
: existsSync(EXAMPLES_INDEX)
|
|
360
430
|
? "examples/index.html landing"
|
|
361
431
|
: "index.html app";
|
|
432
|
+
// Show "localhost" in the URL when bound to 0.0.0.0 — easier to click.
|
|
433
|
+
const urlHost = HOST === "0.0.0.0" || HOST === "::" ? "localhost" : HOST;
|
|
362
434
|
console.log("");
|
|
363
435
|
console.log("Mado dev server");
|
|
364
|
-
console.log(` url: http
|
|
436
|
+
console.log(` url: http://${urlHost}:${PORT}/`);
|
|
437
|
+
console.log(` host: ${HOST}`);
|
|
365
438
|
console.log(` root: ${ROOT}`);
|
|
366
439
|
console.log(` mount: ${mount}`);
|
|
367
440
|
console.log(` hmr: ${HMR ? "on" : "off"}`);
|
|
@@ -5,17 +5,24 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>__APP_NAME__</title>
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
+
<!--
|
|
9
|
+
Paths below MUST be root-absolute (start with "/"), not "./...".
|
|
10
|
+
Mado is an SPA: hard-refreshing /admin/orders/42 still serves this same
|
|
11
|
+
index.html, and the browser resolves "./dist/main.js" against the
|
|
12
|
+
current URL → /admin/orders/dist/main.js → 404 → blank page.
|
|
13
|
+
Root-absolute paths always resolve to /dist/main.js regardless of route.
|
|
14
|
+
-->
|
|
8
15
|
<script type="importmap">
|
|
9
16
|
{
|
|
10
17
|
"imports": {
|
|
11
|
-
"@madojs/mado": "
|
|
12
|
-
"@madojs/mado/": "
|
|
18
|
+
"@madojs/mado": "/node_modules/@madojs/mado/dist/src/index.js",
|
|
19
|
+
"@madojs/mado/": "/node_modules/@madojs/mado/dist/src/"
|
|
13
20
|
}
|
|
14
21
|
}
|
|
15
22
|
</script>
|
|
16
23
|
</head>
|
|
17
24
|
<body>
|
|
18
25
|
<div id="app"></div>
|
|
19
|
-
<script type="module" src="
|
|
26
|
+
<script type="module" src="/dist/main.js"></script>
|
|
20
27
|
</body>
|
|
21
28
|
</html>
|
|
@@ -1,25 +1,45 @@
|
|
|
1
1
|
// <x-button variant="primary|ghost|danger" ?disabled>
|
|
2
2
|
//
|
|
3
3
|
// Wraps a native <button> so it can be slotted with text/icon and styled
|
|
4
|
-
// consistently across the app.
|
|
5
|
-
//
|
|
4
|
+
// consistently across the app.
|
|
5
|
+
//
|
|
6
|
+
// Handles two Shadow DOM gotchas out of the box:
|
|
7
|
+
// 1. Reactive attributes via ctx.attr() — external ?disabled changes
|
|
8
|
+
// re-render the inner button automatically.
|
|
9
|
+
// 2. Form submit — a <button type="submit"> inside Shadow DOM cannot
|
|
10
|
+
// trigger <form> submit in Light DOM (spec limitation). We call
|
|
11
|
+
// form.requestSubmit() from a click handler to bridge this gap.
|
|
6
12
|
|
|
7
13
|
import { component, css, html } from "@madojs/mado";
|
|
8
14
|
|
|
9
15
|
component(
|
|
10
16
|
"x-button",
|
|
11
|
-
({ host }) =>
|
|
12
|
-
const variant =
|
|
13
|
-
const disabled =
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
({ host, attr }) => {
|
|
18
|
+
const variant = attr("variant", "primary");
|
|
19
|
+
const disabled = attr("disabled");
|
|
20
|
+
|
|
21
|
+
const handleClick = () => {
|
|
22
|
+
const typeAttr = host.getAttribute("type");
|
|
23
|
+
if (typeAttr === "button" || typeAttr === "reset") return;
|
|
24
|
+
const form = host.closest("form");
|
|
25
|
+
if (form && !host.hasAttribute("disabled")) form.requestSubmit();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return () => html`
|
|
29
|
+
<button
|
|
30
|
+
data-variant=${variant()}
|
|
31
|
+
?disabled=${() => disabled() !== ""}
|
|
32
|
+
@click=${handleClick}
|
|
33
|
+
>
|
|
16
34
|
<slot></slot>
|
|
17
35
|
</button>
|
|
18
36
|
`;
|
|
19
37
|
},
|
|
20
38
|
{
|
|
21
39
|
styles: css`
|
|
22
|
-
:host {
|
|
40
|
+
:host {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
}
|
|
23
43
|
button {
|
|
24
44
|
display: inline-flex;
|
|
25
45
|
align-items: center;
|
|
@@ -31,11 +51,18 @@ component(
|
|
|
31
51
|
cursor: pointer;
|
|
32
52
|
background: var(--accent);
|
|
33
53
|
color: var(--accent-fg);
|
|
34
|
-
transition: filter .12s ease;
|
|
54
|
+
transition: filter 0.12s ease;
|
|
55
|
+
}
|
|
56
|
+
button:hover:not(:disabled) {
|
|
57
|
+
filter: brightness(1.07);
|
|
58
|
+
}
|
|
59
|
+
button:active:not(:disabled) {
|
|
60
|
+
filter: brightness(0.95);
|
|
61
|
+
}
|
|
62
|
+
button:disabled {
|
|
63
|
+
opacity: 0.55;
|
|
64
|
+
cursor: not-allowed;
|
|
35
65
|
}
|
|
36
|
-
button:hover:not(:disabled) { filter: brightness(1.07); }
|
|
37
|
-
button:active:not(:disabled) { filter: brightness(.95); }
|
|
38
|
-
button:disabled { opacity: .55; cursor: not-allowed; }
|
|
39
66
|
|
|
40
67
|
button[data-variant="ghost"] {
|
|
41
68
|
background: transparent;
|
|
@@ -52,4 +79,4 @@ component(
|
|
|
52
79
|
}
|
|
53
80
|
`,
|
|
54
81
|
},
|
|
55
|
-
);
|
|
82
|
+
);
|