@mandujs/core 0.25.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.25.1",
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
- const { buildClientBundles } = await import("../build");
538
- const manifest = {
539
- version: 1 as const,
540
- routes: [
541
- {
542
- id: "demo",
543
- kind: "page" as const,
544
- pattern: "/",
545
- module: "app/page.tsx",
546
- componentModule: "app/page.tsx",
547
- clientModule: "app/demo.client.tsx",
548
- hydration: {
549
- strategy: "island" as const,
550
- priority: "visible" as const,
551
- preload: false,
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
- if (!result.success) {
564
- console.error("[fr-vendor] errors:", result.errors);
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
- expect(result.success).toBe(true);
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(result.manifest.shared.fastRefresh?.runtime).toBe(
598
+ expect(parsed.manifest?.shared.fastRefresh?.runtime).toBe(
589
599
  "/.mandu/client/_vendor-react-refresh.js",
590
600
  );
591
- expect(result.manifest.shared.fastRefresh?.glue).toBe(
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 type { RoutesManifest } from "../spec/schema";
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: BundleResult;
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
- const manifest: RoutesManifest = {
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) {