@mandujs/core 0.25.1 → 0.25.3
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/package.json +7 -1
- package/src/bundler/__tests__/build-runner.ts +101 -0
- package/src/bundler/__tests__/fast-refresh.test.ts +40 -30
- package/src/bundler/build.test.ts +66 -28
- package/src/config/mandu.ts +18 -0
- package/src/config/validate.ts +8 -0
- package/src/content/prebuild.test.ts +322 -0
- package/src/content/prebuild.ts +261 -25
- package/src/db/migrations/__tests__/runner.test.ts +665 -661
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.3",
|
|
4
4
|
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
"./auth/verification": "./src/auth/verification.ts",
|
|
15
15
|
"./client": "./src/client/index.ts",
|
|
16
16
|
"./content": "./src/content/index.ts",
|
|
17
|
+
"./content/prebuild": "./src/content/prebuild.ts",
|
|
18
|
+
"./content/collection": "./src/content/collection.ts",
|
|
19
|
+
"./content/sidebar": "./src/content/sidebar.ts",
|
|
20
|
+
"./content/slug": "./src/content/slug.ts",
|
|
21
|
+
"./content/llms-txt": "./src/content/llms-txt.ts",
|
|
22
|
+
"./content/schema": "./src/content/schema.ts",
|
|
17
23
|
"./db": "./src/db/index.ts",
|
|
18
24
|
"./desktop": "./src/desktop/index.ts",
|
|
19
25
|
"./desktop/worker": "./src/desktop/worker.ts",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Internal helper used by `build.test.ts` + `fast-refresh.test.ts` to run
|
|
4
|
+
* `buildClientBundles` in an isolated `bun` subprocess.
|
|
5
|
+
*
|
|
6
|
+
* # Why this exists
|
|
7
|
+
*
|
|
8
|
+
* When the parent `bun test` process has loaded `react` or `react-dom`
|
|
9
|
+
* (transitively through most test files that import from `src/testing/*`
|
|
10
|
+
* or `src/runtime/*`), Bun 1.3.x's bundler resolver state interacts with
|
|
11
|
+
* `buildClientBundles`' 7-parallel shim fan-out (runtime + router +
|
|
12
|
+
* vendor[5] + devtools) to produce `AggregateError: Bundle failed` on
|
|
13
|
+
* one or more shims. Which shim fails is non-deterministic per run;
|
|
14
|
+
* retrying in-process does not recover because the state is sticky.
|
|
15
|
+
* Running the build in a fresh `bun` subprocess has a clean module graph
|
|
16
|
+
* and builds successfully on the first attempt.
|
|
17
|
+
*
|
|
18
|
+
* Reproducer (in-process): import `"react"` or `"./src/testing/server.ts"`
|
|
19
|
+
* in ANY other test file that ships with `src/bundler/build.test.ts`, and
|
|
20
|
+
* `bun test src/bundler/build.test.ts <that file>` fails ~100 %.
|
|
21
|
+
*
|
|
22
|
+
* # Contract
|
|
23
|
+
*
|
|
24
|
+
* - Invocation: `bun run src/bundler/__tests__/build-runner.ts <rootDir>`
|
|
25
|
+
* - The caller must pre-create `rootDir` with:
|
|
26
|
+
* - `package.json` (any valid contents; shim cache keys walk up to
|
|
27
|
+
* find `node_modules/react`)
|
|
28
|
+
* - `app/demo.client.tsx` (the fixed manifest references this as an
|
|
29
|
+
* island client module)
|
|
30
|
+
* - stdout: JSON blob terminated by `\n`:
|
|
31
|
+
* { "success": boolean,
|
|
32
|
+
* "errors": string[],
|
|
33
|
+
* "manifest": { shared: { fastRefresh?: { runtime: string; glue: string } } }
|
|
34
|
+
* }
|
|
35
|
+
* Only the fields tests consume are serialized; the in-memory manifest
|
|
36
|
+
* is also persisted at `<rootDir>/.mandu/manifest.json` by the build.
|
|
37
|
+
* - exit code: 0 on successful build, 1 on failure (for scripting).
|
|
38
|
+
*
|
|
39
|
+
* # Lifetime
|
|
40
|
+
*
|
|
41
|
+
* Each test spawns, awaits, and discards the subprocess. The subprocess
|
|
42
|
+
* does NOT reuse a vendor cache across runs — every fresh `rootDir` is a
|
|
43
|
+
* clean tmpdir with no prior `.mandu/vendor-cache/`.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { buildClientBundles } from "../build";
|
|
47
|
+
import type { RoutesManifest } from "../../spec/schema";
|
|
48
|
+
|
|
49
|
+
const rootDir = process.argv[2];
|
|
50
|
+
if (!rootDir) {
|
|
51
|
+
console.error("usage: build-runner.ts <rootDir>");
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const manifest: RoutesManifest = {
|
|
56
|
+
version: 1,
|
|
57
|
+
routes: [
|
|
58
|
+
{
|
|
59
|
+
id: "demo",
|
|
60
|
+
kind: "page",
|
|
61
|
+
pattern: "/",
|
|
62
|
+
module: "app/page.tsx",
|
|
63
|
+
componentModule: "app/page.tsx",
|
|
64
|
+
clientModule: "app/demo.client.tsx",
|
|
65
|
+
hydration: {
|
|
66
|
+
strategy: "island",
|
|
67
|
+
priority: "visible",
|
|
68
|
+
preload: false,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const result = await buildClientBundles(manifest, rootDir, {
|
|
76
|
+
minify: false,
|
|
77
|
+
sourcemap: false,
|
|
78
|
+
splitting: false,
|
|
79
|
+
});
|
|
80
|
+
process.stdout.write(
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
success: result.success,
|
|
83
|
+
errors: result.errors,
|
|
84
|
+
manifest: {
|
|
85
|
+
shared: {
|
|
86
|
+
fastRefresh: result.manifest.shared?.fastRefresh ?? null,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}) + "\n",
|
|
90
|
+
);
|
|
91
|
+
process.exit(result.success ? 0 : 1);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
process.stdout.write(
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
success: false,
|
|
96
|
+
errors: [String(err)],
|
|
97
|
+
manifest: null,
|
|
98
|
+
}) + "\n",
|
|
99
|
+
);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
@@ -534,36 +534,46 @@ describe.skipIf(process.env.MANDU_SKIP_BUNDLER_TESTS === "1")(
|
|
|
534
534
|
});
|
|
535
535
|
|
|
536
536
|
test("[E1] dev build produces _vendor-react-refresh.js + _fast-refresh-runtime.js + manifest.shared.fastRefresh entry", async () => {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
};
|
|
556
|
-
// Force dev-mode build by explicit `minify: false`. The build
|
|
557
|
-
// function's `isDev` branch is keyed off `options.minify`.
|
|
558
|
-
const result = await buildClientBundles(manifest, rootDir, {
|
|
559
|
-
minify: false,
|
|
560
|
-
sourcemap: false,
|
|
561
|
-
splitting: false,
|
|
537
|
+
// Force dev-mode build by spawning `buildClientBundles` in a fresh
|
|
538
|
+
// bun subprocess — see build-runner.ts header for the full story.
|
|
539
|
+
// In short: Bun 1.3.x's bundler resolver state gets poisoned when
|
|
540
|
+
// any sibling test file imports `react` / `react-dom`, making the
|
|
541
|
+
// 7-parallel shim fan-out fail with `AggregateError: Bundle failed`
|
|
542
|
+
// ~100 % of the time for the affected shim(s). A subprocess has a
|
|
543
|
+
// clean module graph.
|
|
544
|
+
const { spawn } = await import("node:child_process");
|
|
545
|
+
const runner = path.join(import.meta.dir, "build-runner.ts");
|
|
546
|
+
const cwd = path.resolve(import.meta.dir, "..", "..", "..");
|
|
547
|
+
const out = await new Promise<string>((resolve) => {
|
|
548
|
+
const proc = spawn(process.execPath, ["run", runner, rootDir], {
|
|
549
|
+
cwd,
|
|
550
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
551
|
+
});
|
|
552
|
+
let buf = "";
|
|
553
|
+
proc.stdout.on("data", (d: Buffer) => (buf += d.toString("utf-8")));
|
|
554
|
+
proc.on("close", () => resolve(buf));
|
|
562
555
|
});
|
|
563
|
-
|
|
564
|
-
|
|
556
|
+
const jsonLine =
|
|
557
|
+
out
|
|
558
|
+
.split(/\r?\n/)
|
|
559
|
+
.filter((l) => l.trim().length > 0)
|
|
560
|
+
.pop() ?? "";
|
|
561
|
+
let parsed: {
|
|
562
|
+
success: boolean;
|
|
563
|
+
errors: string[];
|
|
564
|
+
manifest: { shared: { fastRefresh: { runtime: string; glue: string } | null } } | null;
|
|
565
|
+
};
|
|
566
|
+
try {
|
|
567
|
+
parsed = JSON.parse(jsonLine);
|
|
568
|
+
} catch (e) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
`build-runner stdout could not be parsed as JSON: ${String(e)}\nLast line: ${jsonLine}`,
|
|
571
|
+
);
|
|
565
572
|
}
|
|
566
|
-
|
|
573
|
+
if (!parsed.success) {
|
|
574
|
+
console.error("[fr-vendor] errors:", parsed.errors);
|
|
575
|
+
}
|
|
576
|
+
expect(parsed.success).toBe(true);
|
|
567
577
|
// Both shim files must exist on disk
|
|
568
578
|
const glueFile = path.join(
|
|
569
579
|
rootDir,
|
|
@@ -585,10 +595,10 @@ describe.skipIf(process.env.MANDU_SKIP_BUNDLER_TESTS === "1")(
|
|
|
585
595
|
// bundle output — avoids requiring a full evaluation)
|
|
586
596
|
expect(glueContents).toContain("installGlobal");
|
|
587
597
|
// Manifest exposes the paths
|
|
588
|
-
expect(
|
|
598
|
+
expect(parsed.manifest?.shared.fastRefresh?.runtime).toBe(
|
|
589
599
|
"/.mandu/client/_vendor-react-refresh.js",
|
|
590
600
|
);
|
|
591
|
-
expect(
|
|
601
|
+
expect(parsed.manifest?.shared.fastRefresh?.glue).toBe(
|
|
592
602
|
"/.mandu/client/_fast-refresh-runtime.js",
|
|
593
603
|
);
|
|
594
604
|
});
|
|
@@ -2,19 +2,74 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtemp, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { pathToFileURL } from "url";
|
|
5
|
-
import
|
|
6
|
-
import type { BundleResult } from "./types";
|
|
7
|
-
import { buildClientBundles } from "./build";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
8
6
|
|
|
9
7
|
// 모든 테스트가 하나의 빌드 결과를 공유 — 병렬 Bun.build 충돌 방지
|
|
10
8
|
let rootDir: string;
|
|
11
|
-
let result:
|
|
9
|
+
let result: { success: boolean; errors: string[] };
|
|
12
10
|
|
|
13
11
|
async function importBuiltModule(relativePath: string): Promise<Record<string, unknown>> {
|
|
14
12
|
const fileUrl = pathToFileURL(path.join(rootDir, relativePath)).href;
|
|
15
13
|
return import(`${fileUrl}?t=${Date.now()}`);
|
|
16
14
|
}
|
|
17
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Run `buildClientBundles` in an isolated `bun` subprocess.
|
|
18
|
+
*
|
|
19
|
+
* Bun 1.3.x exhibits a deterministic `AggregateError: Bundle failed` when
|
|
20
|
+
* `buildClientBundles` is called from a test file AND the same `bun test`
|
|
21
|
+
* process has previously imported `react` / `react-dom` through any sibling
|
|
22
|
+
* test file (happens transitively through almost every `src/testing/*` or
|
|
23
|
+
* `src/runtime/*` consumer). Retrying in-process does not recover — the
|
|
24
|
+
* resolver state is sticky. A fresh subprocess has a clean module graph.
|
|
25
|
+
* See `__tests__/build-runner.ts` for the subprocess entrypoint and more
|
|
26
|
+
* background.
|
|
27
|
+
*/
|
|
28
|
+
async function runBuildInSubprocess(root: string): Promise<{
|
|
29
|
+
success: boolean;
|
|
30
|
+
errors: string[];
|
|
31
|
+
}> {
|
|
32
|
+
const runner = path.join(
|
|
33
|
+
import.meta.dir,
|
|
34
|
+
"__tests__",
|
|
35
|
+
"build-runner.ts",
|
|
36
|
+
);
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const proc = spawn(process.execPath, ["run", runner, root], {
|
|
39
|
+
cwd: path.resolve(import.meta.dir, "..", ".."),
|
|
40
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
41
|
+
});
|
|
42
|
+
let out = "";
|
|
43
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
44
|
+
out += chunk.toString("utf-8");
|
|
45
|
+
});
|
|
46
|
+
proc.on("close", () => {
|
|
47
|
+
// Find the final JSON line — the runner may log Mandu dev banners
|
|
48
|
+
// (e.g. "[Mandu] DevTools …") before emitting the payload. The
|
|
49
|
+
// contract is: last non-empty line is the JSON blob.
|
|
50
|
+
const lines = out.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
51
|
+
const last = lines[lines.length - 1] ?? "";
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(last);
|
|
54
|
+
resolve({
|
|
55
|
+
success: parsed.success === true,
|
|
56
|
+
errors: Array.isArray(parsed.errors) ? parsed.errors : [],
|
|
57
|
+
});
|
|
58
|
+
} catch (e) {
|
|
59
|
+
resolve({
|
|
60
|
+
success: false,
|
|
61
|
+
errors: [
|
|
62
|
+
`build-runner output could not be parsed as JSON: ${String(e)}\nLast stdout line: ${last}`,
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
proc.on("error", (err) => {
|
|
68
|
+
resolve({ success: false, errors: [`spawn failed: ${String(err)}`] });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
18
73
|
beforeAll(async () => {
|
|
19
74
|
rootDir = await mkdtemp(path.join(import.meta.dir, ".tmp-bundler-"));
|
|
20
75
|
|
|
@@ -30,30 +85,7 @@ beforeAll(async () => {
|
|
|
30
85
|
"utf-8",
|
|
31
86
|
);
|
|
32
87
|
|
|
33
|
-
|
|
34
|
-
version: 1,
|
|
35
|
-
routes: [
|
|
36
|
-
{
|
|
37
|
-
id: "demo",
|
|
38
|
-
kind: "page",
|
|
39
|
-
pattern: "/",
|
|
40
|
-
module: "app/page.tsx",
|
|
41
|
-
componentModule: "app/page.tsx",
|
|
42
|
-
clientModule: "app/demo.client.tsx",
|
|
43
|
-
hydration: {
|
|
44
|
-
strategy: "island",
|
|
45
|
-
priority: "visible",
|
|
46
|
-
preload: false,
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
result = await buildClientBundles(manifest, rootDir, {
|
|
53
|
-
minify: false,
|
|
54
|
-
sourcemap: false,
|
|
55
|
-
splitting: false,
|
|
56
|
-
});
|
|
88
|
+
result = await runBuildInSubprocess(rootDir);
|
|
57
89
|
});
|
|
58
90
|
|
|
59
91
|
afterAll(async () => {
|
|
@@ -79,6 +111,12 @@ afterAll(async () => {
|
|
|
79
111
|
// deterministically in ~35s on Windows. Confirmed green 3/3 runs without
|
|
80
112
|
// the gate on 2026-04-20. If you are tempted to re-introduce the skip here,
|
|
81
113
|
// first check whether a sibling test is starving the event loop.
|
|
114
|
+
//
|
|
115
|
+
// A second, independent flake — Bun.build `AggregateError: Bundle failed`
|
|
116
|
+
// when another test file in the same invocation has imported `react` —
|
|
117
|
+
// is now sidestepped by running `buildClientBundles` in a spawned `bun`
|
|
118
|
+
// subprocess via `__tests__/build-runner.ts`. In-process retry does not
|
|
119
|
+
// recover from that one; a fresh module graph does.
|
|
82
120
|
describe("buildClientBundles vendor shims", () => {
|
|
83
121
|
test("build succeeds", () => {
|
|
84
122
|
if (!result.success) {
|
package/src/config/mandu.ts
CHANGED
|
@@ -198,6 +198,24 @@ export interface ManduConfig {
|
|
|
198
198
|
* `autoPrebuild === false`. Relative to project root.
|
|
199
199
|
*/
|
|
200
200
|
contentDir?: string;
|
|
201
|
+
/**
|
|
202
|
+
* Issue #203 — Per-script wall-clock timeout for prebuild scripts
|
|
203
|
+
* (milliseconds). Default: `120_000` (2 minutes), matching the MCP
|
|
204
|
+
* `runCommand()` convention (#136). Override for projects that ship
|
|
205
|
+
* slow seed generators (e.g. large docs indexers, image pipelines).
|
|
206
|
+
*
|
|
207
|
+
* Precedence at runtime, highest first:
|
|
208
|
+
* 1. `MANDU_PREBUILD_TIMEOUT_MS` env var — useful for one-off CI
|
|
209
|
+
* overrides without committing to the config.
|
|
210
|
+
* 2. This field (`dev.prebuildTimeoutMs`).
|
|
211
|
+
* 3. Default 120_000 ms.
|
|
212
|
+
*
|
|
213
|
+
* When the timeout fires, `runPrebuildScripts` throws a
|
|
214
|
+
* `PrebuildTimeoutError` whose message names the failing script path,
|
|
215
|
+
* the limit, AND the two override paths — so the user does not need
|
|
216
|
+
* to re-read this comment to recover.
|
|
217
|
+
*/
|
|
218
|
+
prebuildTimeoutMs?: number;
|
|
201
219
|
};
|
|
202
220
|
fsRoutes?: {
|
|
203
221
|
routesDir?: string;
|
package/src/config/validate.ts
CHANGED
|
@@ -122,6 +122,14 @@ const DevConfigSchema = z
|
|
|
122
122
|
* an empty pattern. Default `"content"`.
|
|
123
123
|
*/
|
|
124
124
|
contentDir: z.string().min(1).default("content"),
|
|
125
|
+
/**
|
|
126
|
+
* Issue #203 — per-script wall-clock timeout (ms) for
|
|
127
|
+
* `scripts/prebuild-*.ts`. `undefined` = use default (120_000 ms) or
|
|
128
|
+
* the `MANDU_PREBUILD_TIMEOUT_MS` env var if set. Explicit positive
|
|
129
|
+
* integer overrides both. The boundary check mirrors
|
|
130
|
+
* `server.rateLimit.windowMs` style — positive integers only.
|
|
131
|
+
*/
|
|
132
|
+
prebuildTimeoutMs: z.number().int().positive().optional(),
|
|
125
133
|
})
|
|
126
134
|
.strict();
|
|
127
135
|
|