@madojs/mado 0.11.1 → 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.
- package/AGENTS.md +116 -32
- package/CHANGELOG.md +216 -1
- package/README.md +158 -173
- package/dist/src/component.d.ts +6 -3
- package/dist/src/component.d.ts.map +1 -0
- package/dist/src/component.js +163 -36
- 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 +34 -5
- 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 +7 -6
- package/dist/src/head.d.ts.map +1 -0
- package/dist/src/head.js +8 -7
- 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 +4 -5
- package/dist/src/html/parser.d.ts.map +1 -0
- package/dist/src/html/parser.js +64 -15
- 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 +102 -13
- package/dist/src/html/template.js.map +1 -1
- package/dist/src/index.d.ts +15 -12
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -10
- 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 +63 -28
- package/dist/src/page.d.ts.map +1 -0
- package/dist/src/page.js +5 -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 +128 -50
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/base.d.ts +66 -0
- package/dist/src/router/base.d.ts.map +1 -0
- package/dist/src/router/base.js +168 -0
- package/dist/src/router/base.js.map +1 -0
- 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 +156 -171
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +17 -2
- package/dist/src/router/match.d.ts.map +1 -0
- package/dist/src/router/match.js +60 -6
- 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 +58 -9
- 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 +82 -0
- package/dist/src/static-runtime.d.ts.map +1 -0
- package/dist/src/static-runtime.js +209 -0
- package/dist/src/static-runtime.js.map +1 -0
- package/dist/src/vite/index.d.ts +32 -3
- package/dist/src/vite/index.d.ts.map +1 -0
- package/dist/src/vite/index.js +49 -17
- package/dist/src/vite/index.js.map +1 -1
- package/docs/README.md +5 -8
- package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
- package/docs/architecture/v1-roadmap.md +19 -0
- package/docs/en/00-the-mado-way.md +1 -1
- package/docs/en/01-quickstart.md +183 -0
- package/docs/en/10-pages-and-components.md +279 -0
- package/docs/en/11-templates-and-signals.md +211 -0
- package/docs/en/12-routing.md +229 -0
- package/docs/en/13-data.md +227 -0
- package/docs/en/14-forms.md +91 -0
- package/docs/en/15-static-snapshots.md +181 -0
- package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +56 -4
- package/docs/en/{13-deployment.md → 20-deployment.md} +79 -17
- package/docs/en/{15-error-handling.md → 21-error-handling.md} +9 -6
- package/docs/en/{14-testing.md → 22-testing.md} +6 -5
- package/docs/en/23-cookbook.md +292 -0
- package/docs/en/24-devtools-and-diagnostics.md +45 -0
- package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +17 -9
- package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +5 -5
- package/docs/en/33-migration-0.12-0.13.md +56 -0
- package/docs/en/40-llm-guide.md +774 -0
- package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +28 -25
- package/docs/en/{05-why-mado.md → 42-why-mado.md} +3 -3
- package/docs/en/README.md +99 -27
- package/docs/recipes/nginx/Containerfile +26 -0
- package/docs/recipes/nginx/nginx.conf +42 -0
- package/llms.txt +263 -210
- package/package.json +51 -23
- package/scripts/clean.mjs +8 -0
- package/scripts/cli/generate.mjs +57 -10
- package/scripts/cli/help.mjs +13 -5
- package/scripts/cli/index.mjs +31 -7
- package/scripts/cli/init.mjs +19 -19
- package/scripts/cli/release.mjs +38 -83
- package/scripts/cli/run.mjs +5 -3
- package/scripts/logger.mjs +117 -0
- package/scripts/output-guard.mjs +128 -0
- package/scripts/preview.mjs +105 -24
- package/scripts/static/browser.mjs +695 -0
- package/scripts/static/discover.mjs +284 -0
- package/scripts/static/output.mjs +141 -0
- package/scripts/static/serialize.mjs +212 -0
- package/scripts/static/server.mjs +167 -0
- package/scripts/static.mjs +205 -0
- package/starters/default/README.md +57 -55
- package/starters/default/package.json +6 -12
- package/starters/default/src/app.routes.ts +20 -33
- package/starters/default/src/components/feature-card.component.ts +40 -0
- package/starters/default/src/components/live-counter.component.ts +57 -0
- package/starters/default/src/content/guides.ts +55 -0
- package/starters/default/src/main.ts +9 -13
- package/starters/default/src/pages/app.page.ts +41 -0
- package/starters/default/src/pages/guide.page.ts +40 -0
- package/starters/default/src/pages/home.page.ts +49 -0
- package/starters/default/src/pages/not-found.page.ts +18 -0
- package/starters/default/src/styles/document.css +39 -0
- package/starters/default/src/styles/reset.css +37 -0
- package/starters/default/src/styles/tokens.css +30 -0
- package/starters/default/src/styles.d.ts +3 -1
- package/starters/default/src/vite-env.d.ts +1 -1
- package/starters/default/tsconfig.node.json +13 -0
- package/starters/default/vite.config.ts +14 -4
- package/starters/modular/README.md +142 -0
- package/starters/modular/index.html +13 -0
- package/starters/modular/package.json +30 -0
- package/starters/modular/src/app.routes.ts +39 -0
- package/starters/{default → modular}/src/layouts/app-shell.layout.ts +9 -7
- package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
- package/starters/modular/src/main.ts +16 -0
- package/starters/{default → modular}/src/modules/auth/login.page.ts +5 -12
- package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +3 -4
- package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +4 -4
- package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
- package/starters/modular/src/modules/home/not-found.page.ts +14 -0
- package/starters/{default → modular}/src/shared/styles/reset.css +21 -1
- package/starters/modular/src/styles.d.ts +1 -0
- package/starters/modular/src/vite-env.d.ts +1 -0
- package/starters/modular/tsconfig.json +33 -0
- package/starters/modular/tsconfig.node.json +13 -0
- package/starters/modular/vite.config.ts +132 -0
- package/TODO.md +0 -79
- package/dist/src/lazy.d.ts +0 -38
- package/dist/src/lazy.js +0 -73
- package/dist/src/lazy.js.map +0 -1
- package/docs/en/01-routing.md +0 -152
- package/docs/en/02-project-layout.md +0 -124
- package/docs/en/03-static-bake.md +0 -249
- package/docs/en/04-ide-setup.md +0 -162
- package/docs/en/07-llm-pitfalls.md +0 -724
- package/docs/en/08-llm-zero-history-test.md +0 -56
- package/docs/en/09-shadow-vs-light-dom.md +0 -174
- package/docs/en/11-layouts.md +0 -113
- package/docs/en/12-auth-and-api.md +0 -124
- package/docs/en/16-bake-cookbook.md +0 -100
- package/docs/en/17-shadow-dom-forms.md +0 -192
- package/docs/fr/00-the-mado-way.md +0 -85
- package/docs/fr/01-routing.md +0 -120
- package/docs/fr/02-project-layout.md +0 -84
- package/docs/fr/03-static-bake.md +0 -289
- package/docs/fr/04-ide-setup.md +0 -162
- package/docs/fr/05-why-mado.md +0 -193
- package/docs/fr/06-for-backenders.md +0 -438
- package/docs/fr/07-llm-pitfalls.md +0 -625
- package/docs/fr/08-llm-zero-history-test.md +0 -38
- package/docs/fr/09-shadow-vs-light-dom.md +0 -65
- package/docs/fr/10-app-architecture.md +0 -138
- package/docs/fr/11-layouts.md +0 -47
- package/docs/fr/12-auth-and-api.md +0 -76
- package/docs/fr/13-deployment.md +0 -57
- package/docs/fr/14-testing.md +0 -41
- package/docs/fr/15-error-handling.md +0 -50
- package/docs/fr/16-bake-cookbook.md +0 -88
- package/docs/fr/17-shadow-dom-forms.md +0 -196
- package/docs/fr/18-api-freeze-map.md +0 -63
- package/docs/fr/19-reactivity-ordering.md +0 -97
- package/docs/fr/20-v1-stability.md +0 -88
- package/docs/fr/README.md +0 -27
- package/docs/ru/00-the-mado-way.md +0 -84
- package/docs/ru/01-routing.md +0 -119
- package/docs/ru/02-project-layout.md +0 -84
- package/docs/ru/03-static-bake.md +0 -250
- package/docs/ru/04-ide-setup.md +0 -144
- package/docs/ru/05-why-mado.md +0 -193
- package/docs/ru/06-for-backenders.md +0 -428
- package/docs/ru/07-llm-pitfalls.md +0 -624
- package/docs/ru/08-llm-zero-history-test.md +0 -57
- package/docs/ru/09-shadow-vs-light-dom.md +0 -63
- package/docs/ru/10-app-architecture.md +0 -152
- package/docs/ru/11-layouts.md +0 -47
- package/docs/ru/12-auth-and-api.md +0 -75
- package/docs/ru/13-deployment.md +0 -66
- package/docs/ru/14-testing.md +0 -50
- package/docs/ru/15-error-handling.md +0 -56
- package/docs/ru/16-bake-cookbook.md +0 -95
- package/docs/ru/17-shadow-dom-forms.md +0 -193
- package/docs/ru/18-api-freeze-map.md +0 -64
- package/docs/ru/19-reactivity-ordering.md +0 -95
- package/docs/ru/20-v1-stability.md +0 -82
- package/docs/ru/README.md +0 -25
- package/docs/uk/00-the-mado-way.md +0 -82
- package/docs/uk/01-routing.md +0 -76
- package/docs/uk/02-project-layout.md +0 -73
- package/docs/uk/03-static-bake.md +0 -48
- package/docs/uk/04-ide-setup.md +0 -26
- package/docs/uk/05-why-mado.md +0 -34
- package/docs/uk/06-for-backenders.md +0 -55
- package/docs/uk/07-llm-pitfalls.md +0 -145
- package/docs/uk/08-llm-zero-history-test.md +0 -34
- package/docs/uk/09-shadow-vs-light-dom.md +0 -58
- package/docs/uk/10-app-architecture.md +0 -97
- package/docs/uk/11-layouts.md +0 -47
- package/docs/uk/12-auth-and-api.md +0 -70
- package/docs/uk/13-deployment.md +0 -40
- package/docs/uk/14-testing.md +0 -34
- package/docs/uk/15-error-handling.md +0 -32
- package/docs/uk/16-bake-cookbook.md +0 -36
- package/docs/uk/17-shadow-dom-forms.md +0 -193
- package/docs/uk/18-api-freeze-map.md +0 -61
- package/docs/uk/19-reactivity-ordering.md +0 -95
- package/docs/uk/20-v1-stability.md +0 -83
- package/docs/uk/README.md +0 -27
- package/scripts/bake.mjs +0 -570
- package/scripts/package-smoke.mjs +0 -77
- package/scripts/release-notes.mjs +0 -66
- package/scripts/size-budget.mjs +0 -91
- package/starters/default/src/modules/home/not-found.page.ts +0 -11
- package/starters/default/src/shared/ui/x-button.component.ts +0 -49
- /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
- /package/starters/{default → modular}/.editorconfig +0 -0
- /package/starters/{default → modular}/eslint.config.mjs +0 -0
- /package/starters/{default → modular}/public/favicon.svg +0 -0
- /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
- /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
- /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// PREVIEW_AUTOBUILD=1.
|
|
10
10
|
// 3. Starts a static server with:
|
|
11
11
|
// - immutable cache for hashed bundles;
|
|
12
|
-
// - SPA fallback to index.html;
|
|
12
|
+
// - SPA fallback to _mado/spa.html when present, else index.html;
|
|
13
13
|
// - exact `out/` route files before SPA fallback;
|
|
14
14
|
// - precompressed .gz / .br serving via Accept-Encoding.
|
|
15
15
|
//
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
// what a static host (nginx / Cloudflare Pages / S3) would serve.
|
|
18
18
|
|
|
19
19
|
import { createServer } from "node:http";
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
21
|
import { readFile, stat, access } from "node:fs/promises";
|
|
21
22
|
import { extname, join, resolve, sep } from "node:path";
|
|
22
23
|
import { spawnSync } from "node:child_process";
|
|
23
24
|
|
|
25
|
+
import { configureLogger, logger } from "./logger.mjs";
|
|
26
|
+
|
|
24
27
|
// Tiny argv parser. Supports --flag, --flag=value, --flag value.
|
|
25
28
|
function parsePreviewArgs(argv) {
|
|
26
29
|
const flags = {};
|
|
@@ -46,7 +49,7 @@ function parsePreviewArgs(argv) {
|
|
|
46
49
|
return flags;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
const PREVIEW_FLAGS = parsePreviewArgs(process.argv.slice(2));
|
|
52
|
+
const PREVIEW_FLAGS = parsePreviewArgs(configureLogger(process.argv.slice(2)));
|
|
50
53
|
|
|
51
54
|
const ROOT = resolve(process.cwd());
|
|
52
55
|
const OUT = resolve(ROOT, process.env.OUT_DIR ?? "out");
|
|
@@ -55,6 +58,68 @@ const HOST = String(PREVIEW_FLAGS.host ?? process.env.HOST ?? "localhost");
|
|
|
55
58
|
const AUTOBUILD = process.env.PREVIEW_AUTOBUILD === "1";
|
|
56
59
|
const SKIP_BUILD = process.env.SKIP_BUILD === "1" || !AUTOBUILD;
|
|
57
60
|
|
|
61
|
+
// Active Vite base. We try the internal build bridge first (the
|
|
62
|
+
// `@madojs/mado/vite` plugin writes `_mado/build.json` during `vite
|
|
63
|
+
// build`); if `mado static` has already dropped that file before
|
|
64
|
+
// shipping the production artifact, we fall back to parsing the asset
|
|
65
|
+
// prefix out of `out/index.html`. Both paths converge on the same
|
|
66
|
+
// canonical "/" or "/prefix/" form so the preview server replays the
|
|
67
|
+
// real deployment URL shape regardless of pipeline stage.
|
|
68
|
+
const BASE = detectDeployedBase(OUT);
|
|
69
|
+
|
|
70
|
+
function detectDeployedBase(out) {
|
|
71
|
+
const fromBridge = readBridgeBase(out);
|
|
72
|
+
if (fromBridge != null) return fromBridge;
|
|
73
|
+
const fromHtml = readHtmlBase(out);
|
|
74
|
+
if (fromHtml != null) return fromHtml;
|
|
75
|
+
return "/";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readBridgeBase(out) {
|
|
79
|
+
try {
|
|
80
|
+
const meta = JSON.parse(
|
|
81
|
+
readFileSync(join(out, "_mado/build.json"), "utf8"),
|
|
82
|
+
);
|
|
83
|
+
return meta?.base ? normalizeBase(meta.base) : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readHtmlBase(out) {
|
|
90
|
+
try {
|
|
91
|
+
const html = readFileSync(join(out, "index.html"), "utf8");
|
|
92
|
+
// Look at the first hashed asset; Vite always prefixes it with
|
|
93
|
+
// the deployed base.
|
|
94
|
+
const match = /(?:href|src)="(\/[^"]*?\/)assets\//.exec(html);
|
|
95
|
+
if (match && match[1]) return normalizeBase(match[1]);
|
|
96
|
+
// No prefix means root-deployed.
|
|
97
|
+
if (/(?:href|src)="\/assets\//.test(html)) return "/";
|
|
98
|
+
return null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeBase(raw) {
|
|
105
|
+
if (!raw) return "/";
|
|
106
|
+
let s = String(raw).trim();
|
|
107
|
+
if (!s || s === "/") return "/";
|
|
108
|
+
if (!s.startsWith("/")) s = "/" + s;
|
|
109
|
+
if (!s.endsWith("/")) s = s + "/";
|
|
110
|
+
return s.replace(/\/+/g, "/");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stripBase(pathname) {
|
|
114
|
+
if (BASE === "/") return pathname.startsWith("/") ? pathname : "/" + pathname;
|
|
115
|
+
if (pathname === BASE || pathname === BASE.slice(0, -1)) return "/";
|
|
116
|
+
if (pathname.startsWith(BASE)) {
|
|
117
|
+
const rest = pathname.slice(BASE.length - 1);
|
|
118
|
+
return rest || "/";
|
|
119
|
+
}
|
|
120
|
+
return pathname;
|
|
121
|
+
}
|
|
122
|
+
|
|
58
123
|
const MIME = {
|
|
59
124
|
".html": "text/html; charset=utf-8",
|
|
60
125
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -75,23 +140,20 @@ const MIME = {
|
|
|
75
140
|
// ---------- Optional release ----------
|
|
76
141
|
|
|
77
142
|
if (!SKIP_BUILD) {
|
|
78
|
-
|
|
143
|
+
logger.info("preview", "autobuild", "PREVIEW_AUTOBUILD=1 — running mado release");
|
|
79
144
|
run("node", ["scripts/cli.mjs", "release"]);
|
|
80
145
|
}
|
|
81
146
|
|
|
82
147
|
if (!(await exists(OUT))) {
|
|
83
|
-
|
|
84
|
-
`[preview] missing ${OUT}/ — run \`mado release\` first.`,
|
|
85
|
-
);
|
|
148
|
+
logger.error("preview", "missing-output", `missing ${OUT}/ — run \`mado release\` first`);
|
|
86
149
|
process.exit(1);
|
|
87
150
|
}
|
|
88
151
|
|
|
89
|
-
const spaShell = join(OUT, "
|
|
152
|
+
const spaShell = existsSync(join(OUT, "_mado", "spa.html"))
|
|
153
|
+
? join(OUT, "_mado", "spa.html")
|
|
154
|
+
: join(OUT, "index.html");
|
|
90
155
|
if (!(await exists(spaShell))) {
|
|
91
|
-
|
|
92
|
-
`[preview] missing ${spaShell} — \`mado release\` did not produce an HTML entry.\n` +
|
|
93
|
-
`[preview] Without it any non-baked route will 404 instead of falling back to the SPA.`,
|
|
94
|
-
);
|
|
156
|
+
logger.error("preview", "missing-shell", `missing ${spaShell}; mado release did not produce an HTML entry`);
|
|
95
157
|
process.exit(1);
|
|
96
158
|
}
|
|
97
159
|
|
|
@@ -103,7 +165,24 @@ const isImmutable = (filename) =>
|
|
|
103
165
|
const server = createServer(async (req, res) => {
|
|
104
166
|
try {
|
|
105
167
|
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
106
|
-
const
|
|
168
|
+
const fullPathname = decodeURIComponent(url.pathname);
|
|
169
|
+
// Honour the deployed Vite base: redirect bare `/` to `/${base}/` and
|
|
170
|
+
// strip the prefix before any lookup, so `out/index.html`,
|
|
171
|
+
// `out/assets/...` and `out/docs/index.html` are reachable through
|
|
172
|
+
// the same URL shape the CDN serves.
|
|
173
|
+
if (BASE !== "/") {
|
|
174
|
+
const bareBase = BASE.slice(0, -1);
|
|
175
|
+
if (fullPathname === "/" || fullPathname === bareBase) {
|
|
176
|
+
res.writeHead(302, { location: BASE });
|
|
177
|
+
res.end();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!fullPathname.startsWith(BASE) && fullPathname !== bareBase) {
|
|
181
|
+
res.writeHead(404).end("not found");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const pathname = stripBase(fullPathname);
|
|
107
186
|
const accepts = (req.headers["accept-encoding"] ?? "").toString();
|
|
108
187
|
|
|
109
188
|
const target = await resolveTarget(pathname);
|
|
@@ -134,22 +213,17 @@ const server = createServer(async (req, res) => {
|
|
|
134
213
|
res.writeHead(200, headers);
|
|
135
214
|
res.end(data);
|
|
136
215
|
} catch (err) {
|
|
137
|
-
|
|
216
|
+
logger.error("preview", "request", "request failed", err);
|
|
138
217
|
res.writeHead(500).end(String(err));
|
|
139
218
|
}
|
|
140
219
|
});
|
|
141
220
|
|
|
142
221
|
server.on("error", (err) => {
|
|
143
222
|
if (err.code === "EPERM" || err.code === "EACCES") {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`[preview] tip: this sandbox may disallow binding "${HOST}".\n` +
|
|
147
|
-
`[preview] try: mado preview --host 127.0.0.1`,
|
|
148
|
-
);
|
|
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");
|
|
149
225
|
} else {
|
|
150
|
-
|
|
151
|
-
`[preview] failed to listen on ${HOST}:${PORT}: ${err.message}`,
|
|
152
|
-
);
|
|
226
|
+
logger.error("preview", "listen", `failed to listen on ${HOST}:${PORT}: ${err.message}`);
|
|
153
227
|
}
|
|
154
228
|
process.exit(1);
|
|
155
229
|
});
|
|
@@ -158,7 +232,12 @@ server.listen(PORT, HOST, async () => {
|
|
|
158
232
|
const urlHost = HOST === "0.0.0.0" || HOST === "::" ? "localhost" : HOST;
|
|
159
233
|
console.log("");
|
|
160
234
|
console.log("Mado preview (production-like)");
|
|
161
|
-
|
|
235
|
+
// Print the URL the server actually serves the app at. When the
|
|
236
|
+
// deployment uses a non-trivial Vite `base`, the bare root just
|
|
237
|
+
// 302-redirects to the base prefix, which surprises users who copy
|
|
238
|
+
// the logged URL into a browser and land on "Not Found".
|
|
239
|
+
console.log(` url: http://${urlHost}:${PORT}${BASE}`);
|
|
240
|
+
console.log(` base: ${BASE}`);
|
|
162
241
|
console.log(` out: ${OUT}`);
|
|
163
242
|
console.log(" (Ctrl-C to stop)");
|
|
164
243
|
console.log("");
|
|
@@ -169,7 +248,7 @@ server.listen(PORT, HOST, async () => {
|
|
|
169
248
|
function run(cmd, args, { allowFail = false } = {}) {
|
|
170
249
|
const r = spawnSync(cmd, args, { stdio: "inherit" });
|
|
171
250
|
if (r.status !== 0 && !allowFail) {
|
|
172
|
-
|
|
251
|
+
logger.error("preview", "child-exit", `command "${cmd} ${args.join(" ")}" exited with ${r.status}`);
|
|
173
252
|
process.exit(r.status ?? 1);
|
|
174
253
|
}
|
|
175
254
|
}
|
|
@@ -216,7 +295,9 @@ async function resolveTarget(pathname) {
|
|
|
216
295
|
// extension) deliberately 404 instead — otherwise a 200 on
|
|
217
296
|
// /missing.png would mask real bugs.
|
|
218
297
|
if (!extname(pathname)) {
|
|
219
|
-
const spa = join(OUT, "
|
|
298
|
+
const spa = existsSync(join(OUT, "_mado", "spa.html"))
|
|
299
|
+
? join(OUT, "_mado", "spa.html")
|
|
300
|
+
: join(OUT, "index.html");
|
|
220
301
|
if (await exists(spa)) return spa;
|
|
221
302
|
}
|
|
222
303
|
|