@madojs/mado 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/AGENTS.md +15 -8
  2. package/CHANGELOG.md +58 -0
  3. package/README.md +25 -3
  4. package/dist/src/component.d.ts +3 -1
  5. package/dist/src/component.d.ts.map +1 -0
  6. package/dist/src/component.js +65 -30
  7. package/dist/src/component.js.map +1 -1
  8. package/dist/src/context.d.ts +13 -32
  9. package/dist/src/context.d.ts.map +1 -0
  10. package/dist/src/context.js +73 -48
  11. package/dist/src/context.js.map +1 -1
  12. package/dist/src/css.d.ts +1 -0
  13. package/dist/src/css.d.ts.map +1 -0
  14. package/dist/src/css.js.map +1 -1
  15. package/dist/src/devtools-hook.d.ts +14 -0
  16. package/dist/src/devtools-hook.d.ts.map +1 -0
  17. package/dist/src/devtools-hook.js +27 -0
  18. package/dist/src/devtools-hook.js.map +1 -0
  19. package/dist/src/devtools.d.ts +24 -22
  20. package/dist/src/devtools.d.ts.map +1 -0
  21. package/dist/src/devtools.js +230 -58
  22. package/dist/src/devtools.js.map +1 -1
  23. package/dist/src/diagnostics.d.ts +12 -0
  24. package/dist/src/diagnostics.d.ts.map +1 -0
  25. package/dist/src/diagnostics.js +68 -8
  26. package/dist/src/diagnostics.js.map +1 -1
  27. package/dist/src/each.d.ts +1 -5
  28. package/dist/src/each.d.ts.map +1 -0
  29. package/dist/src/each.js +0 -8
  30. package/dist/src/each.js.map +1 -1
  31. package/dist/src/forms.d.ts +27 -96
  32. package/dist/src/forms.d.ts.map +1 -0
  33. package/dist/src/forms.js +184 -394
  34. package/dist/src/forms.js.map +1 -1
  35. package/dist/src/head.d.ts +1 -0
  36. package/dist/src/head.d.ts.map +1 -0
  37. package/dist/src/head.js +2 -1
  38. package/dist/src/head.js.map +1 -1
  39. package/dist/src/html/bindings.d.ts +1 -0
  40. package/dist/src/html/bindings.d.ts.map +1 -0
  41. package/dist/src/html/bindings.js.map +1 -1
  42. package/dist/src/html/parser.d.ts +3 -4
  43. package/dist/src/html/parser.d.ts.map +1 -0
  44. package/dist/src/html/parser.js +63 -14
  45. package/dist/src/html/parser.js.map +1 -1
  46. package/dist/src/html/template-types.d.ts +1 -0
  47. package/dist/src/html/template-types.d.ts.map +1 -0
  48. package/dist/src/html/template-types.js.map +1 -1
  49. package/dist/src/html/template.d.ts +5 -1
  50. package/dist/src/html/template.d.ts.map +1 -0
  51. package/dist/src/html/template.js +78 -3
  52. package/dist/src/html/template.js.map +1 -1
  53. package/dist/src/index.d.ts +7 -8
  54. package/dist/src/index.d.ts.map +1 -0
  55. package/dist/src/index.js +5 -7
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/json.d.ts +3 -0
  58. package/dist/src/json.d.ts.map +1 -0
  59. package/dist/src/json.js +14 -0
  60. package/dist/src/json.js.map +1 -0
  61. package/dist/src/lifecycle.d.ts +1 -0
  62. package/dist/src/lifecycle.d.ts.map +1 -0
  63. package/dist/src/lifecycle.js +3 -2
  64. package/dist/src/lifecycle.js.map +1 -1
  65. package/dist/src/page.d.ts +1 -0
  66. package/dist/src/page.d.ts.map +1 -0
  67. package/dist/src/page.js.map +1 -1
  68. package/dist/src/persisted.d.ts +6 -1
  69. package/dist/src/persisted.d.ts.map +1 -0
  70. package/dist/src/persisted.js +17 -11
  71. package/dist/src/persisted.js.map +1 -1
  72. package/dist/src/resource.d.ts +2 -1
  73. package/dist/src/resource.d.ts.map +1 -0
  74. package/dist/src/resource.js +126 -49
  75. package/dist/src/resource.js.map +1 -1
  76. package/dist/src/router/base.d.ts +1 -0
  77. package/dist/src/router/base.d.ts.map +1 -0
  78. package/dist/src/router/base.js.map +1 -1
  79. package/dist/src/router/manifest.d.ts +1 -0
  80. package/dist/src/router/manifest.d.ts.map +1 -0
  81. package/dist/src/router/manifest.js +89 -148
  82. package/dist/src/router/manifest.js.map +1 -1
  83. package/dist/src/router/match.d.ts +1 -0
  84. package/dist/src/router/match.d.ts.map +1 -0
  85. package/dist/src/router/match.js +19 -5
  86. package/dist/src/router/match.js.map +1 -1
  87. package/dist/src/router/navigation.d.ts +1 -0
  88. package/dist/src/router/navigation.d.ts.map +1 -0
  89. package/dist/src/router/navigation.js.map +1 -1
  90. package/dist/src/signal.d.ts +1 -0
  91. package/dist/src/signal.d.ts.map +1 -0
  92. package/dist/src/signal.js +46 -17
  93. package/dist/src/signal.js.map +1 -1
  94. package/dist/src/static-runtime.d.ts +1 -0
  95. package/dist/src/static-runtime.d.ts.map +1 -0
  96. package/dist/src/static-runtime.js.map +1 -1
  97. package/dist/src/vite/index.d.ts +1 -0
  98. package/dist/src/vite/index.d.ts.map +1 -0
  99. package/dist/src/vite/index.js +4 -3
  100. package/dist/src/vite/index.js.map +1 -1
  101. package/docs/architecture/v1-roadmap.md +19 -0
  102. package/docs/en/10-pages-and-components.md +9 -1
  103. package/docs/en/13-data.md +8 -6
  104. package/docs/en/14-forms.md +66 -219
  105. package/docs/en/15-static-snapshots.md +4 -4
  106. package/docs/en/16-app-architecture.md +3 -2
  107. package/docs/en/20-deployment.md +21 -8
  108. package/docs/en/21-error-handling.md +9 -6
  109. package/docs/en/24-devtools-and-diagnostics.md +45 -0
  110. package/docs/en/30-api-freeze-map.md +8 -9
  111. package/docs/en/32-v1-stability.md +2 -2
  112. package/docs/en/33-migration-0.12-0.13.md +56 -0
  113. package/docs/en/40-llm-guide.md +7 -9
  114. package/docs/en/41-for-backenders.md +13 -9
  115. package/docs/en/42-why-mado.md +2 -2
  116. package/docs/en/README.md +2 -0
  117. package/docs/recipes/nginx/nginx.conf +2 -2
  118. package/llms.txt +25 -7
  119. package/package.json +37 -16
  120. package/scripts/clean.mjs +8 -0
  121. package/scripts/cli/generate.mjs +11 -14
  122. package/scripts/cli/help.mjs +2 -0
  123. package/scripts/cli/index.mjs +14 -6
  124. package/scripts/cli/init.mjs +13 -18
  125. package/scripts/cli/release.mjs +22 -19
  126. package/scripts/cli/run.mjs +5 -3
  127. package/scripts/logger.mjs +117 -0
  128. package/scripts/output-guard.mjs +128 -0
  129. package/scripts/preview.mjs +11 -19
  130. package/scripts/static/browser.mjs +49 -8
  131. package/scripts/static/discover.mjs +4 -1
  132. package/scripts/static.mjs +21 -7
  133. package/starters/default/package.json +4 -4
  134. package/starters/default/tsconfig.node.json +13 -0
  135. package/starters/default/vite.config.ts +0 -3
  136. package/starters/modular/README.md +4 -4
  137. package/starters/modular/package.json +4 -4
  138. package/starters/modular/src/layouts/app-shell.layout.ts +1 -2
  139. package/starters/modular/src/modules/auth/login.page.ts +5 -12
  140. package/starters/modular/src/modules/billing/pages/invoice-detail.page.ts +3 -4
  141. package/starters/modular/src/modules/billing/pages/invoices-list.page.ts +1 -1
  142. package/starters/modular/src/shared/styles/reset.css +21 -1
  143. package/starters/modular/tsconfig.json +10 -1
  144. package/starters/modular/tsconfig.node.json +13 -0
  145. package/starters/modular/vite.config.ts +6 -5
  146. package/dist/src/lazy.d.ts +0 -38
  147. package/dist/src/lazy.js +0 -73
  148. package/dist/src/lazy.js.map +0 -1
  149. package/scripts/bake.mjs +0 -5
  150. package/scripts/docs-lint.mjs +0 -170
  151. package/scripts/package-smoke.mjs +0 -135
  152. package/scripts/release-notes.mjs +0 -66
  153. package/scripts/size-budget.mjs +0 -95
  154. package/starters/modular/src/shared/ui/x-button.component.ts +0 -49
