@mandujs/core 0.54.3 → 0.54.5
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 +2 -1
- package/src/agent/__tests__/context.test.ts +237 -0
- package/src/agent/context.ts +535 -0
- package/src/agent/index.ts +6 -0
- package/src/agent/plan.ts +282 -0
- package/src/agent/repair.ts +171 -0
- package/src/agent/sync.ts +200 -0
- package/src/agent/types.ts +308 -0
- package/src/agent/verify.ts +406 -0
- package/src/bundler/__tests__/build-runner.ts +33 -13
- package/src/bundler/__tests__/css.test.ts +20 -0
- package/src/bundler/build.test.ts +40 -5
- package/src/bundler/build.ts +45 -23
- package/src/bundler/css.ts +42 -12
- package/src/index.ts +3 -2
- package/src/router/client-entry.ts +71 -0
- package/src/router/fs-routes.ts +16 -8
- package/src/router/fs-scanner.ts +4 -3
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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, {
|
|
@@ -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
|
+
});
|
|
@@ -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
|
|
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",
|
|
@@ -183,4 +185,37 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
183
185
|
expect(runtimeSource).toContain("document.getElementById(\"__MANDU_DATA__\")");
|
|
184
186
|
expect(runtimeSource).toContain("JSON.parse");
|
|
185
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
|
+
});
|
|
186
221
|
});
|
package/src/bundler/build.ts
CHANGED
|
@@ -20,9 +20,10 @@ import { safeBuild } from "./safe-build";
|
|
|
20
20
|
import { fastRefreshPlugin } from "./fast-refresh-plugin";
|
|
21
21
|
import { defaultBundlerPlugins } from "./plugins";
|
|
22
22
|
import type { BunPlugin } from "bun";
|
|
23
|
-
import { mark, measure } from "../perf";
|
|
24
|
-
import { HMR_PERF } from "../perf/hmr-markers";
|
|
25
|
-
import { runOnBundleComplete } from "../plugins/runner";
|
|
23
|
+
import { mark, measure } from "../perf";
|
|
24
|
+
import { HMR_PERF } from "../perf/hmr-markers";
|
|
25
|
+
import { runOnBundleComplete } from "../plugins/runner";
|
|
26
|
+
import { validateClientModuleForBrowserBundle } from "../router/client-entry";
|
|
26
27
|
import {
|
|
27
28
|
readVendorCache,
|
|
28
29
|
writeVendorCache,
|
|
@@ -2122,7 +2123,21 @@ export async function buildClientBundles(
|
|
|
2122
2123
|
const env = resolveBundlerMode(options);
|
|
2123
2124
|
|
|
2124
2125
|
// 1. Hydration이 필요한 라우트 필터링
|
|
2125
|
-
const
|
|
2126
|
+
const invalidClientRouteIds = new Set<string>();
|
|
2127
|
+
let hydratedRoutes = getHydratedRoutes(manifest);
|
|
2128
|
+
if (hydratedRoutes.length > 0) {
|
|
2129
|
+
const validRoutes: RouteSpec[] = [];
|
|
2130
|
+
for (const route of hydratedRoutes) {
|
|
2131
|
+
const validationError = await validateClientModuleForBrowserBundle(route, rootDir);
|
|
2132
|
+
if (validationError) {
|
|
2133
|
+
invalidClientRouteIds.add(route.id);
|
|
2134
|
+
errors.push(validationError);
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2137
|
+
validRoutes.push(route);
|
|
2138
|
+
}
|
|
2139
|
+
hydratedRoutes = validRoutes;
|
|
2140
|
+
}
|
|
2126
2141
|
const runtimeRoutes = manifest.routes.filter((route) => route.kind === "page" && needsHydration(route));
|
|
2127
2142
|
const partialFiles = runtimeRoutes.length > 0 ? await scanPartialFiles(rootDir) : [];
|
|
2128
2143
|
|
|
@@ -2134,7 +2149,7 @@ export async function buildClientBundles(
|
|
|
2134
2149
|
// (이전 빌드의 stale 매니페스트 참조 방지)
|
|
2135
2150
|
if (hydratedRoutes.length === 0 && partialFiles.length === 0) {
|
|
2136
2151
|
// #185: skipFrameworkBundles 모드에서는 기존 manifest를 그대로 유지 (devtools 재빌드도 스킵)
|
|
2137
|
-
if (options.skipFrameworkBundles) {
|
|
2152
|
+
if (options.skipFrameworkBundles && errors.length === 0) {
|
|
2138
2153
|
const manifestPath = path.join(rootDir, ".mandu/manifest.json");
|
|
2139
2154
|
try {
|
|
2140
2155
|
const manifestRaw = await fs.readFile(manifestPath, "utf-8");
|
|
@@ -2185,11 +2200,11 @@ export async function buildClientBundles(
|
|
|
2185
2200
|
path.join(rootDir, ".mandu/manifest.json"),
|
|
2186
2201
|
JSON.stringify(emptyManifest, null, 2)
|
|
2187
2202
|
);
|
|
2188
|
-
return {
|
|
2189
|
-
success:
|
|
2190
|
-
outputs: [],
|
|
2191
|
-
errors
|
|
2192
|
-
manifest: emptyManifest,
|
|
2203
|
+
return {
|
|
2204
|
+
success: errors.length === 0,
|
|
2205
|
+
outputs: [],
|
|
2206
|
+
errors,
|
|
2207
|
+
manifest: emptyManifest,
|
|
2193
2208
|
stats: {
|
|
2194
2209
|
totalSize: 0,
|
|
2195
2210
|
totalGzipSize: 0,
|
|
@@ -2228,11 +2243,14 @@ export async function buildClientBundles(
|
|
|
2228
2243
|
return buildClientBundles(manifest, rootDir, { ...options, targetRouteIds: undefined });
|
|
2229
2244
|
}
|
|
2230
2245
|
|
|
2231
|
-
// Only update manifest with successfully built outputs (#10: preserve previous good manifest on failure)
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2246
|
+
// Only update manifest with successfully built outputs (#10: preserve previous good manifest on failure)
|
|
2247
|
+
for (const routeId of invalidClientRouteIds) {
|
|
2248
|
+
delete existingManifest.bundles[routeId];
|
|
2249
|
+
}
|
|
2250
|
+
if (outputs.length > 0 || invalidClientRouteIds.size > 0) {
|
|
2251
|
+
for (const output of outputs) {
|
|
2252
|
+
if (existingManifest.bundles[output.routeId]) {
|
|
2253
|
+
existingManifest.bundles[output.routeId].js = output.outputPath;
|
|
2236
2254
|
} else {
|
|
2237
2255
|
const route = targetRoutes.find((r) => r.id === output.routeId);
|
|
2238
2256
|
const hydration = route ? getRouteHydration(route) : null;
|
|
@@ -2244,11 +2262,11 @@ export async function buildClientBundles(
|
|
|
2244
2262
|
}
|
|
2245
2263
|
}
|
|
2246
2264
|
|
|
2247
|
-
await fs.writeFile(
|
|
2248
|
-
path.join(rootDir, ".mandu/manifest.json"),
|
|
2249
|
-
JSON.stringify(existingManifest, null, 2)
|
|
2250
|
-
);
|
|
2251
|
-
}
|
|
2265
|
+
await fs.writeFile(
|
|
2266
|
+
path.join(rootDir, ".mandu/manifest.json"),
|
|
2267
|
+
JSON.stringify(existingManifest, null, 2)
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2252
2270
|
// When all builds failed, do NOT overwrite manifest — keep previous good state
|
|
2253
2271
|
|
|
2254
2272
|
const stats = calculateStats(outputs, startTime);
|
|
@@ -2284,9 +2302,13 @@ export async function buildClientBundles(
|
|
|
2284
2302
|
"[Mandu] Existing manifest missing required fields (shared/bundles), falling back to full build",
|
|
2285
2303
|
);
|
|
2286
2304
|
return buildClientBundles(manifest, rootDir, { ...options, skipFrameworkBundles: false });
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
for (const routeId of invalidClientRouteIds) {
|
|
2308
|
+
delete existingManifest.bundles[routeId];
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// Pre-build validation + 병렬 island 빌드 (framework 번들은 스킵)
|
|
2290
2312
|
for (const route of hydratedRoutes) {
|
|
2291
2313
|
if (!route.clientModule) continue;
|
|
2292
2314
|
const clientModulePath = path.join(rootDir, route.clientModule);
|
package/src/bundler/css.ts
CHANGED
|
@@ -18,14 +18,38 @@ import fs from "fs/promises";
|
|
|
18
18
|
import { watch as fsWatch, type FSWatcher } from "fs";
|
|
19
19
|
import { withPerf } from "../perf";
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* Tailwind CLI 실행 명령어를 결정한다.
|
|
23
|
-
* Windows에서 Bun.spawn은 PATH 기반 명령어 해석이 불안정하므로 (#152)
|
|
24
|
-
* process.execPath (절대 경로)를
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Tailwind CLI 실행 명령어를 결정한다.
|
|
23
|
+
* Windows에서 Bun.spawn은 PATH 기반 명령어 해석이 불안정하므로 (#152)
|
|
24
|
+
* `bun run mandu` 환경에서는 process.execPath (bun 절대 경로)를 사용한다.
|
|
25
|
+
* Standalone Mandu binary에서는 process.execPath가 mandu.exe를 가리키므로
|
|
26
|
+
* `mandu x @tailwindcss/cli`로 오해석된다. 이 경우 PATH에서 Bun을 찾는다.
|
|
27
|
+
*/
|
|
28
|
+
function isBunExecutable(executablePath: string | undefined): boolean {
|
|
29
|
+
if (!executablePath) return false;
|
|
30
|
+
const base = path.basename(executablePath).toLowerCase();
|
|
31
|
+
return base === "bun" || base === "bun.exe";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type WhichExecutable = (command: string) => string | null | undefined;
|
|
35
|
+
|
|
36
|
+
function resolveBunExecutable(
|
|
37
|
+
execPath = process.execPath,
|
|
38
|
+
which: WhichExecutable = (command) => Bun.which(command),
|
|
39
|
+
): string {
|
|
40
|
+
if (isBunExecutable(execPath)) return execPath;
|
|
41
|
+
const fromPath = which("bun");
|
|
42
|
+
if (fromPath) return fromPath;
|
|
43
|
+
return process.platform === "win32" ? "bun.exe" : "bun";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getTailwindCommand(
|
|
47
|
+
args: string[],
|
|
48
|
+
execPath = process.execPath,
|
|
49
|
+
which?: WhichExecutable,
|
|
50
|
+
): string[] {
|
|
51
|
+
return [resolveBunExecutable(execPath, which), "x", ...args];
|
|
52
|
+
}
|
|
29
53
|
|
|
30
54
|
// ========== Types ==========
|
|
31
55
|
|
|
@@ -320,7 +344,13 @@ export function getCSSServerPath(): string {
|
|
|
320
344
|
/**
|
|
321
345
|
* CSS 링크 태그 생성
|
|
322
346
|
*/
|
|
323
|
-
export function generateCSSLinkTag(isDev: boolean = false): string {
|
|
324
|
-
const cacheBust = isDev ? `?t=${Date.now()}` : "";
|
|
325
|
-
return `<link rel="stylesheet" href="${SERVER_CSS_PATH}${cacheBust}">`;
|
|
326
|
-
}
|
|
347
|
+
export function generateCSSLinkTag(isDev: boolean = false): string {
|
|
348
|
+
const cacheBust = isDev ? `?t=${Date.now()}` : "";
|
|
349
|
+
return `<link rel="stylesheet" href="${SERVER_CSS_PATH}${cacheBust}">`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export const __private = {
|
|
353
|
+
getTailwindCommand,
|
|
354
|
+
isBunExecutable,
|
|
355
|
+
resolveBunExecutable,
|
|
356
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -21,8 +21,9 @@ export const __MANDU_CORE_VERSION__: string = (() => {
|
|
|
21
21
|
}
|
|
22
22
|
})();
|
|
23
23
|
|
|
24
|
-
export * from "./spec";
|
|
25
|
-
export * from "./
|
|
24
|
+
export * from "./spec";
|
|
25
|
+
export * from "./agent";
|
|
26
|
+
export * from "./runtime";
|
|
26
27
|
export * from "./generator";
|
|
27
28
|
export * from "./guard";
|
|
28
29
|
export * from "./report";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { RouteSpec } from "../spec/schema";
|
|
4
|
+
|
|
5
|
+
export function normalizeRouteModulePath(value: string | undefined): string {
|
|
6
|
+
return (value ?? "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function hasUseClientDirective(source: string): boolean {
|
|
10
|
+
return /^(?:\uFEFF)?\s*(?:(?:\/\/[^\r\n]*(?:\r?\n|$))|(?:\/\*[\s\S]*?\*\/\s*))*["']use client["']\s*;?/.test(source);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function hasUseServerDirective(source: string): boolean {
|
|
14
|
+
return /^(?:\uFEFF)?\s*(?:(?:\/\/[^\r\n]*(?:\r?\n|$))|(?:\/\*[\s\S]*?\*\/\s*))*["']use server["']\s*;?/.test(source);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function clientModuleIsRouteComponent(route: RouteSpec, clientModule = route.clientModule): boolean {
|
|
18
|
+
if (route.kind !== "page" || !clientModule) return false;
|
|
19
|
+
|
|
20
|
+
const client = normalizeRouteModulePath(clientModule);
|
|
21
|
+
return (
|
|
22
|
+
client === normalizeRouteModulePath(route.componentModule) ||
|
|
23
|
+
client === normalizeRouteModulePath(route.module)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readRouteModule(rootDir: string, modulePath: string): Promise<string | null> {
|
|
28
|
+
try {
|
|
29
|
+
return await readFile(path.resolve(rootDir, modulePath), "utf-8");
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function shouldPreserveExistingClientModule(
|
|
36
|
+
route: RouteSpec,
|
|
37
|
+
clientModule: string,
|
|
38
|
+
rootDir: string,
|
|
39
|
+
): Promise<boolean> {
|
|
40
|
+
const source = await readRouteModule(rootDir, clientModule);
|
|
41
|
+
if (source === null) return false;
|
|
42
|
+
if (hasUseServerDirective(source)) return false;
|
|
43
|
+
if (clientModuleIsRouteComponent(route, clientModule)) {
|
|
44
|
+
return hasUseClientDirective(source);
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function validateClientModuleForBrowserBundle(
|
|
50
|
+
route: RouteSpec,
|
|
51
|
+
rootDir: string,
|
|
52
|
+
): Promise<string | null> {
|
|
53
|
+
if (!route.clientModule) return null;
|
|
54
|
+
|
|
55
|
+
const source = await readRouteModule(rootDir, route.clientModule);
|
|
56
|
+
if (source === null) return null;
|
|
57
|
+
|
|
58
|
+
if (hasUseServerDirective(source)) {
|
|
59
|
+
return `[${route.id}] Client module "${route.clientModule}" has a "use server" directive and cannot be bundled for the browser.`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (clientModuleIsRouteComponent(route) && !hasUseClientDirective(source)) {
|
|
63
|
+
return (
|
|
64
|
+
`[${route.id}] Route component "${route.clientModule}" is configured as clientModule, ` +
|
|
65
|
+
`but it is a server page (missing "use client"). Mandu will not bundle server pages into client islands. ` +
|
|
66
|
+
`Remove the stale clientModule from .mandu/routes.manifest.json or use a *.partial.tsx / spec/slots/${route.id}.client.tsx client entry.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
package/src/router/fs-routes.ts
CHANGED
|
@@ -14,10 +14,11 @@ import { DEFAULT_SCANNER_CONFIG } from "./fs-types";
|
|
|
14
14
|
import { scanRoutes } from "./fs-scanner";
|
|
15
15
|
import { loadManduConfig } from "../config";
|
|
16
16
|
import type { ManduPlugin, ManduHooks } from "../plugins/hooks";
|
|
17
|
-
import {
|
|
18
|
-
runOnRouteRegistered,
|
|
19
|
-
runOnManifestBuilt,
|
|
20
|
-
} from "../plugins/runner";
|
|
17
|
+
import {
|
|
18
|
+
runOnRouteRegistered,
|
|
19
|
+
runOnManifestBuilt,
|
|
20
|
+
} from "../plugins/runner";
|
|
21
|
+
import { shouldPreserveExistingClientModule } from "./client-entry";
|
|
21
22
|
|
|
22
23
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
24
|
// Types
|
|
@@ -308,10 +309,17 @@ export async function generateManifest(
|
|
|
308
309
|
for (const route of manifest.routes) {
|
|
309
310
|
const prev = existingMap.get(route.id);
|
|
310
311
|
if (!prev) continue;
|
|
311
|
-
// 사용자가 설정한 clientModule/hydration
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
312
|
+
// 사용자가 설정한 clientModule/hydration 보존.
|
|
313
|
+
// If app/page.tsx used to be a client page and later becomes a server
|
|
314
|
+
// page, preserving the old clientModule would leak the server graph into
|
|
315
|
+
// the browser bundle.
|
|
316
|
+
if (
|
|
317
|
+
prev.clientModule &&
|
|
318
|
+
!route.clientModule &&
|
|
319
|
+
await shouldPreserveExistingClientModule(route, prev.clientModule, rootDir)
|
|
320
|
+
) {
|
|
321
|
+
route.clientModule = prev.clientModule;
|
|
322
|
+
}
|
|
315
323
|
if (prev.hydration && !route.hydration) {
|
|
316
324
|
route.hydration = prev.hydration;
|
|
317
325
|
}
|
package/src/router/fs-scanner.ts
CHANGED
|
@@ -28,8 +28,9 @@ import {
|
|
|
28
28
|
sortRoutesByPriority,
|
|
29
29
|
getPatternShape,
|
|
30
30
|
} from "./fs-patterns";
|
|
31
|
-
import { mark, measure } from "../perf";
|
|
32
|
-
import { METADATA_ROUTES } from "../routes/types";
|
|
31
|
+
import { mark, measure } from "../perf";
|
|
32
|
+
import { METADATA_ROUTES } from "../routes/types";
|
|
33
|
+
import { hasUseClientDirective } from "./client-entry";
|
|
33
34
|
|
|
34
35
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
35
36
|
// Scanner Class
|
|
@@ -390,7 +391,7 @@ export class FSScanner {
|
|
|
390
391
|
}
|
|
391
392
|
} else if (file.type === "page" && pageFileContent) {
|
|
392
393
|
// page 파일 자체에서 "use client" 확인
|
|
393
|
-
const hasUseClient =
|
|
394
|
+
const hasUseClient = hasUseClientDirective(pageFileContent);
|
|
394
395
|
if (hasUseClient) {
|
|
395
396
|
clientModule = modulePath;
|
|
396
397
|
}
|