@mandujs/core 0.54.12 → 0.54.14
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 +9 -1
- package/scripts/postinstall-lock.ts +153 -153
- package/src/a11y/run-audit.ts +15 -15
- package/src/agent/__tests__/context.test.ts +49 -1
- package/src/agent/context.ts +535 -535
- package/src/agent/index.ts +6 -6
- package/src/agent/plan.ts +282 -282
- package/src/agent/repair.ts +171 -171
- package/src/agent/sync.ts +200 -200
- package/src/agent/types.ts +8 -0
- package/src/agent/verify.ts +100 -2
- package/src/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__tests__/build-runner.ts +36 -16
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +20 -20
- package/src/bundler/__tests__/fast-refresh.test.ts +24 -17
- package/src/bundler/analyzer.ts +15 -15
- package/src/bundler/build.test.ts +136 -48
- package/src/bundler/build.ts +193 -122
- package/src/bundler/css.ts +42 -42
- package/src/bundler/manifest-schema.ts +21 -21
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
- package/src/bundler/plugins/block-generated-imports.ts +13 -13
- package/src/bundler/types.ts +31 -31
- package/src/client/island.ts +79 -79
- package/src/config/validate.ts +1 -1
- package/src/contract/schema.ts +7 -0
- package/src/deploy/inference/context.ts +82 -82
- package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- package/src/error/formatter.ts +10 -1
- package/src/experimental/index.ts +10 -0
- package/src/filling/context.ts +17 -17
- package/src/filling/filling.ts +22 -1
- package/src/filling/index.ts +15 -1
- package/src/generator/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +210 -210
- package/src/guard/check.ts +9 -9
- package/src/guard/config-guard.ts +13 -13
- package/src/guard/fs-routes-policy.ts +51 -51
- package/src/guard/index.ts +11 -11
- package/src/index.ts +0 -10
- package/src/internal/index.ts +25 -0
- package/src/kitchen/api/file-api.ts +11 -11
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +6 -6
- package/src/resource/__tests__/schema.test.ts +14 -14
- package/src/resource/ddl/__tests__/emit.test.ts +165 -165
- package/src/resource/ddl/emit.ts +146 -146
- package/src/resource/generator-schema.ts +11 -11
- package/src/resource/generators/slot.ts +72 -72
- package/src/resource/schema.ts +21 -21
- package/src/router/client-entry.test.ts +84 -41
- package/src/router/client-entry.ts +156 -89
- package/src/router/fs-routes.test.ts +90 -0
- package/src/router/fs-routes.ts +37 -29
- package/src/router/fs-scanner.ts +81 -33
- package/src/router/fs-types.ts +8 -5
- package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
- package/src/runtime/__tests__/page-render-response.test.ts +164 -103
- package/src/runtime/__tests__/request-middleware.test.ts +70 -70
- package/src/runtime/devtools-adapter.ts +68 -68
- package/src/runtime/escape.ts +34 -34
- package/src/runtime/image-feature.ts +15 -0
- package/src/runtime/observability-lifecycle.ts +290 -290
- package/src/runtime/page-render-response.ts +208 -106
- package/src/runtime/rate-limit.ts +231 -0
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/router.test.ts +4 -4
- package/src/runtime/router.ts +10 -12
- package/src/runtime/scheduler-lifecycle.ts +64 -0
- package/src/runtime/server.ts +148 -353
- package/src/runtime/ssr.ts +59 -59
- package/src/runtime/static-files.ts +289 -289
- package/src/runtime/streaming-ssr.ts +22 -22
- package/src/spec/schema.ts +4 -3
- package/src/watcher/__tests__/watcher.test.ts +59 -59
- package/src/watcher/watcher.ts +61 -61
|
@@ -28,13 +28,13 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
|
28
28
|
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
29
29
|
import { tmpdir } from "os";
|
|
30
30
|
import path from "path";
|
|
31
|
-
import type { RouteSpec, RoutesManifest } from "../../spec/schema";
|
|
32
|
-
import {
|
|
33
|
-
_testOnly_scanIslandFiles,
|
|
34
|
-
_testOnly_scanPartialFiles,
|
|
35
|
-
_testOnly_getHydratedRoutes,
|
|
36
|
-
_testOnly_getHydrationRoutesMissingClientModule,
|
|
37
|
-
} from "../build";
|
|
31
|
+
import type { RouteSpec, RoutesManifest } from "../../spec/schema";
|
|
32
|
+
import {
|
|
33
|
+
_testOnly_scanIslandFiles,
|
|
34
|
+
_testOnly_scanPartialFiles,
|
|
35
|
+
_testOnly_getHydratedRoutes,
|
|
36
|
+
_testOnly_getHydrationRoutesMissingClientModule,
|
|
37
|
+
} from "../build";
|
|
38
38
|
import { HMR_PERF } from "../../perf/hmr-markers";
|
|
39
39
|
import {
|
|
40
40
|
_resetCacheForTesting as _resetPerfCache,
|
|
@@ -374,7 +374,7 @@ describe("Phase 7.1 R1 Agent C — getHydratedRoutes filter", () => {
|
|
|
374
374
|
});
|
|
375
375
|
});
|
|
376
376
|
|
|
377
|
-
describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", () => {
|
|
377
|
+
describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", () => {
|
|
378
378
|
let project: ReturnType<typeof createProject>;
|
|
379
379
|
|
|
380
380
|
beforeEach(() => {
|
|
@@ -502,55 +502,55 @@ describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", (
|
|
|
502
502
|
];
|
|
503
503
|
const result = await _testOnly_scanIslandFiles(routes, project.rootDir);
|
|
504
504
|
expect(result).toEqual([]);
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
describe("hydration route validation", () => {
|
|
509
|
-
it("finds pages that request hydration without a client module", () => {
|
|
510
|
-
const manifest = {
|
|
511
|
-
version: 1,
|
|
512
|
-
routes: [
|
|
513
|
-
{
|
|
514
|
-
id: "login",
|
|
515
|
-
pattern: "/login",
|
|
516
|
-
kind: "page",
|
|
517
|
-
module: "app/login/page.tsx",
|
|
518
|
-
componentModule: "app/login/page.tsx",
|
|
519
|
-
hydration: { strategy: "full", priority: "immediate", preload: false },
|
|
520
|
-
},
|
|
521
|
-
pureSsrRoute("about", "/about", "app/about"),
|
|
522
|
-
pageRoute("dashboard", "/dashboard", "app/dashboard"),
|
|
523
|
-
],
|
|
524
|
-
} as RoutesManifest;
|
|
525
|
-
|
|
526
|
-
const missing = _testOnly_getHydrationRoutesMissingClientModule(manifest);
|
|
527
|
-
expect(missing.map((route) => route.id)).toEqual(["login"]);
|
|
528
|
-
});
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
describe("partial bundle scan", () => {
|
|
532
|
-
let project: ReturnType<typeof createProject>;
|
|
533
|
-
|
|
534
|
-
beforeEach(() => {
|
|
535
|
-
project = createProject();
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
afterEach(() => {
|
|
539
|
-
try {
|
|
540
|
-
rmSync(project.rootDir, { recursive: true, force: true });
|
|
541
|
-
} catch {
|
|
542
|
-
/* Windows lock tolerance */
|
|
543
|
-
}
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
it("discovers *.partial.tsx files outside .mandu and node_modules", async () => {
|
|
547
|
-
project.writeIslandFile("app", "Home.partial.tsx");
|
|
548
|
-
project.writeIslandFile(".mandu/client", "Stale.partial.tsx");
|
|
549
|
-
project.writeIslandFile("node_modules/pkg", "Ignored.partial.tsx");
|
|
550
|
-
|
|
551
|
-
const result = await _testOnly_scanPartialFiles(project.rootDir);
|
|
552
|
-
|
|
553
|
-
expect(result.map((entry) => entry.name)).toEqual(["Home"]);
|
|
554
|
-
expect(result[0].priority).toBe("visible");
|
|
555
|
-
});
|
|
556
|
-
});
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
describe("hydration route validation", () => {
|
|
509
|
+
it("finds pages that request hydration without a client module", () => {
|
|
510
|
+
const manifest = {
|
|
511
|
+
version: 1,
|
|
512
|
+
routes: [
|
|
513
|
+
{
|
|
514
|
+
id: "login",
|
|
515
|
+
pattern: "/login",
|
|
516
|
+
kind: "page",
|
|
517
|
+
module: "app/login/page.tsx",
|
|
518
|
+
componentModule: "app/login/page.tsx",
|
|
519
|
+
hydration: { strategy: "full", priority: "immediate", preload: false },
|
|
520
|
+
},
|
|
521
|
+
pureSsrRoute("about", "/about", "app/about"),
|
|
522
|
+
pageRoute("dashboard", "/dashboard", "app/dashboard"),
|
|
523
|
+
],
|
|
524
|
+
} as RoutesManifest;
|
|
525
|
+
|
|
526
|
+
const missing = _testOnly_getHydrationRoutesMissingClientModule(manifest);
|
|
527
|
+
expect(missing.map((route) => route.id)).toEqual(["login"]);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe("partial bundle scan", () => {
|
|
532
|
+
let project: ReturnType<typeof createProject>;
|
|
533
|
+
|
|
534
|
+
beforeEach(() => {
|
|
535
|
+
project = createProject();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
afterEach(() => {
|
|
539
|
+
try {
|
|
540
|
+
rmSync(project.rootDir, { recursive: true, force: true });
|
|
541
|
+
} catch {
|
|
542
|
+
/* Windows lock tolerance */
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("discovers *.partial.tsx files outside .mandu and node_modules", async () => {
|
|
547
|
+
project.writeIslandFile("app", "Home.partial.tsx");
|
|
548
|
+
project.writeIslandFile(".mandu/client", "Stale.partial.tsx");
|
|
549
|
+
project.writeIslandFile("node_modules/pkg", "Ignored.partial.tsx");
|
|
550
|
+
|
|
551
|
+
const result = await _testOnly_scanPartialFiles(project.rootDir);
|
|
552
|
+
|
|
553
|
+
expect(result.map((entry) => entry.name)).toEqual(["Home"]);
|
|
554
|
+
expect(result[0].priority).toBe("visible");
|
|
555
|
+
});
|
|
556
|
+
});
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { __private } from "../css";
|
|
3
|
-
|
|
4
|
-
describe("CSS Tailwind command resolution", () => {
|
|
5
|
-
test("uses process.execPath when it is Bun", () => {
|
|
6
|
-
expect(
|
|
7
|
-
__private.getTailwindCommand(["@tailwindcss/cli"], "C:\\tools\\bun.exe"),
|
|
8
|
-
).toEqual(["C:\\tools\\bun.exe", "x", "@tailwindcss/cli"]);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test("does not treat standalone mandu.exe as Bun", () => {
|
|
12
|
-
expect(
|
|
13
|
-
__private.getTailwindCommand(
|
|
14
|
-
["@tailwindcss/cli"],
|
|
15
|
-
"C:\\Users\\User\\AppData\\Local\\Mandu\\bin\\mandu.exe",
|
|
16
|
-
() => "C:\\Users\\User\\.bun\\bin\\bun.exe",
|
|
17
|
-
),
|
|
18
|
-
).toEqual(["C:\\Users\\User\\.bun\\bin\\bun.exe", "x", "@tailwindcss/cli"]);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { __private } from "../css";
|
|
3
|
+
|
|
4
|
+
describe("CSS Tailwind command resolution", () => {
|
|
5
|
+
test("uses process.execPath when it is Bun", () => {
|
|
6
|
+
expect(
|
|
7
|
+
__private.getTailwindCommand(["@tailwindcss/cli"], "C:\\tools\\bun.exe"),
|
|
8
|
+
).toEqual(["C:\\tools\\bun.exe", "x", "@tailwindcss/cli"]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("does not treat standalone mandu.exe as Bun", () => {
|
|
12
|
+
expect(
|
|
13
|
+
__private.getTailwindCommand(
|
|
14
|
+
["@tailwindcss/cli"],
|
|
15
|
+
"C:\\Users\\User\\AppData\\Local\\Mandu\\bin\\mandu.exe",
|
|
16
|
+
() => "C:\\Users\\User\\.bun\\bin\\bun.exe",
|
|
17
|
+
),
|
|
18
|
+
).toEqual(["C:\\Users\\User\\.bun\\bin\\bun.exe", "x", "@tailwindcss/cli"]);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* packages/core/src/runtime/fast-refresh-runtime.ts
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
23
|
-
import { mkdtemp, mkdir, rm, writeFile, readFile } from "fs/promises";
|
|
24
|
-
import path from "path";
|
|
22
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
23
|
+
import { mkdtemp, mkdir, rm, writeFile, readFile } from "fs/promises";
|
|
24
|
+
import path from "path";
|
|
25
25
|
|
|
26
26
|
import {
|
|
27
27
|
appendBoundary,
|
|
@@ -40,12 +40,19 @@ import {
|
|
|
40
40
|
_isRefreshScheduledForTests,
|
|
41
41
|
type ReactRefreshRuntime,
|
|
42
42
|
} from "../../runtime/fast-refresh-runtime";
|
|
43
|
-
import {
|
|
44
|
-
createManduHot,
|
|
45
|
-
dispatchReplacement,
|
|
46
|
-
_resetRegistryForTests,
|
|
47
|
-
} from "../../runtime/hmr-client";
|
|
48
|
-
import { generateFastRefreshPreamble } from "../dev";
|
|
43
|
+
import {
|
|
44
|
+
createManduHot,
|
|
45
|
+
dispatchReplacement,
|
|
46
|
+
_resetRegistryForTests,
|
|
47
|
+
} from "../../runtime/hmr-client";
|
|
48
|
+
import { generateFastRefreshPreamble } from "../dev";
|
|
49
|
+
|
|
50
|
+
const repoTempRoot = path.resolve(import.meta.dir, "../../../../..", ".tmp-test-artifacts");
|
|
51
|
+
|
|
52
|
+
async function mkRepoTempDir(prefix: string): Promise<string> {
|
|
53
|
+
await mkdir(repoTempRoot, { recursive: true });
|
|
54
|
+
return mkdtemp(path.join(repoTempRoot, prefix));
|
|
55
|
+
}
|
|
49
56
|
|
|
50
57
|
// ═══════════════════════════════════════════════════════════════════
|
|
51
58
|
// Section A — plugin pure unit tests
|
|
@@ -263,10 +270,10 @@ describe("manduHMR — __MANDU_HMR__ global behavior", () => {
|
|
|
263
270
|
describe.skipIf(process.env.MANDU_SKIP_BUNDLER_TESTS === "1")(
|
|
264
271
|
"Bun.build({ reactFastRefresh: true }) + fastRefreshPlugin()",
|
|
265
272
|
() => {
|
|
266
|
-
let rootDir: string;
|
|
267
|
-
|
|
268
|
-
beforeAll(async () => {
|
|
269
|
-
rootDir = await
|
|
273
|
+
let rootDir: string;
|
|
274
|
+
|
|
275
|
+
beforeAll(async () => {
|
|
276
|
+
rootDir = await mkRepoTempDir("fr-build-");
|
|
270
277
|
// Three source files — one boundary, one plain, one .island.tsx —
|
|
271
278
|
// give us the minimal matrix for the plugin's include filter.
|
|
272
279
|
await writeFile(
|
|
@@ -512,10 +519,10 @@ describe("dispatchReplacement + __MANDU_HMR__ integration", () => {
|
|
|
512
519
|
describe.skipIf(process.env.MANDU_SKIP_BUNDLER_TESTS === "1")(
|
|
513
520
|
"buildVendorShims emits fast-refresh shims in dev mode",
|
|
514
521
|
() => {
|
|
515
|
-
let rootDir: string;
|
|
516
|
-
|
|
517
|
-
beforeAll(async () => {
|
|
518
|
-
rootDir = await
|
|
522
|
+
let rootDir: string;
|
|
523
|
+
|
|
524
|
+
beforeAll(async () => {
|
|
525
|
+
rootDir = await mkRepoTempDir("fr-vendor-");
|
|
519
526
|
await mkdir(path.join(rootDir, "app"), { recursive: true });
|
|
520
527
|
await writeFile(
|
|
521
528
|
path.join(rootDir, "package.json"),
|
package/src/bundler/analyzer.ts
CHANGED
|
@@ -341,26 +341,26 @@ export async function analyzeBundle(
|
|
|
341
341
|
deps: entry.dependencies ?? [],
|
|
342
342
|
});
|
|
343
343
|
}
|
|
344
|
-
for (const [islandName, entry] of Object.entries(manifest.islands ?? {})) {
|
|
345
|
-
// Avoid double-counting: if a per-island chunk shares its route id with
|
|
346
|
-
// a route-level bundle, we prefer the island entry (finer granularity).
|
|
347
|
-
const existing = islandSources.findIndex((s) => s.name === islandName);
|
|
344
|
+
for (const [islandName, entry] of Object.entries(manifest.islands ?? {})) {
|
|
345
|
+
// Avoid double-counting: if a per-island chunk shares its route id with
|
|
346
|
+
// a route-level bundle, we prefer the island entry (finer granularity).
|
|
347
|
+
const existing = islandSources.findIndex((s) => s.name === islandName);
|
|
348
348
|
if (existing !== -1) islandSources.splice(existing, 1);
|
|
349
349
|
islandSources.push({
|
|
350
350
|
name: islandName,
|
|
351
351
|
url: entry.js,
|
|
352
352
|
priority: entry.priority,
|
|
353
|
-
deps: [],
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
for (const [partialName, entry] of Object.entries(manifest.partials ?? {})) {
|
|
357
|
-
islandSources.push({
|
|
358
|
-
name: `partial:${partialName}`,
|
|
359
|
-
url: entry.js,
|
|
360
|
-
priority: entry.priority,
|
|
361
|
-
deps: [],
|
|
362
|
-
});
|
|
363
|
-
}
|
|
353
|
+
deps: [],
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
for (const [partialName, entry] of Object.entries(manifest.partials ?? {})) {
|
|
357
|
+
islandSources.push({
|
|
358
|
+
name: `partial:${partialName}`,
|
|
359
|
+
url: entry.js,
|
|
360
|
+
priority: entry.priority,
|
|
361
|
+
deps: [],
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
364
|
|
|
365
365
|
const islands: AnalyzeIsland[] = [];
|
|
366
366
|
for (const src of islandSources) {
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtemp, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { pathToFileURL } from "url";
|
|
5
|
-
|
|
6
|
-
// 모든 테스트가 하나의 빌드 결과를 공유 — 병렬 Bun.build 충돌 방지
|
|
7
|
-
let rootDir: string;
|
|
8
|
-
let result: { success: boolean; errors: string[] };
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
|
|
6
|
+
// 모든 테스트가 하나의 빌드 결과를 공유 — 병렬 Bun.build 충돌 방지
|
|
7
|
+
let rootDir: string;
|
|
8
|
+
let result: { success: boolean; errors: string[] };
|
|
9
|
+
const repoTempRoot = path.resolve(import.meta.dir, "../../../..", ".tmp-test-artifacts");
|
|
10
|
+
|
|
11
|
+
async function mkRepoTempDir(prefix: string): Promise<string> {
|
|
12
|
+
await mkdir(repoTempRoot, { recursive: true });
|
|
13
|
+
return mkdtemp(path.join(repoTempRoot, prefix));
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
async function importBuiltModule(relativePath: string): Promise<Record<string, unknown>> {
|
|
11
17
|
const fileUrl = pathToFileURL(path.join(rootDir, relativePath)).href;
|
|
@@ -24,10 +30,13 @@ async function importBuiltModule(relativePath: string): Promise<Record<string, u
|
|
|
24
30
|
* See `__tests__/build-runner.ts` for the subprocess entrypoint and more
|
|
25
31
|
* background.
|
|
26
32
|
*/
|
|
27
|
-
async function runBuildInSubprocess(root: string, mode?: string): Promise<{
|
|
28
|
-
success: boolean;
|
|
29
|
-
errors: string[];
|
|
30
|
-
|
|
33
|
+
async function runBuildInSubprocess(root: string, mode?: string): Promise<{
|
|
34
|
+
success: boolean;
|
|
35
|
+
errors: string[];
|
|
36
|
+
manifest?: {
|
|
37
|
+
bundles?: Record<string, { js?: string }>;
|
|
38
|
+
} | null;
|
|
39
|
+
}> {
|
|
31
40
|
const runner = path.join(
|
|
32
41
|
import.meta.dir,
|
|
33
42
|
"__tests__",
|
|
@@ -52,25 +61,27 @@ async function runBuildInSubprocess(root: string, mode?: string): Promise<{
|
|
|
52
61
|
const last = lines[lines.length - 1] ?? "";
|
|
53
62
|
try {
|
|
54
63
|
const parsed = JSON.parse(last);
|
|
55
|
-
return {
|
|
56
|
-
success: parsed.success === true,
|
|
57
|
-
errors: Array.isArray(parsed.errors) ? parsed.errors : [],
|
|
58
|
-
|
|
64
|
+
return {
|
|
65
|
+
success: parsed.success === true,
|
|
66
|
+
errors: Array.isArray(parsed.errors) ? parsed.errors : [],
|
|
67
|
+
manifest: parsed.manifest ?? null,
|
|
68
|
+
};
|
|
59
69
|
} catch (e) {
|
|
60
|
-
return {
|
|
61
|
-
success: false,
|
|
62
|
-
errors: [
|
|
63
|
-
`build-runner output could not be parsed as JSON: ${String(e)}\nLast stdout line: ${last}`,
|
|
64
|
-
],
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
errors: [
|
|
73
|
+
`build-runner output could not be parsed as JSON: ${String(e)}\nLast stdout line: ${last}`,
|
|
74
|
+
],
|
|
75
|
+
manifest: null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return { success: false, errors: [`spawn failed: ${String(err)}`], manifest: null };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
71
82
|
|
|
72
|
-
beforeAll(async () => {
|
|
73
|
-
rootDir = await
|
|
83
|
+
beforeAll(async () => {
|
|
84
|
+
rootDir = await mkRepoTempDir("bundler-");
|
|
74
85
|
|
|
75
86
|
await mkdir(path.join(rootDir, "app"), { recursive: true });
|
|
76
87
|
await writeFile(
|
|
@@ -186,8 +197,8 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
186
197
|
expect(runtimeSource).toContain("JSON.parse");
|
|
187
198
|
});
|
|
188
199
|
|
|
189
|
-
test("does not bundle a server page when stale manifest marks page.tsx as clientModule", async () => {
|
|
190
|
-
const staleRoot = await
|
|
200
|
+
test("does not bundle a server page when stale manifest marks page.tsx as clientModule", async () => {
|
|
201
|
+
const staleRoot = await mkRepoTempDir("stale-client-module-");
|
|
191
202
|
try {
|
|
192
203
|
await mkdir(path.join(staleRoot, "app"), { recursive: true });
|
|
193
204
|
await mkdir(path.join(staleRoot, "src", "shared", "contracts"), { recursive: true });
|
|
@@ -219,8 +230,8 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
219
230
|
}
|
|
220
231
|
});
|
|
221
232
|
|
|
222
|
-
test("rewrites route-component clientModule to the real client import before bundling", async () => {
|
|
223
|
-
const routeClientRoot = await
|
|
233
|
+
test("rewrites route-component clientModule to the real client import before bundling", async () => {
|
|
234
|
+
const routeClientRoot = await mkRepoTempDir("route-client-import-");
|
|
224
235
|
try {
|
|
225
236
|
await mkdir(path.join(routeClientRoot, "app", "login"), { recursive: true });
|
|
226
237
|
await mkdir(path.join(routeClientRoot, "src", "client", "pages", "login"), { recursive: true });
|
|
@@ -237,16 +248,16 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
237
248
|
"}\n",
|
|
238
249
|
"utf-8",
|
|
239
250
|
);
|
|
240
|
-
await writeFile(
|
|
241
|
-
path.join(routeClientRoot, "src", "client", "pages", "login", "LoginPage.client.tsx"),
|
|
242
|
-
'"use client";\n' +
|
|
243
|
-
'import { useState } from "react";\n' +
|
|
244
|
-
"export default function LoginPage() {\n" +
|
|
245
|
-
' const [email, setEmail] = useState("");\n' +
|
|
246
|
-
' return <form><input value={email} onChange={(event) => setEmail(event.currentTarget.value)} /></form>;\n' +
|
|
247
|
-
"}\n",
|
|
248
|
-
"utf-8",
|
|
249
|
-
);
|
|
251
|
+
await writeFile(
|
|
252
|
+
path.join(routeClientRoot, "src", "client", "pages", "login", "LoginPage.client.tsx"),
|
|
253
|
+
'"use client";\n' +
|
|
254
|
+
'import { useState } from "react";\n' +
|
|
255
|
+
"export default function LoginPage() {\n" +
|
|
256
|
+
' const [email, setEmail] = useState("");\n' +
|
|
257
|
+
' return <form><input value={email} onChange={(event) => setEmail(event.currentTarget.value)} /></form>;\n' +
|
|
258
|
+
"}\n",
|
|
259
|
+
"utf-8",
|
|
260
|
+
);
|
|
250
261
|
|
|
251
262
|
const routeClientResult = await runBuildInSubprocess(routeClientRoot, "server-page-route-client-import");
|
|
252
263
|
expect(routeClientResult.success).toBe(true);
|
|
@@ -263,11 +274,88 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
263
274
|
expect(parseResult.success).toBe(true);
|
|
264
275
|
} finally {
|
|
265
276
|
await rm(routeClientRoot, { recursive: true, force: true });
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test("
|
|
270
|
-
const
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("bundles route-level named client exports without requiring a default export", async () => {
|
|
281
|
+
const routeClientRoot = await mkRepoTempDir("route-named-client-import-");
|
|
282
|
+
try {
|
|
283
|
+
await mkdir(path.join(routeClientRoot, "app", "login"), { recursive: true });
|
|
284
|
+
await mkdir(path.join(routeClientRoot, "src", "client", "pages", "login"), { recursive: true });
|
|
285
|
+
await writeFile(
|
|
286
|
+
path.join(routeClientRoot, "package.json"),
|
|
287
|
+
JSON.stringify({ name: "mandu-route-named-client-import-test", type: "module" }, null, 2),
|
|
288
|
+
"utf-8",
|
|
289
|
+
);
|
|
290
|
+
await writeFile(
|
|
291
|
+
path.join(routeClientRoot, "app", "login", "page.tsx"),
|
|
292
|
+
'import { LoginPage } from "@/client/pages/login/LoginPage.client";\n' +
|
|
293
|
+
"export default function Page() {\n" +
|
|
294
|
+
" return <LoginPage />;\n" +
|
|
295
|
+
"}\n",
|
|
296
|
+
"utf-8",
|
|
297
|
+
);
|
|
298
|
+
await writeFile(
|
|
299
|
+
path.join(routeClientRoot, "src", "client", "pages", "login", "LoginPage.client.tsx"),
|
|
300
|
+
'"use client";\n' +
|
|
301
|
+
'import { useState } from "react";\n' +
|
|
302
|
+
"export function LoginPage() {\n" +
|
|
303
|
+
' const [email, setEmail] = useState("");\n' +
|
|
304
|
+
' return <form><input value={email} onChange={(event) => setEmail(event.currentTarget.value)} /></form>;\n' +
|
|
305
|
+
"}\n",
|
|
306
|
+
"utf-8",
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const routeClientResult = await runBuildInSubprocess(routeClientRoot, "server-page-route-named-client-import");
|
|
310
|
+
if (!routeClientResult.success) {
|
|
311
|
+
console.error("[named route client] errors:", routeClientResult.errors);
|
|
312
|
+
}
|
|
313
|
+
expect(routeClientResult.success).toBe(true);
|
|
314
|
+
const bundlePath = path.join(routeClientRoot, ".mandu", "client", "login.island.js");
|
|
315
|
+
expect(await Bun.file(bundlePath).exists()).toBe(true);
|
|
316
|
+
|
|
317
|
+
const bundleSource = await readFile(bundlePath, "utf-8");
|
|
318
|
+
expect(bundleSource).toContain("LoginPage");
|
|
319
|
+
expect(bundleSource).not.toContain("Client islands cannot use server-side modules");
|
|
320
|
+
const parseResult = await Bun.build({
|
|
321
|
+
entrypoints: [bundlePath],
|
|
322
|
+
target: "browser",
|
|
323
|
+
external: ["react", "react-dom", "react-dom/client", "react/jsx-dev-runtime"],
|
|
324
|
+
});
|
|
325
|
+
expect(parseResult.success).toBe(true);
|
|
326
|
+
} finally {
|
|
327
|
+
await rm(routeClientRoot, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("uses Windows-safe asset filenames for locale-prefixed route ids", async () => {
|
|
332
|
+
const localeRoot = await mkRepoTempDir("i18n-locale-route-id-");
|
|
333
|
+
try {
|
|
334
|
+
await mkdir(path.join(localeRoot, "app"), { recursive: true });
|
|
335
|
+
await writeFile(
|
|
336
|
+
path.join(localeRoot, "package.json"),
|
|
337
|
+
JSON.stringify({ name: "mandu-i18n-route-id-test", type: "module" }, null, 2),
|
|
338
|
+
"utf-8",
|
|
339
|
+
);
|
|
340
|
+
await writeFile(
|
|
341
|
+
path.join(localeRoot, "app", "demo.client.tsx"),
|
|
342
|
+
"export default function DemoIsland() { return null; }\n",
|
|
343
|
+
"utf-8",
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const localeResult = await runBuildInSubprocess(localeRoot, "i18n-locale-route-id");
|
|
347
|
+
expect(localeResult.success).toBe(true);
|
|
348
|
+
const bundle = localeResult.manifest?.bundles?.["ko::demo"];
|
|
349
|
+
expect(bundle?.js).toBe("/.mandu/client/ko_3a__3a_demo.island.js");
|
|
350
|
+
expect(bundle?.js).not.toContain(":");
|
|
351
|
+
expect(await Bun.file(path.join(localeRoot, ".mandu", "client", "ko_3a__3a_demo.island.js")).exists()).toBe(true);
|
|
352
|
+
} finally {
|
|
353
|
+
await rm(localeRoot, { recursive: true, force: true });
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("fails when hydration is enabled but no clientModule can be resolved", async () => {
|
|
358
|
+
const missingRoot = await mkRepoTempDir("hydration-no-client-");
|
|
271
359
|
try {
|
|
272
360
|
await mkdir(path.join(missingRoot, "app", "login"), { recursive: true });
|
|
273
361
|
await mkdir(path.join(missingRoot, "src", "client", "widgets", "login-form"), { recursive: true });
|