@madojs/mado 0.12.0 → 0.13.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 +15 -8
- package/CHANGELOG.md +66 -0
- package/README.md +25 -3
- package/dist/src/component.d.ts +3 -1
- package/dist/src/component.d.ts.map +1 -0
- package/dist/src/component.js +65 -30
- package/dist/src/component.js.map +1 -1
- package/dist/src/context.d.ts +13 -32
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +73 -48
- package/dist/src/context.js.map +1 -1
- package/dist/src/css.d.ts +1 -0
- package/dist/src/css.d.ts.map +1 -0
- package/dist/src/css.js.map +1 -1
- package/dist/src/devtools-hook.d.ts +14 -0
- package/dist/src/devtools-hook.d.ts.map +1 -0
- package/dist/src/devtools-hook.js +27 -0
- package/dist/src/devtools-hook.js.map +1 -0
- package/dist/src/devtools.d.ts +24 -22
- package/dist/src/devtools.d.ts.map +1 -0
- package/dist/src/devtools.js +230 -58
- package/dist/src/devtools.js.map +1 -1
- package/dist/src/diagnostics.d.ts +12 -0
- package/dist/src/diagnostics.d.ts.map +1 -0
- package/dist/src/diagnostics.js +68 -8
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/each.d.ts +1 -5
- package/dist/src/each.d.ts.map +1 -0
- package/dist/src/each.js +0 -8
- package/dist/src/each.js.map +1 -1
- package/dist/src/forms.d.ts +27 -96
- package/dist/src/forms.d.ts.map +1 -0
- package/dist/src/forms.js +184 -394
- package/dist/src/forms.js.map +1 -1
- package/dist/src/head.d.ts +1 -0
- package/dist/src/head.d.ts.map +1 -0
- package/dist/src/head.js +2 -1
- package/dist/src/head.js.map +1 -1
- package/dist/src/html/bindings.d.ts +1 -0
- package/dist/src/html/bindings.d.ts.map +1 -0
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/html/parser.d.ts +3 -4
- package/dist/src/html/parser.d.ts.map +1 -0
- package/dist/src/html/parser.js +63 -14
- package/dist/src/html/parser.js.map +1 -1
- package/dist/src/html/template-types.d.ts +1 -0
- package/dist/src/html/template-types.d.ts.map +1 -0
- package/dist/src/html/template-types.js.map +1 -1
- package/dist/src/html/template.d.ts +5 -1
- package/dist/src/html/template.d.ts.map +1 -0
- package/dist/src/html/template.js +78 -3
- package/dist/src/html/template.js.map +1 -1
- package/dist/src/index.d.ts +7 -8
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +5 -7
- package/dist/src/index.js.map +1 -1
- package/dist/src/json.d.ts +3 -0
- package/dist/src/json.d.ts.map +1 -0
- package/dist/src/json.js +14 -0
- package/dist/src/json.js.map +1 -0
- package/dist/src/lifecycle.d.ts +1 -0
- package/dist/src/lifecycle.d.ts.map +1 -0
- package/dist/src/lifecycle.js +3 -2
- package/dist/src/lifecycle.js.map +1 -1
- package/dist/src/page.d.ts +1 -0
- package/dist/src/page.d.ts.map +1 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/persisted.d.ts +6 -1
- package/dist/src/persisted.d.ts.map +1 -0
- package/dist/src/persisted.js +17 -11
- package/dist/src/persisted.js.map +1 -1
- package/dist/src/resource.d.ts +2 -1
- package/dist/src/resource.d.ts.map +1 -0
- package/dist/src/resource.js +126 -49
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/base.d.ts +1 -0
- package/dist/src/router/base.d.ts.map +1 -0
- package/dist/src/router/base.js.map +1 -1
- package/dist/src/router/manifest.d.ts +1 -0
- package/dist/src/router/manifest.d.ts.map +1 -0
- package/dist/src/router/manifest.js +89 -148
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +1 -0
- package/dist/src/router/match.d.ts.map +1 -0
- package/dist/src/router/match.js +19 -5
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.d.ts +1 -0
- package/dist/src/router/navigation.d.ts.map +1 -0
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/signal.d.ts +1 -0
- package/dist/src/signal.d.ts.map +1 -0
- package/dist/src/signal.js +46 -17
- package/dist/src/signal.js.map +1 -1
- package/dist/src/static-runtime.d.ts +1 -0
- package/dist/src/static-runtime.d.ts.map +1 -0
- package/dist/src/static-runtime.js.map +1 -1
- package/dist/src/vite/index.d.ts +1 -0
- package/dist/src/vite/index.d.ts.map +1 -0
- package/dist/src/vite/index.js +4 -3
- package/dist/src/vite/index.js.map +1 -1
- package/docs/architecture/v1-roadmap.md +19 -0
- package/docs/en/10-pages-and-components.md +9 -1
- package/docs/en/13-data.md +8 -6
- package/docs/en/14-forms.md +66 -219
- package/docs/en/15-static-snapshots.md +4 -4
- package/docs/en/16-app-architecture.md +3 -2
- package/docs/en/20-deployment.md +21 -8
- package/docs/en/21-error-handling.md +9 -6
- package/docs/en/24-devtools-and-diagnostics.md +45 -0
- package/docs/en/30-api-freeze-map.md +8 -9
- package/docs/en/32-v1-stability.md +2 -2
- package/docs/en/33-migration-0.12-0.13.md +56 -0
- package/docs/en/40-llm-guide.md +7 -9
- package/docs/en/41-for-backenders.md +13 -9
- package/docs/en/42-why-mado.md +2 -2
- package/docs/en/README.md +2 -0
- package/docs/recipes/nginx/nginx.conf +2 -2
- package/llms.txt +25 -7
- package/package.json +37 -16
- package/scripts/clean.mjs +8 -0
- package/scripts/cli/generate.mjs +11 -14
- package/scripts/cli/help.mjs +2 -0
- package/scripts/cli/index.mjs +14 -6
- package/scripts/cli/init.mjs +13 -18
- package/scripts/cli/release.mjs +22 -19
- package/scripts/cli/run.mjs +5 -3
- package/scripts/logger.mjs +117 -0
- package/scripts/output-guard.mjs +128 -0
- package/scripts/preview.mjs +11 -19
- package/scripts/static/browser.mjs +58 -8
- package/scripts/static/discover.mjs +4 -1
- package/scripts/static.mjs +21 -7
- package/starters/default/package.json +4 -4
- package/starters/default/tsconfig.node.json +13 -0
- package/starters/default/vite.config.ts +0 -3
- package/starters/modular/README.md +4 -4
- package/starters/modular/package.json +4 -4
- package/starters/modular/src/layouts/app-shell.layout.ts +1 -2
- package/starters/modular/src/modules/auth/login.page.ts +5 -12
- package/starters/modular/src/modules/billing/pages/invoice-detail.page.ts +3 -4
- package/starters/modular/src/modules/billing/pages/invoices-list.page.ts +1 -1
- package/starters/modular/src/shared/styles/reset.css +21 -1
- package/starters/modular/tsconfig.json +10 -1
- package/starters/modular/tsconfig.node.json +13 -0
- package/starters/modular/vite.config.ts +6 -5
- package/dist/src/lazy.d.ts +0 -38
- package/dist/src/lazy.js +0 -73
- package/dist/src/lazy.js.map +0 -1
- package/scripts/bake.mjs +0 -5
- package/scripts/docs-lint.mjs +0 -170
- package/scripts/package-smoke.mjs +0 -135
- package/scripts/release-notes.mjs +0 -66
- package/scripts/size-budget.mjs +0 -95
- package/starters/modular/src/shared/ui/x-button.component.ts +0 -49
package/scripts/cli/run.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/scripts/preview.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
|
@@ -184,6 +212,15 @@ async function captureRoute(browser, record, options) {
|
|
|
184
212
|
|
|
185
213
|
async function waitForMadoStability(page, record, timeout) {
|
|
186
214
|
try {
|
|
215
|
+
// DOMContentLoaded does not guarantee that an async module graph has
|
|
216
|
+
// finished evaluating. Apps may legitimately use top-level await during
|
|
217
|
+
// bootstrap (the modular starter restores auth this way), so wait for the
|
|
218
|
+
// runtime handshake instead of reading it once and racing slow runners.
|
|
219
|
+
await page.waitForFunction(
|
|
220
|
+
() => window.__MADO_STATIC__ !== undefined,
|
|
221
|
+
undefined,
|
|
222
|
+
{ timeout },
|
|
223
|
+
);
|
|
187
224
|
await withTimeout(
|
|
188
225
|
page.evaluate(async () => {
|
|
189
226
|
const runtime = window.__MADO_STATIC__;
|
|
@@ -229,7 +266,7 @@ async function runWithTimeout(promise, timeout, swallowMessage) {
|
|
|
229
266
|
]);
|
|
230
267
|
} catch (err) {
|
|
231
268
|
if (err && err.message === "timeout") {
|
|
232
|
-
|
|
269
|
+
logger.warn("static", "browser-resource", swallowMessage);
|
|
233
270
|
return;
|
|
234
271
|
}
|
|
235
272
|
throw err;
|
|
@@ -544,6 +581,19 @@ function isIgnorableResourceUrl(url) {
|
|
|
544
581
|
}
|
|
545
582
|
}
|
|
546
583
|
|
|
584
|
+
function isCaptureAllowedUrl(url, serverOrigin) {
|
|
585
|
+
if (!url) return false;
|
|
586
|
+
if (url.startsWith("data:") || url.startsWith("blob:")) return true;
|
|
587
|
+
try {
|
|
588
|
+
return new URL(url).origin === new URL(serverOrigin).origin;
|
|
589
|
+
} catch {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** @internal */
|
|
595
|
+
export const _testHooks = { isCaptureAllowedUrl };
|
|
596
|
+
|
|
547
597
|
function isIgnorableConsoleError(text) {
|
|
548
598
|
if (!text) return false;
|
|
549
599
|
// 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__:
|
|
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
|
: {}),
|
package/scripts/static.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, readFileSync
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
20
|
-
"playwright-core": "^1.
|
|
21
|
-
"typescript": "^
|
|
22
|
-
"vite": "^8.
|
|
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
|
+
}
|
|
@@ -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` / `
|
|
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
|
|
106
|
-
|
|
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
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
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
|
}
|