@mandujs/core 0.54.2 → 0.54.4

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.
@@ -54,16 +54,36 @@
54
54
  import { buildClientBundles } from "../build";
55
55
  import type { RoutesManifest } from "../../spec/schema";
56
56
 
57
- const rootDir = process.argv[2];
58
- if (!rootDir) {
59
- console.error("usage: build-runner.ts <rootDir>");
60
- process.exit(2);
61
- }
62
-
63
- const manifest: RoutesManifest = {
64
- version: 1,
65
- routes: [
66
- {
57
+ const rootDir = process.argv[2];
58
+ if (!rootDir) {
59
+ console.error("usage: build-runner.ts <rootDir>");
60
+ process.exit(2);
61
+ }
62
+ const mode = process.argv[3] ?? "default";
63
+
64
+ const manifest: RoutesManifest = mode === "server-page-client-module"
65
+ ? {
66
+ version: 1,
67
+ routes: [
68
+ {
69
+ id: "index",
70
+ kind: "page",
71
+ pattern: "/",
72
+ module: "app/page.tsx",
73
+ componentModule: "app/page.tsx",
74
+ clientModule: "app/page.tsx",
75
+ hydration: {
76
+ strategy: "island",
77
+ priority: "visible",
78
+ preload: false,
79
+ },
80
+ },
81
+ ],
82
+ }
83
+ : {
84
+ version: 1,
85
+ routes: [
86
+ {
67
87
  id: "demo",
68
88
  kind: "page",
69
89
  pattern: "/",
@@ -75,9 +95,9 @@ const manifest: RoutesManifest = {
75
95
  priority: "visible",
76
96
  preload: false,
77
97
  },
78
- },
79
- ],
80
- };
98
+ },
99
+ ],
100
+ };
81
101
 
82
102
  try {
83
103
  const result = await buildClientBundles(manifest, rootDir, {
@@ -29,10 +29,11 @@ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
29
29
  import { tmpdir } from "os";
30
30
  import path from "path";
31
31
  import type { RouteSpec } from "../../spec/schema";
32
- import {
33
- _testOnly_scanIslandFiles,
34
- _testOnly_getHydratedRoutes,
35
- } from "../build";
32
+ import {
33
+ _testOnly_scanIslandFiles,
34
+ _testOnly_scanPartialFiles,
35
+ _testOnly_getHydratedRoutes,
36
+ } from "../build";
36
37
  import { HMR_PERF } from "../../perf/hmr-markers";
37
38
  import {
38
39
  _resetCacheForTesting as _resetPerfCache,
@@ -372,7 +373,7 @@ describe("Phase 7.1 R1 Agent C — getHydratedRoutes filter", () => {
372
373
  });
373
374
  });
374
375
 
375
- describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", () => {
376
+ describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", () => {
376
377
  let project: ReturnType<typeof createProject>;
377
378
 
378
379
  beforeEach(() => {
@@ -500,5 +501,32 @@ describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", (
500
501
  ];
501
502
  const result = await _testOnly_scanIslandFiles(routes, project.rootDir);
502
503
  expect(result).toEqual([]);
503
- });
504
- });
504
+ });
505
+ });
506
+
507
+ describe("partial bundle scan", () => {
508
+ let project: ReturnType<typeof createProject>;
509
+
510
+ beforeEach(() => {
511
+ project = createProject();
512
+ });
513
+
514
+ afterEach(() => {
515
+ try {
516
+ rmSync(project.rootDir, { recursive: true, force: true });
517
+ } catch {
518
+ /* Windows lock tolerance */
519
+ }
520
+ });
521
+
522
+ it("discovers *.partial.tsx files outside .mandu and node_modules", async () => {
523
+ project.writeIslandFile("app", "Home.partial.tsx");
524
+ project.writeIslandFile(".mandu/client", "Stale.partial.tsx");
525
+ project.writeIslandFile("node_modules/pkg", "Ignored.partial.tsx");
526
+
527
+ const result = await _testOnly_scanPartialFiles(project.rootDir);
528
+
529
+ expect(result.map((entry) => entry.name)).toEqual(["Home"]);
530
+ expect(result[0].priority).toBe("visible");
531
+ });
532
+ });
@@ -0,0 +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
+ });
@@ -341,18 +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
- }
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
+ }
356
364
 
357
365
  const islands: AnalyzeIsland[] = [];
