@mandujs/core 0.25.0 → 0.25.2
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 +84 -36
- package/src/bundler/safe-build.test.ts +22 -4
- package/src/db/migrations/__tests__/runner.test.ts +665 -661
- package/src/runtime/server.ts +49 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
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 () => {
|
|
@@ -62,14 +94,30 @@ afterAll(async () => {
|
|
|
62
94
|
}
|
|
63
95
|
});
|
|
64
96
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
97
|
+
// Historical note — `MANDU_SKIP_BUNDLER_TESTS` gate REMOVED.
|
|
98
|
+
//
|
|
99
|
+
// A previous revision gated this describe block behind
|
|
100
|
+
// `describe.skipIf(MANDU_SKIP_BUNDLER_TESTS === "1")` because running
|
|
101
|
+
// `bun test src/bundler/` without the gate hung indefinitely on Windows
|
|
102
|
+
// (see Phase 0.6 and `docs/qa/wave-R2-integration-report.md`). Root cause
|
|
103
|
+
// was NOT actually in THIS file — it was a deadlock in `safe-build.test.ts`'s
|
|
104
|
+
// "slot handoff" regression test, which drove Bun's microtask queue with a
|
|
105
|
+
// `while (!stop) { await Promise.resolve() }` sampler. That starved libuv
|
|
106
|
+
// I/O callbacks, so the 7 parallel `safeBuild()` calls never completed, the
|
|
107
|
+
// whole test process hung, and downstream test files (including this one
|
|
108
|
+
// when run in the same invocation) looked flaky when they were simply
|
|
109
|
+
// never reached. The handoff sampler now yields via `setImmediate`, which
|
|
110
|
+
// unblocks Bun.build completion and makes `bun test src/bundler/` finish
|
|
111
|
+
// deterministically in ~35s on Windows. Confirmed green 3/3 runs without
|
|
112
|
+
// the gate on 2026-04-20. If you are tempted to re-introduce the skip here,
|
|
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.
|
|
120
|
+
describe("buildClientBundles vendor shims", () => {
|
|
73
121
|
test("build succeeds", () => {
|
|
74
122
|
if (!result.success) {
|
|
75
123
|
console.error("[build.test] errors:", result.errors);
|
|
@@ -141,16 +141,34 @@ describe("safeBuild", () => {
|
|
|
141
141
|
|
|
142
142
|
let peak = 0;
|
|
143
143
|
let samples = 0;
|
|
144
|
-
// Sample
|
|
145
|
-
//
|
|
146
|
-
//
|
|
144
|
+
// Sample active-slot count while the build burst is in flight. An earlier
|
|
145
|
+
// revision of this test used a `while (!stop) { await Promise.resolve() }`
|
|
146
|
+
// microtask busy-loop to push sampling granularity below setInterval's
|
|
147
|
+
// Windows 4ms-ish clamp. That deadlocks under Bun 1.3.x on Windows:
|
|
148
|
+
// `await Promise.resolve()` stays on the microtask queue, which runs
|
|
149
|
+
// to exhaustion before Bun's libuv I/O phase — so Bun.build completion
|
|
150
|
+
// callbacks never fire, `releaseSlot()` never runs, and the promises
|
|
151
|
+
// returned by the 7 parallel `safeBuild()` calls hang indefinitely.
|
|
152
|
+
// Reproduction: `bun test src/bundler/safe-build.test.ts` times out with
|
|
153
|
+
// only the banner printed (confirmed with a standalone repro of the
|
|
154
|
+
// sampler + 7 safeBuild calls — hung at "start" past 60s).
|
|
155
|
+
//
|
|
156
|
+
// Fix: yield to the macrotask queue via `setImmediate`. This lets
|
|
157
|
+
// libuv I/O callbacks run between samples, so Bun.build completes and
|
|
158
|
+
// `releaseSlot()` advances the queue. Per-tick granularity on Node/Bun
|
|
159
|
+
// is still sub-millisecond and fires ~hundreds of times during a 7-
|
|
160
|
+
// build burst — more than enough to statistically catch the cap+1
|
|
161
|
+
// regression window if it ever returned (the window is microtask-sized,
|
|
162
|
+
// but any cross-tick sampling with high fan-out has a realistic chance
|
|
163
|
+
// of landing inside it). The strict assertion is still `peak <= max`.
|
|
147
164
|
let stop = false;
|
|
148
165
|
const sample = async () => {
|
|
149
166
|
while (!stop) {
|
|
150
167
|
const { active } = _getConcurrencyState();
|
|
151
168
|
if (active > peak) peak = active;
|
|
152
169
|
samples++;
|
|
153
|
-
|
|
170
|
+
// Yield to libuv I/O phase so Bun.build callbacks can fire.
|
|
171
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
154
172
|
}
|
|
155
173
|
};
|
|
156
174
|
const sampler = sample();
|