@@ -3,6 +3,8 @@ import { existsSync } from "node:fs";
3
3
  import { readdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
 
6
+ import { logger } from "../logger.mjs";
7
+
6
8
  export async function run(cmd, args, options = {}) {
7
9
  const child = spawn(cmd, args, {
8
10
  cwd: options.cwd,
@@ -48,7 +50,7 @@ export function resolvePackageBin(ctx, pkg, binPath) {
48
50
  if (existsSync(projectPath)) return projectPath;
49
51
  const packagePath = join(ctx.packageRoot, "node_modules", pkg, binPath);
50
52
  if (existsSync(packagePath)) return packagePath;
51
- console.error(`[mado] missing ${pkg}. Install it in this project: npm i -D ${pkg}`);
53
+ logger.error("mado", "missing-package", `missing ${pkg}. Install it in this project: npm i -D ${pkg}`);
52
54
  process.exit(1);
53
55
  }
54
56
 
@@ -57,7 +59,7 @@ export function resolveBin(ctx, bin) {
57
59
  if (existsSync(projectPath)) return projectPath;
58
60
  const packagePath = join(ctx.packageRoot, "node_modules", bin);
59
61
  if (!existsSync(packagePath)) {
60
- console.error(`[mado] missing ${bin}. Run npm install first.`);
62
+ logger.error("mado", "missing-binary", `missing ${bin}. Run npm install first.`);
61
63
  process.exit(1);
62
64
  }
63
65
  return packagePath;
@@ -66,7 +68,7 @@ export function resolveBin(ctx, bin) {
66
68
  export async function writeIfMissing(path, content, logPrefix = "[mado]") {
67
69
  if (existsSync(path)) return;
68
70
  await writeFile(path, content);
69
- console.log(`${logPrefix} wrote ${path}`);
71
+ logger.info("release", "write", `wrote ${path}`, { prefix: logPrefix });
70
72
  }
71
73
 
72
74
  export async function listTestFiles(projectRoot) {
@@ -0,0 +1,117 @@
1
+ const LEVELS = { debug: 10, info: 20, warn: 30, error: 40, silent: 50 };
2
+ const COLORS = {
3
+ debug: "\u001b[2m",
4
+ info: "\u001b[36m",
5
+ warn: "\u001b[33m",
6
+ error: "\u001b[31m",
7
+ };
8
+
9
+ let level = normaliseLevel(process.env.MADO_LOG_LEVEL) ?? "info";
10
+ let format = normaliseFormat(process.env.MADO_LOG_FORMAT) ?? "auto";
11
+
12
+ export function configureLogger(argv) {
13
+ const clean = [];
14
+ for (let index = 0; index < argv.length; index++) {
15
+ const arg = argv[index];
16
+ const [name, inline] = arg.split("=", 2);
17
+ if (name === "--log-level" || name === "--log-format") {
18
+ const value = inline ?? argv[++index];
19
+ if (!value) throw new Error(`${name} requires a value`);
20
+ if (name === "--log-level") {
21
+ const next = normaliseLevel(value);
22
+ if (!next) throw new Error(`invalid log level: ${value}`);
23
+ level = next;
24
+ } else {
25
+ const next = normaliseFormat(value);
26
+ if (!next) throw new Error(`invalid log format: ${value}`);
27
+ format = next;
28
+ }
29
+ continue;
30
+ }
31
+ if (arg === "--no-color") {
32
+ format = "plain";
33
+ continue;
34
+ }
35
+ clean.push(arg);
36
+ }
37
+ return clean;
38
+ }
39
+
40
+ export function diagnostic(levelName, scope, code, message, data) {
41
+ if (LEVELS[levelName] < LEVELS[level]) return;
42
+ const record = {
43
+ timestamp: new Date().toISOString(),
44
+ level: levelName,
45
+ scope,
46
+ code,
47
+ message,
48
+ ...(data === undefined ? {} : { data }),
49
+ };
50
+ const stream = levelName === "warn" || levelName === "error"
51
+ ? process.stderr
52
+ : process.stdout;
53
+ if (resolvedFormat(stream) === "json") {
54
+ stream.write(`${JSON.stringify(record, safeJson)}\n`);
55
+ return;
56
+ }
57
+ const prefix = scope === "mado" ? "[mado]" : `[${scope}]`;
58
+ const suffix = data === undefined ? "" : ` ${safePreview(data)}`;
59
+ const line = `${prefix}${code ? ` ${code}` : ""} ${message}${suffix}`;
60
+ if (resolvedFormat(stream) === "pretty") {
61
+ stream.write(`${COLORS[levelName] ?? ""}${line}\u001b[0m\n`);
62
+ } else {
63
+ stream.write(`${line}\n`);
64
+ }
65
+ }
66
+
67
+ export const logger = {
68
+ debug(scope, code, message, data) {
69
+ diagnostic("debug", scope, code, message, data);
70
+ },
71
+ info(scope, code, message, data) {
72
+ diagnostic("info", scope, code, message, data);
73
+ },
74
+ warn(scope, code, message, data) {
75
+ diagnostic("warn", scope, code, message, data);
76
+ },
77
+ error(scope, code, message, data) {
78
+ diagnostic("error", scope, code, message, data);
79
+ },
80
+ };
81
+
82
+ function resolvedFormat(stream) {
83
+ if (format !== "auto") return format;
84
+ if (process.env.NO_COLOR !== undefined || !stream.isTTY) return "plain";
85
+ return "pretty";
86
+ }
87
+
88
+ function normaliseLevel(value) {
89
+ if (!value) return null;
90
+ const candidate = String(value).toLowerCase();
91
+ return Object.hasOwn(LEVELS, candidate) ? candidate : null;
92
+ }
93
+
94
+ function normaliseFormat(value) {
95
+ if (!value) return null;
96
+ const candidate = String(value).toLowerCase();
97
+ return ["auto", "pretty", "plain", "json"].includes(candidate)
98
+ ? candidate
99
+ : null;
100
+ }
101
+
102
+ function safeJson(_key, value) {
103
+ if (typeof value === "bigint") return `${value}n`;
104
+ if (value instanceof Error) {
105
+ return { name: value.name, message: value.message, stack: value.stack };
106
+ }
107
+ return value;
108
+ }
109
+
110
+ function safePreview(value) {
111
+ try {
112
+ const text = JSON.stringify(value, safeJson);
113
+ return text.length > 500 ? `${text.slice(0, 497)}...` : text;
114
+ } catch {
115
+ return String(value);
116
+ }
117
+ }
@@ -0,0 +1,128 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readdir, realpath, rm, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+
6
+ export const OUTPUT_SENTINEL = ".mado-output";
7
+ const BUILD_BRIDGE = join("_mado", "build.json");
8
+ const PROTECTED_PROJECT_DIRS = new Set([
9
+ ".git",
10
+ "docs",
11
+ "node_modules",
12
+ "scripts",
13
+ "src",
14
+ "starters",
15
+ "test",
16
+ ]);
17
+
18
+ export async function prepareOutputDirectory({
19
+ projectRoot,
20
+ outDir,
21
+ clean = true,
22
+ force = false,
23
+ }) {
24
+ const safe = await resolveSafeOutput(projectRoot, outDir);
25
+ const state = await ownershipState(safe.outDir);
26
+
27
+ if (state === "foreign" && !force) {
28
+ throw new Error(
29
+ `[mado:output] refusing to use non-empty directory not owned by Mado: ${safe.outDir}\n` +
30
+ `Choose an empty directory or pass --force-output after checking the path.`,
31
+ );
32
+ }
33
+
34
+ if (clean && existsSync(safe.outDir)) {
35
+ await rm(safe.outDir, { recursive: true, force: true });
36
+ }
37
+ await mkdir(safe.outDir, { recursive: true });
38
+ await writeSentinel(safe.outDir, safe.projectRoot);
39
+ return safe.outDir;
40
+ }
41
+
42
+ export async function claimOutputDirectory({ projectRoot, outDir, force = false }) {
43
+ const safe = await validateOutputDirectory({ projectRoot, outDir, force });
44
+ await mkdir(safe.outDir, { recursive: true });
45
+ await writeSentinel(safe.outDir, safe.projectRoot);
46
+ return safe.outDir;
47
+ }
48
+
49
+ export async function validateOutputDirectory({ projectRoot, outDir, force = false }) {
50
+ const safe = await resolveSafeOutput(projectRoot, outDir);
51
+ const state = await ownershipState(safe.outDir);
52
+ if (state === "foreign" && !force) {
53
+ throw new Error(
54
+ `[mado:output] refusing to write into non-empty directory not owned by Mado: ${safe.outDir}\n` +
55
+ `Run a Mado/Vite build first, choose another --out, or pass --force-output.`,
56
+ );
57
+ }
58
+ return safe;
59
+ }
60
+
61
+ export async function resolveSafeOutput(projectRoot, outDir) {
62
+ const project = await canonicalPath(resolve(projectRoot));
63
+ const output = await canonicalPath(resolve(projectRoot, outDir));
64
+ const home = await canonicalPath(homedir());
65
+
66
+ if (isFilesystemRoot(output)) {
67
+ throw new Error(`[mado:output] refusing filesystem root: ${output}`);
68
+ }
69
+ if (output === project) {
70
+ throw new Error(`[mado:output] refusing project root: ${output}`);
71
+ }
72
+ if (output === home) {
73
+ throw new Error(`[mado:output] refusing home directory: ${output}`);
74
+ }
75
+ if (isInside(output, project)) {
76
+ throw new Error(`[mado:output] refusing a parent of the project: ${output}`);
77
+ }
78
+
79
+ const projectRelative = relative(project, output);
80
+ if (projectRelative && !projectRelative.startsWith("..") && !isAbsolute(projectRelative)) {
81
+ const first = projectRelative.split(/[\\/]/, 1)[0];
82
+ if (PROTECTED_PROJECT_DIRS.has(first)) {
83
+ throw new Error(`[mado:output] refusing protected project directory: ${output}`);
84
+ }
85
+ }
86
+
87
+ return { projectRoot: project, outDir: output };
88
+ }
89
+
90
+ async function ownershipState(outDir) {
91
+ if (!existsSync(outDir)) return "empty";
92
+ const entries = await readdir(outDir);
93
+ if (entries.length === 0) return "empty";
94
+ if (existsSync(join(outDir, OUTPUT_SENTINEL))) return "owned";
95
+ // A build bridge is emitted by the Mado Vite plugin and is sufficient
96
+ // evidence for a direct `mado static` invocation after `mado build`.
97
+ if (existsSync(join(outDir, BUILD_BRIDGE))) return "owned";
98
+ return "foreign";
99
+ }
100
+
101
+ async function writeSentinel(outDir, projectRoot) {
102
+ await writeFile(
103
+ join(outDir, OUTPUT_SENTINEL),
104
+ `${JSON.stringify({ owner: "@madojs/mado", projectRoot }, null, 2)}\n`,
105
+ );
106
+ }
107
+
108
+ async function canonicalPath(path) {
109
+ let cursor = path;
110
+ const suffix = [];
111
+ while (!existsSync(cursor)) {
112
+ const parent = dirname(cursor);
113
+ if (parent === cursor) break;
114
+ suffix.unshift(relative(parent, cursor));
115
+ cursor = parent;
116
+ }
117
+ const base = await realpath(cursor).catch(() => resolve(cursor));
118
+ return resolve(base, ...suffix);
119
+ }
120
+
121
+ function isFilesystemRoot(path) {
122
+ return dirname(path) === path;
123
+ }
124
+
125
+ function isInside(parent, child) {
126
+ const rel = relative(parent, child);
127
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
128
+ }
@@ -22,6 +22,8 @@ import { readFile, stat, access } from "node:fs/promises";
22
22
  import { extname, join, resolve, sep } from "node:path";
23
23
  import { spawnSync } from "node:child_process";
24
24
 
25
+ import { configureLogger, logger } from "./logger.mjs";
26
+
25
27
  // Tiny argv parser. Supports --flag, --flag=value, --flag value.
26
28
  function parsePreviewArgs(argv) {
27
29
  const flags = {};
@@ -47,7 +49,7 @@ function parsePreviewArgs(argv) {
47
49
  return flags;
48
50
  }
49
51
 
50
- const PREVIEW_FLAGS = parsePreviewArgs(process.argv.slice(2));
52
+ const PREVIEW_FLAGS = parsePreviewArgs(configureLogger(process.argv.slice(2)));
51
53
 
52
54
  const ROOT = resolve(process.cwd());
53
55
  const OUT = resolve(ROOT, process.env.OUT_DIR ?? "out");
@@ -138,14 +140,12 @@ const MIME = {
138
140
  // ---------- Optional release ----------
139
141
 
140
142
  if (!SKIP_BUILD) {
141
- console.log("[preview] PREVIEW_AUTOBUILD=1 — running mado release");
143
+ logger.info("preview", "autobuild", "PREVIEW_AUTOBUILD=1 — running mado release");
142
144
  run("node", ["scripts/cli.mjs", "release"]);
143
145
  }
144
146
 
145
147
  if (!(await exists(OUT))) {
146
- console.error(
147
- `[preview] missing ${OUT}/ — run \`mado release\` first.`,
148
- );
148
+ logger.error("preview", "missing-output", `missing ${OUT}/ — run \`mado release\` first`);
149
149
  process.exit(1);
150
150
  }
151
151
 
@@ -153,10 +153,7 @@ const spaShell = existsSync(join(OUT, "_mado", "spa.html"))
153
153
  ? join(OUT, "_mado", "spa.html")
154
154
  : join(OUT, "index.html");
155
155
  if (!(await exists(spaShell))) {
156
- console.error(
157
- `[preview] missing ${spaShell} — \`mado release\` did not produce an HTML entry.\n` +
158
- `[preview] Without it any non-static route will 404 instead of falling back to the SPA.`,
159
- );
156
+ logger.error("preview", "missing-shell", `missing ${spaShell}; mado release did not produce an HTML entry`);
160
157
  process.exit(1);
161
158
  }
162
159
 
@@ -216,22 +213,17 @@ const server = createServer(async (req, res) => {
216
213
  res.writeHead(200, headers);
217
214
  res.end(data);
218
215
  } catch (err) {
219
- console.error("[preview] error:", err);
216
+ logger.error("preview", "request", "request failed", err);
220
217
  res.writeHead(500).end(String(err));
221
218
  }
222
219
  });
223
220
 
224
221
  server.on("error", (err) => {
225
222
  if (err.code === "EPERM" || err.code === "EACCES") {
226
- console.error(
227
- `[preview] failed to bind ${HOST}:${PORT}: ${err.message}\n` +
228
- `[preview] tip: this sandbox may disallow binding "${HOST}".\n` +
229
- `[preview] try: mado preview --host 127.0.0.1`,
230
- );
223
+ logger.error("preview", "bind", `failed to bind ${HOST}:${PORT}: ${err.message}`);
224
+ logger.info("preview", "bind-hint", "try: mado preview --host 127.0.0.1");
231
225
  } else {
232
- console.error(
233
- `[preview] failed to listen on ${HOST}:${PORT}: ${err.message}`,
234
- );
226
+ logger.error("preview", "listen", `failed to listen on ${HOST}:${PORT}: ${err.message}`);
235
227
  }
236
228
  process.exit(1);
237
229
  });
@@ -256,7 +248,7 @@ server.listen(PORT, HOST, async () => {
256
248
  function run(cmd, args, { allowFail = false } = {}) {
257
249
  const r = spawnSync(cmd, args, { stdio: "inherit" });
258
250
  if (r.status !== 0 && !allowFail) {
259
- console.error(`[preview] command "${cmd} ${args.join(" ")}" exited with ${r.status}`);
251
+ logger.error("preview", "child-exit", `command "${cmd} ${args.join(" ")}" exited with ${r.status}`);
260
252
  process.exit(r.status ?? 1);
261
253
  }
262
254
  }
@@ -31,6 +31,8 @@
31
31
  */
32
32
  import { existsSync } from "node:fs";
33
33
 
34
+ import { logger } from "../logger.mjs";
35
+
34
36
  const KNOWN_CHROMIUM_PATHS = [
35
37
  "/usr/bin/google-chrome",
36
38
  "/usr/bin/google-chrome-stable",
@@ -46,18 +48,19 @@ const KNOWN_CHROMIUM_PATHS = [
46
48
  export async function captureStaticRoutes(options) {
47
49
  const { chromium } = await importPlaywright();
48
50
  const browser = await launchBrowser(chromium, options);
49
- const captured = [];
51
+ let captured = [];
50
52
 
51
53
  try {
52
54
  const browserVersion =
53
55
  typeof browser.version === "function" ? browser.version() : null;
54
56
  if (browserVersion) {
55
- console.log(`[static] browser: chromium ${browserVersion}`);
56
- }
57
- for (const record of options.records) {
58
- const html = await captureRoute(browser, record, options);
59
- captured.push({ ...record, html });
57
+ logger.info("static", "browser", `browser: chromium ${browserVersion}`);
60
58
  }
59
+ captured = await mapConcurrent(
60
+ options.records,
61
+ options.concurrency ?? 4,
62
+ async (record) => ({ ...record, html: await captureRoute(browser, record, options) }),
63
+ );
61
64
  } finally {
62
65
  await browser.close();
63
66
  }
@@ -65,6 +68,20 @@ export async function captureStaticRoutes(options) {
65
68
  return captured;
66
69
  }
67
70
 
71
+ export async function mapConcurrent(items, concurrency, task) {
72
+ const results = new Array(items.length);
73
+ let cursor = 0;
74
+ const worker = async () => {
75
+ while (cursor < items.length) {
76
+ const index = cursor++;
77
+ results[index] = await task(items[index], index);
78
+ }
79
+ };
80
+ const count = Math.max(1, Math.min(items.length, Number(concurrency) || 1));
81
+ await Promise.all(Array.from({ length: count }, worker));
82
+ return results;
83
+ }
84
+
68
85
  /**
69
86
  * Join a Mado-canonical base (`"/"` or `"/prefix/"`) with a route
70
87
  * pathname so the capture browser navigates to the same URL shape the
@@ -85,10 +102,21 @@ function withBaseLocal(base, pathname) {
85
102
 
86
103
  async function captureRoute(browser, record, options) {
87
104
  const context = await browser.newContext({ serviceWorkers: "block" });
88
- const page = await context.newPage();
89
105
  const pageErrors = [];
90
106
  const failedRequests = [];
91
107
  const consoleErrors = [];
108
+ await context.route("**/*", async (route) => {
109
+ const request = route.request();
110
+ if (isCaptureAllowedUrl(request.url(), options.serverOrigin)) {
111
+ await route.continue();
112
+ return;
113
+ }
114
+ failedRequests.push(
115
+ `${request.method()} ${request.url()} blocked external request`,
116
+ );
117
+ await route.abort("blockedbyclient");
118
+ });
119
+ const page = await context.newPage();
92
120
 
93
121
  page.on("pageerror", (err) => pageErrors.push(err.message));
94
122
  page.on("requestfailed", (request) => {
@@ -229,7 +257,7 @@ async function runWithTimeout(promise, timeout, swallowMessage) {
229
257
  ]);
230
258
  } catch (err) {
231
259
  if (err && err.message === "timeout") {
232
- console.warn(swallowMessage);
260
+ logger.warn("static", "browser-resource", swallowMessage);
233
261
  return;
234
262
  }
235
263
  throw err;
@@ -544,6 +572,19 @@ function isIgnorableResourceUrl(url) {
544
572
  }
545
573
  }
546
574
 
575
+ function isCaptureAllowedUrl(url, serverOrigin) {
576
+ if (!url) return false;
577
+ if (url.startsWith("data:") || url.startsWith("blob:")) return true;
578
+ try {
579
+ return new URL(url).origin === new URL(serverOrigin).origin;
580
+ } catch {
581
+ return false;
582
+ }
583
+ }
584
+
585
+ /** @internal */
586
+ export const _testHooks = { isCaptureAllowedUrl };
587
+
547
588
  function isIgnorableConsoleError(text) {
548
589
  if (!text) return false;
549
590
  // Browser-level "Failed to load resource" lines without context. The
@@ -48,8 +48,11 @@ export async function discoverStaticRoutes(options) {
48
48
  // 2. App runs need our routing helpers loaded through SSR alongside the
49
49
  // user manifest; we expose them via `__mado_match__` so we can ask
50
50
  // for them via ssrLoadModule without colliding with user imports.
51
+ const sourceMatch = resolve(getPackageRoot(), "src/router/match.ts");
51
52
  const aliases = {
52
- __mado_match__: resolve(getPackageRoot(), "src/router/match.ts"),
53
+ __mado_match__: existsSync(sourceMatch)
54
+ ? sourceMatch
55
+ : resolve(getPackageRoot(), "dist/src/router/match.js"),
53
56
  ...(context === "repo"
54
57
  ? { "@madojs/mado": resolve(projectRoot, "src/index.ts") }
55
58
  : {}),
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync, readFileSync, writeSync } from "node:fs";
3
+ import { existsSync, readFileSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
 
6
6
  import { parseFlags } from "./_config.mjs";
7
+ import { configureLogger, logger } from "./logger.mjs";
8
+ import { claimOutputDirectory, validateOutputDirectory } from "./output-guard.mjs";
7
9
  import { discoverStaticRoutes } from "./static/discover.mjs";
8
10
  import { createStaticCaptureServer } from "./static/server.mjs";
9
11
  import { captureStaticRoutes } from "./static/browser.mjs";
@@ -17,7 +19,7 @@ import {
17
19
  writeStaticDeploymentFiles,
18
20
  } from "./static/output.mjs";
19
21
 
20
- const { flags } = parseFlags(process.argv.slice(2));
22
+ const { flags } = parseFlags(configureLogger(process.argv.slice(2)));
21
23
  const projectRoot = resolve(process.cwd());
22
24
  const outDir = resolve(
23
25
  projectRoot,
@@ -27,7 +29,12 @@ const timeout = Number(flags.timeout ?? 30_000);
27
29
 
28
30
  let tempRootForCleanup = null;
29
31
  try {
30
- console.log(`[static] artifact: ${outDir}`);
32
+ await validateOutputDirectory({
33
+ projectRoot,
34
+ outDir,
35
+ force: flags["force-output"] === true,
36
+ });
37
+ logger.info("static", "artifact", `artifact: ${outDir}`);
31
38
 
32
39
  // Source the public origin and Vite base from (in order):
33
40
  // --base-url, --base / MADO_SITE env, _mado/build.json (the bridge
@@ -49,7 +56,13 @@ try {
49
56
  entry: typeof flags.entry === "string" ? flags.entry : undefined,
50
57
  });
51
58
 
52
- console.log(`[static] discovered ${records.length} static route(s)`);
59
+ logger.info("static", "discover", `discovered ${records.length} static route(s)`);
60
+
61
+ await claimOutputDirectory({
62
+ projectRoot,
63
+ outDir,
64
+ force: flags["force-output"] === true,
65
+ });
53
66
 
54
67
  if (records.length > 0 && !site) {
55
68
  throw new Error(
@@ -85,6 +98,7 @@ try {
85
98
  base,
86
99
  site: publicOrigin,
87
100
  timeout,
101
+ concurrency: Number(flags.concurrency ?? 4),
88
102
  browserChannel:
89
103
  typeof flags["browser-channel"] === "string"
90
104
  ? flags["browser-channel"]
@@ -99,7 +113,7 @@ try {
99
113
  // existing deployment files are touched.
100
114
  await writeCapturedRoutes(routesDir, captured);
101
115
  await promoteCapturedRoutes({ outDir, routesDir, captured });
102
- console.log(`[static] captured ${captured.length} route snapshot(s)`);
116
+ logger.info("static", "capture", `captured ${captured.length} route snapshot(s)`);
103
117
  } finally {
104
118
  await server.close();
105
119
  }
@@ -121,14 +135,14 @@ try {
121
135
  // Drop the internal build bridge so the production artifact does not
122
136
  // ship Vite's resolved view of the project (site, base, assetsDir).
123
137
  await dropBuildBridge(outDir);
124
- console.log(`[static] done`);
138
+ logger.info("static", "done", "done");
125
139
  } catch (err) {
126
140
  // Defer the actual termination: `process.exit()` is synchronous and
127
141
  // would race the awaited cleanup in the `finally` block, leaving the
128
142
  // OS to garbage-collect a half-promoted staging directory. Setting
129
143
  // `exitCode` lets Node finish the microtask queue (including the
130
144
  // cleanup below) and then exit with the failure status.
131
- writeSync(2, `${err?.stack ?? err}\n`);
145
+ logger.error("static", "failed", err instanceof Error ? err.message : String(err), err);
132
146
  process.exitCode = 1;
133
147
  } finally {
134
148
  if (tempRootForCleanup) {
@@ -16,9 +16,9 @@
16
16
  "@madojs/mado": "__MADOJS_VERSION__"
17
17
  },
18
18
  "devDependencies": {
19
- "lightningcss": "^1.32.0",
20
- "playwright-core": "^1.47.0",
21
- "typescript": "^5.5.0",
22
- "vite": "^8.0.16"
19
+ "@types/node": "^22.12.0",
20
+ "playwright-core": "^1.61.1",
21
+ "typescript": "^7.0.2",
22
+ "vite": "^8.1.4"
23
23
  }
24
24
  }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "types": ["node"],
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true
11
+ },
12
+ "include": ["vite.config.ts"]
13
+ }
@@ -16,7 +16,4 @@ export default defineConfig({
16
16
  // site: "https://your-app.example",
17
17
  }),
18
18
  ],
19
- css: {
20
- transformer: "lightningcss",
21
- },
22
19
  });
@@ -51,7 +51,7 @@ this starter so `npm run dev` is runnable out of the box:
51
51
 
52
52
  | Method + path | Behaviour |
53
53
  | ---------------------------------------- | ---------------------------------------------- |
54
- | `POST /api/auth/login` | accepts `demo@mado.dev` / `demo` |
54
+ | `POST /api/auth/login` | accepts `demo@mado.dev` / `demo123` |
55
55
  | `GET /api/auth/me` | current user (after login) |
56
56
  | `POST /api/auth/logout` | clears the in-memory session |
57
57
  | `GET /api/billing/stripe/invoices` | lists six seeded invoices |
@@ -102,8 +102,8 @@ src/
102
102
  - Reusable leaf components keep their own styles in ``css`...` ``
103
103
  inside `component()` options.
104
104
 
105
- Vite uses Lightning CSS for CSS transforms/minification in this
106
- starter.
105
+ Vite 8's standard pipeline owns CSS transforms/minification; Mado itself does
106
+ not own CSS processing.
107
107
 
108
108
  ## Internal links
109
109
 
@@ -139,4 +139,4 @@ The generator writes files only. Wire new routes in
139
139
  ## More
140
140
 
141
141
  The full architecture guide lives in the framework docs:
142
- https://github.com/madojs/mado/tree/main/docs/en
142
+ https://github.com/madojs/mado/tree/main/docs/en
@@ -18,13 +18,13 @@
18
18
  "@madojs/mado": "__MADOJS_VERSION__"
19
19
  },
20
20
  "devDependencies": {
21
+ "@types/node": "^22.12.0",
21
22
  "@typescript-eslint/eslint-plugin": "^8.0.0",
22
23
  "@typescript-eslint/parser": "^8.0.0",
23
24
  "eslint": "^9.0.0",
24
25
  "eslint-plugin-boundaries": "^5.0.0",
25
- "lightningcss": "^1.32.0",
26
- "playwright-core": "^1.47.0",
27
- "typescript": "^5.5.0",
28
- "vite": "^8.0.16"
26
+ "playwright-core": "^1.61.1",
27
+ "typescript": "^6.0.3",
28
+ "vite": "^8.1.4"
29
29
  }
30
30
  }
@@ -12,7 +12,6 @@
12
12
  import { html, page, routeUrl } from "@madojs/mado";
13
13
 
14
14
  import { isAuthed, logout, user } from "../modules/auth/auth.public";
15
- import "../shared/ui/x-button.component";
16
15
 
17
16
  export default page({
18
17
  title: "Mado App",
@@ -28,7 +27,7 @@ export default page({
28
27
  ${() =>
29
28
  isAuthed()
30
29
  ? html`<span class="who">${() => user()?.email ?? ""}</span>
31
- <x-button variant="ghost" @click=${logout}>Sign out</x-button>`
30
+ <button class="button button--ghost" type="button" @click=${logout}>Sign out</button>`
32
31
  : html`<a data-link href=${routeUrl("/login")}>Sign in</a>`}
33
32
  </nav>
34
33
  </header>