358
366
  for (const src of islandSources) {
@@ -24,17 +24,19 @@ async function importBuiltModule(relativePath: string): Promise<Record<string, u
24
24
  * See `__tests__/build-runner.ts` for the subprocess entrypoint and more
25
25
  * background.
26
26
  */
27
- async function runBuildInSubprocess(root: string): Promise<{
28
- success: boolean;
29
- errors: string[];
30
- }> {
27
+ async function runBuildInSubprocess(root: string, mode?: string): Promise<{
28
+ success: boolean;
29
+ errors: string[];
30
+ }> {
31
31
  const runner = path.join(
32
32
  import.meta.dir,
33
33
  "__tests__",
34
34
  "build-runner.ts",
35
35
  );
36
36
  try {
37
- const proc = Bun.spawn([process.execPath, "run", runner, root], {
37
+ const args = [process.execPath, "run", runner, root];
38
+ if (mode) args.push(mode);
39
+ const proc = Bun.spawn(args, {
38
40
  cwd: path.resolve(import.meta.dir, "..", ".."),
39
41
  stdin: "ignore",
40
42
  stdout: "pipe",
@@ -171,9 +173,49 @@ describe("buildClientBundles vendor shims", () => {
171
173
  expect(runtimeSource).toContain("function hasHydratableMarkup");
172
174
  expect(runtimeSource).toContain("function shouldHydrateCompiledIsland");
173
175
  expect(runtimeSource).toContain("onRecoverableError");
174
- expect(runtimeSource).toContain("data-mandu-hydrating");
175
- expect(runtimeSource).toContain("data-mandu-render-mode");
176
- expect(runtimeSource).toContain("data-mandu-recoverable-error");
177
- expect(runtimeSource).toContain("pointerdown");
178
- });
179
- });
176
+ expect(runtimeSource).toContain("data-mandu-hydrating");
177
+ expect(runtimeSource).toContain("data-mandu-render-mode");
178
+ expect(runtimeSource).toContain("data-mandu-recoverable-error");
179
+ expect(runtimeSource).toContain("pointerdown");
180
+ });
181
+
182
+ test("runtime parses SSR data script before island setup", async () => {
183
+ const runtimeSource = await readFile(path.join(rootDir, ".mandu", "client", "_runtime.js"), "utf-8");
184
+ expect(runtimeSource).toContain("function readManduData");
185
+ expect(runtimeSource).toContain("document.getElementById(\"__MANDU_DATA__\")");
186
+ expect(runtimeSource).toContain("JSON.parse");
187
+ });
188
+
189
+ test("does not bundle a server page when stale manifest marks page.tsx as clientModule", async () => {
190
+ const staleRoot = await mkdtemp(path.join(import.meta.dir, ".tmp-stale-client-module-"));
191
+ try {
192
+ await mkdir(path.join(staleRoot, "app"), { recursive: true });
193
+ await mkdir(path.join(staleRoot, "src", "shared", "contracts"), { recursive: true });
194
+ await writeFile(
195
+ path.join(staleRoot, "package.json"),
196
+ JSON.stringify({ name: "mandu-stale-client-module-test", type: "module" }, null, 2),
197
+ "utf-8",
198
+ );
199
+ await writeFile(
200
+ path.join(staleRoot, "src", "shared", "contracts", "api.ts"),
201
+ 'export const INTERNAL_BASE = process.env.MANDU_INTERNAL_URL ?? "http://localhost:3333";\n',
202
+ "utf-8",
203
+ );
204
+ await writeFile(
205
+ path.join(staleRoot, "app", "page.tsx"),
206
+ 'import { INTERNAL_BASE } from "../src/shared/contracts/api";\n' +
207
+ "export default async function HomePage() {\n" +
208
+ " return <main>{INTERNAL_BASE}</main>;\n" +
209
+ "}\n",
210
+ "utf-8",
211
+ );
212
+
213
+ const staleResult = await runBuildInSubprocess(staleRoot, "server-page-client-module");
214
+ expect(staleResult.success).toBe(false);
215
+ expect(staleResult.errors.join("\n")).toContain("missing \"use client\"");
216
+ expect(await Bun.file(path.join(staleRoot, ".mandu", "client", "index.island.js")).exists()).toBe(false);
217
+ } finally {
218
+ await rm(staleRoot, { recursive: true, force: true });
219
+ }
220
+ });
221
+ });