@mandujs/core 0.54.10 → 0.54.12
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 +1 -1
- package/scripts/postinstall-lock.ts +153 -153
- package/src/a11y/run-audit.ts +15 -15
- package/src/agent/__tests__/context.test.ts +17 -65
- 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 +10 -18
- package/src/agent/verify.ts +17 -115
- package/src/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__tests__/build-runner.ts +81 -62
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +20 -20
- package/src/bundler/analyzer.ts +15 -15
- package/src/bundler/build.test.ts +130 -83
- package/src/bundler/build.ts +570 -524
- 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/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 +22 -31
- package/src/filling/context.ts +17 -17
- package/src/generator/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.test.ts +66 -65
- 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 +3 -3
- 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 +192 -140
- package/src/router/client-entry.ts +516 -486
- package/src/router/fs-routes.ts +16 -16
- package/src/router/fs-scanner.ts +16 -28
- 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 +103 -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/observability-lifecycle.ts +290 -290
- package/src/runtime/page-render-response.ts +106 -106
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/server.ts +243 -243
- 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/watcher/__tests__/watcher.test.ts +59 -59
- package/src/watcher/watcher.ts +61 -61
package/src/bundler/build.ts
CHANGED
|
@@ -10,23 +10,23 @@ import type {
|
|
|
10
10
|
BundleResult,
|
|
11
11
|
BundleOutput,
|
|
12
12
|
BundleManifest,
|
|
13
|
-
BundleStats,
|
|
14
|
-
BundlerOptions,
|
|
15
|
-
IslandFileEntry,
|
|
16
|
-
PartialFileEntry,
|
|
17
|
-
} from "./types";
|
|
13
|
+
BundleStats,
|
|
14
|
+
BundlerOptions,
|
|
15
|
+
IslandFileEntry,
|
|
16
|
+
PartialFileEntry,
|
|
17
|
+
} from "./types";
|
|
18
18
|
import { HYDRATION } from "../constants";
|
|
19
19
|
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";
|
|
26
|
-
import {
|
|
27
|
-
describeMissingHydrationClientModule,
|
|
28
|
-
validateClientModuleForBrowserBundle,
|
|
29
|
-
} from "../router/client-entry";
|
|
23
|
+
import { mark, measure } from "../perf";
|
|
24
|
+
import { HMR_PERF } from "../perf/hmr-markers";
|
|
25
|
+
import { runOnBundleComplete } from "../plugins/runner";
|
|
26
|
+
import {
|
|
27
|
+
describeMissingHydrationClientModule,
|
|
28
|
+
validateClientModuleForBrowserBundle,
|
|
29
|
+
} from "../router/client-entry";
|
|
30
30
|
import {
|
|
31
31
|
readVendorCache,
|
|
32
32
|
writeVendorCache,
|
|
@@ -100,13 +100,16 @@ function shouldSplitChunks(options: BundlerOptions): boolean {
|
|
|
100
100
|
return options.splitting ?? (resolveBundlerMode(options) === "production");
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function nodeEnvDefine(options: BundlerOptions): string {
|
|
104
|
-
return JSON.stringify(resolveBundlerMode(options));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
function nodeEnvDefine(options: BundlerOptions): string {
|
|
104
|
+
return JSON.stringify(resolveBundlerMode(options));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const FAST_REFRESH_SELF_ALIAS_PATTERN =
|
|
108
|
+
/^var\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*\1;\s*\r?\n(?=\$RefreshReg\$\(\1,\s*["'][^"']*:default["']\);)/gm;
|
|
109
|
+
|
|
110
|
+
function resolveClientOutDir(rootDir: string, outDir?: string): string {
|
|
111
|
+
const defaultOutDir = path.join(rootDir, ".mandu/client");
|
|
112
|
+
if (!outDir) return defaultOutDir;
|
|
110
113
|
|
|
111
114
|
const resolvedOutDir = path.isAbsolute(outDir) ? outDir : path.resolve(rootDir, outDir);
|
|
112
115
|
if (path.normalize(resolvedOutDir) === path.normalize(path.join(rootDir, ".mandu"))) {
|
|
@@ -191,46 +194,46 @@ async function scanIslandFiles(routes: RouteSpec[], rootDir: string): Promise<Is
|
|
|
191
194
|
*
|
|
192
195
|
* @internal
|
|
193
196
|
*/
|
|
194
|
-
export const _testOnly_scanIslandFiles = scanIslandFiles;
|
|
195
|
-
|
|
196
|
-
function normalizeClientEntryName(name: string): string {
|
|
197
|
-
return name
|
|
198
|
-
.trim()
|
|
199
|
-
.replace(/[^A-Za-z0-9_-]/g, "-")
|
|
200
|
-
.replace(/^-+|-+$/g, "") || "partial";
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function scanPartialFiles(rootDir: string): Promise<PartialFileEntry[]> {
|
|
204
|
-
const entries: PartialFileEntry[] = [];
|
|
205
|
-
const seen = new Set<string>();
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
const glob = new Bun.Glob("**/*.partial.{ts,tsx}");
|
|
209
|
-
for await (const rel of glob.scan({ cwd: rootDir, absolute: true })) {
|
|
210
|
-
if (rel.includes(`${path.sep}node_modules${path.sep}`)) continue;
|
|
211
|
-
if (rel.includes(`${path.sep}.mandu${path.sep}`)) continue;
|
|
212
|
-
|
|
213
|
-
const filePath = path.resolve(rel);
|
|
214
|
-
if (seen.has(filePath)) continue;
|
|
215
|
-
seen.add(filePath);
|
|
216
|
-
|
|
217
|
-
const base = path.basename(filePath).replace(/\.partial\.tsx?$/, "");
|
|
218
|
-
entries.push({
|
|
219
|
-
name: normalizeClientEntryName(base),
|
|
220
|
-
filePath,
|
|
221
|
-
priority: HYDRATION.DEFAULT_PRIORITY,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
} catch {
|
|
225
|
-
// Bun.Glob unavailable or scan failed — a missing partial bundle should
|
|
226
|
-
// not prevent route-level islands from building.
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return entries.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** @internal test helper */
|
|
233
|
-
export const _testOnly_scanPartialFiles = scanPartialFiles;
|
|
197
|
+
export const _testOnly_scanIslandFiles = scanIslandFiles;
|
|
198
|
+
|
|
199
|
+
function normalizeClientEntryName(name: string): string {
|
|
200
|
+
return name
|
|
201
|
+
.trim()
|
|
202
|
+
.replace(/[^A-Za-z0-9_-]/g, "-")
|
|
203
|
+
.replace(/^-+|-+$/g, "") || "partial";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function scanPartialFiles(rootDir: string): Promise<PartialFileEntry[]> {
|
|
207
|
+
const entries: PartialFileEntry[] = [];
|
|
208
|
+
const seen = new Set<string>();
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const glob = new Bun.Glob("**/*.partial.{ts,tsx}");
|
|
212
|
+
for await (const rel of glob.scan({ cwd: rootDir, absolute: true })) {
|
|
213
|
+
if (rel.includes(`${path.sep}node_modules${path.sep}`)) continue;
|
|
214
|
+
if (rel.includes(`${path.sep}.mandu${path.sep}`)) continue;
|
|
215
|
+
|
|
216
|
+
const filePath = path.resolve(rel);
|
|
217
|
+
if (seen.has(filePath)) continue;
|
|
218
|
+
seen.add(filePath);
|
|
219
|
+
|
|
220
|
+
const base = path.basename(filePath).replace(/\.partial\.tsx?$/, "");
|
|
221
|
+
entries.push({
|
|
222
|
+
name: normalizeClientEntryName(base),
|
|
223
|
+
filePath,
|
|
224
|
+
priority: HYDRATION.DEFAULT_PRIORITY,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// Bun.Glob unavailable or scan failed — a missing partial bundle should
|
|
229
|
+
// not prevent route-level islands from building.
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return entries.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @internal test helper */
|
|
236
|
+
export const _testOnly_scanPartialFiles = scanPartialFiles;
|
|
234
237
|
|
|
235
238
|
/**
|
|
236
239
|
* Test-only accessor for the hydrated-routes filter. Mirrors the rationale
|
|
@@ -239,9 +242,9 @@ export const _testOnly_scanPartialFiles = scanPartialFiles;
|
|
|
239
242
|
*
|
|
240
243
|
* @internal
|
|
241
244
|
*/
|
|
242
|
-
export const _testOnly_getHydratedRoutes = getHydratedRoutes;
|
|
243
|
-
export const _testOnly_getHydrationRoutesMissingClientModule =
|
|
244
|
-
getHydrationRoutesMissingClientModule;
|
|
245
|
+
export const _testOnly_getHydratedRoutes = getHydratedRoutes;
|
|
246
|
+
export const _testOnly_getHydrationRoutesMissingClientModule =
|
|
247
|
+
getHydrationRoutesMissingClientModule;
|
|
245
248
|
|
|
246
249
|
/**
|
|
247
250
|
* Issue #240 Phase 2 — collect every file the React Compiler plugin
|
|
@@ -260,37 +263,37 @@ export const _testOnly_getHydrationRoutesMissingClientModule =
|
|
|
260
263
|
* Paths are absolute and deduplicated. Returns `[]` when the manifest
|
|
261
264
|
* has no hydrated routes.
|
|
262
265
|
*/
|
|
263
|
-
export async function collectCompilerLintTargets(
|
|
264
|
-
manifest: RoutesManifest,
|
|
265
|
-
rootDir: string,
|
|
266
|
-
): Promise<string[]> {
|
|
267
|
-
const hydratedRoutes = getHydratedRoutes(manifest);
|
|
268
|
-
|
|
269
|
-
const out = new Set<string>();
|
|
270
|
-
|
|
271
|
-
// 1) Islands (reuses the bundler's canonical scanner).
|
|
272
|
-
if (hydratedRoutes.length > 0) {
|
|
273
|
-
const islandFiles = await scanIslandFiles(hydratedRoutes, rootDir);
|
|
274
|
-
for (const entry of islandFiles) {
|
|
275
|
-
out.add(path.resolve(entry.filePath));
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// 2) `"use client"` page modules — the hydrated-route filter
|
|
279
|
-
// already asserted `clientModule` exists.
|
|
280
|
-
for (const route of hydratedRoutes) {
|
|
281
|
-
const rel = route.clientModule ?? route.module;
|
|
282
|
-
if (rel) out.add(path.resolve(rootDir, rel));
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// 3) Partials — glob the whole project once. Partials live anywhere
|
|
287
|
-
// the user chooses, they're not sibling-scoped like islands.
|
|
288
|
-
for (const entry of await scanPartialFiles(rootDir)) {
|
|
289
|
-
out.add(path.resolve(entry.filePath));
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return Array.from(out).sort();
|
|
293
|
-
}
|
|
266
|
+
export async function collectCompilerLintTargets(
|
|
267
|
+
manifest: RoutesManifest,
|
|
268
|
+
rootDir: string,
|
|
269
|
+
): Promise<string[]> {
|
|
270
|
+
const hydratedRoutes = getHydratedRoutes(manifest);
|
|
271
|
+
|
|
272
|
+
const out = new Set<string>();
|
|
273
|
+
|
|
274
|
+
// 1) Islands (reuses the bundler's canonical scanner).
|
|
275
|
+
if (hydratedRoutes.length > 0) {
|
|
276
|
+
const islandFiles = await scanIslandFiles(hydratedRoutes, rootDir);
|
|
277
|
+
for (const entry of islandFiles) {
|
|
278
|
+
out.add(path.resolve(entry.filePath));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 2) `"use client"` page modules — the hydrated-route filter
|
|
282
|
+
// already asserted `clientModule` exists.
|
|
283
|
+
for (const route of hydratedRoutes) {
|
|
284
|
+
const rel = route.clientModule ?? route.module;
|
|
285
|
+
if (rel) out.add(path.resolve(rootDir, rel));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 3) Partials — glob the whole project once. Partials live anywhere
|
|
290
|
+
// the user chooses, they're not sibling-scoped like islands.
|
|
291
|
+
for (const entry of await scanPartialFiles(rootDir)) {
|
|
292
|
+
out.add(path.resolve(entry.filePath));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return Array.from(out).sort();
|
|
296
|
+
}
|
|
294
297
|
|
|
295
298
|
/** Build a single per-island bundle. */
|
|
296
299
|
async function buildPerIslandBundle(
|
|
@@ -317,73 +320,74 @@ async function buildPerIslandBundle(
|
|
|
317
320
|
define: { "process.env.NODE_ENV": nodeEnvDefine(options), ...options.define },
|
|
318
321
|
});
|
|
319
322
|
await fs.unlink(entryPath).catch(() => {});
|
|
320
|
-
if (!result.success) {
|
|
321
|
-
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
322
|
-
throw new Error(`Island build failed for '${entry.name}' (source: ${entry.filePath}):\n${grouped}\n Hint: Check the import paths and TypeScript types in this island file.`);
|
|
323
|
-
}
|
|
324
|
-
return { name: entry.name, js: `/.mandu/client/${outputName}`, route: entry.routeId, priority: entry.priority };
|
|
325
|
-
} catch (error) {
|
|
326
|
-
await fs.unlink(entryPath).catch(() => {});
|
|
327
|
-
throw error;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
interface PartialBundleBuild {
|
|
332
|
-
name: string;
|
|
333
|
-
js: string;
|
|
334
|
-
priority: PartialFileEntry["priority"];
|
|
335
|
-
size: number;
|
|
336
|
-
gzipSize: number;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/** Build a single inline partial bundle. */
|
|
340
|
-
async function buildPartialBundle(
|
|
341
|
-
entry: PartialFileEntry,
|
|
342
|
-
outDir: string,
|
|
343
|
-
options: BundlerOptions,
|
|
344
|
-
): Promise<PartialBundleBuild> {
|
|
345
|
-
const entryPath = path.join(outDir, `_entry_partial_${entry.name}.js`);
|
|
346
|
-
const outputName = `${entry.name}.partial.js`;
|
|
347
|
-
const isDev = isDevelopmentBuild(options);
|
|
348
|
-
|
|
349
|
-
try {
|
|
350
|
-
await Bun.write(entryPath, generatePartialEntry(entry.name, entry.filePath));
|
|
351
|
-
const result = await safeBuild({
|
|
352
|
-
entrypoints: [entryPath],
|
|
353
|
-
outdir: outDir,
|
|
354
|
-
naming: outputName,
|
|
355
|
-
minify: shouldMinify(options),
|
|
356
|
-
sourcemap: options.sourcemap ? "external" : "none",
|
|
357
|
-
target: "browser",
|
|
358
|
-
...(isDev ? { reactFastRefresh: true } : {}),
|
|
359
|
-
plugins: [...manduClientPlugins(options), ...(isDev ? [fastRefreshPlugin()] : [])],
|
|
360
|
-
external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
|
|
361
|
-
define: { "process.env.NODE_ENV": nodeEnvDefine(options), ...options.define },
|
|
362
|
-
});
|
|
363
|
-
await fs.unlink(entryPath).catch(() => {});
|
|
364
|
-
|
|
365
323
|
if (!result.success) {
|
|
366
324
|
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
367
|
-
throw new Error(`
|
|
325
|
+
throw new Error(`Island build failed for '${entry.name}' (source: ${entry.filePath}):\n${grouped}\n Hint: Check the import paths and TypeScript types in this island file.`);
|
|
368
326
|
}
|
|
327
|
+
await sanitizeGeneratedClientBundle(path.join(outDir, outputName), isDev);
|
|
328
|
+
return { name: entry.name, js: `/.mandu/client/${outputName}`, route: entry.routeId, priority: entry.priority };
|
|
329
|
+
} catch (error) {
|
|
330
|
+
await fs.unlink(entryPath).catch(() => {});
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface PartialBundleBuild {
|
|
336
|
+
name: string;
|
|
337
|
+
js: string;
|
|
338
|
+
priority: PartialFileEntry["priority"];
|
|
339
|
+
size: number;
|
|
340
|
+
gzipSize: number;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Build a single inline partial bundle. */
|
|
344
|
+
async function buildPartialBundle(
|
|
345
|
+
entry: PartialFileEntry,
|
|
346
|
+
outDir: string,
|
|
347
|
+
options: BundlerOptions,
|
|
348
|
+
): Promise<PartialBundleBuild> {
|
|
349
|
+
const entryPath = path.join(outDir, `_entry_partial_${entry.name}.js`);
|
|
350
|
+
const outputName = `${entry.name}.partial.js`;
|
|
351
|
+
const isDev = isDevelopmentBuild(options);
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
await Bun.write(entryPath, generatePartialEntry(entry.name, entry.filePath));
|
|
355
|
+
const result = await safeBuild({
|
|
356
|
+
entrypoints: [entryPath],
|
|
357
|
+
outdir: outDir,
|
|
358
|
+
naming: outputName,
|
|
359
|
+
minify: shouldMinify(options),
|
|
360
|
+
sourcemap: options.sourcemap ? "external" : "none",
|
|
361
|
+
target: "browser",
|
|
362
|
+
...(isDev ? { reactFastRefresh: true } : {}),
|
|
363
|
+
plugins: [...manduClientPlugins(options), ...(isDev ? [fastRefreshPlugin()] : [])],
|
|
364
|
+
external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
|
|
365
|
+
define: { "process.env.NODE_ENV": nodeEnvDefine(options), ...options.define },
|
|
366
|
+
});
|
|
367
|
+
await fs.unlink(entryPath).catch(() => {});
|
|
368
|
+
|
|
369
|
+
if (!result.success) {
|
|
370
|
+
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
371
|
+
throw new Error(`Partial build failed for '${entry.name}' (source: ${entry.filePath}):\n${grouped}\n Hint: Export a Mandu partial from this file with \`partial({ component })\`.`);
|
|
372
|
+
}
|
|
369
373
|
|
|
370
374
|
const outputPath = path.join(outDir, outputName);
|
|
375
|
+
const sanitizedContent = await sanitizeGeneratedClientBundle(outputPath, isDev);
|
|
371
376
|
const outputFile = Bun.file(outputPath);
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
377
|
+
const gzipped = Bun.gzipSync(Buffer.from(sanitizedContent));
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
name: entry.name,
|
|
381
|
+
js: `/.mandu/client/${outputName}`,
|
|
382
|
+
priority: entry.priority,
|
|
383
|
+
size: outputFile.size,
|
|
384
|
+
gzipSize: gzipped.length,
|
|
385
|
+
};
|
|
386
|
+
} catch (error) {
|
|
387
|
+
await fs.unlink(entryPath).catch(() => {});
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
387
391
|
|
|
388
392
|
/**
|
|
389
393
|
* 빈 매니페스트 생성
|
|
@@ -407,23 +411,23 @@ function createEmptyManifest(env: "development" | "production"): BundleManifest
|
|
|
407
411
|
/**
|
|
408
412
|
* Hydration이 필요한 라우트 필터링
|
|
409
413
|
*/
|
|
410
|
-
function getHydratedRoutes(manifest: RoutesManifest): RouteSpec[] {
|
|
411
|
-
return manifest.routes.filter(
|
|
412
|
-
(route) =>
|
|
413
|
-
route.kind === "page" &&
|
|
414
|
-
route.clientModule &&
|
|
415
|
-
needsHydration(route)
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function getHydrationRoutesMissingClientModule(manifest: RoutesManifest): RouteSpec[] {
|
|
420
|
-
return manifest.routes.filter(
|
|
421
|
-
(route) =>
|
|
422
|
-
route.kind === "page" &&
|
|
423
|
-
!route.clientModule &&
|
|
424
|
-
needsHydration(route)
|
|
425
|
-
);
|
|
426
|
-
}
|
|
414
|
+
function getHydratedRoutes(manifest: RoutesManifest): RouteSpec[] {
|
|
415
|
+
return manifest.routes.filter(
|
|
416
|
+
(route) =>
|
|
417
|
+
route.kind === "page" &&
|
|
418
|
+
route.clientModule &&
|
|
419
|
+
needsHydration(route)
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function getHydrationRoutesMissingClientModule(manifest: RoutesManifest): RouteSpec[] {
|
|
424
|
+
return manifest.routes.filter(
|
|
425
|
+
(route) =>
|
|
426
|
+
route.kind === "page" &&
|
|
427
|
+
!route.clientModule &&
|
|
428
|
+
needsHydration(route)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
427
431
|
|
|
428
432
|
const REACT_SHIM_EXPORTS = [
|
|
429
433
|
"Activity",
|
|
@@ -518,31 +522,31 @@ function generateRuntimeSource(): string {
|
|
|
518
522
|
import React, { useState, useEffect, Component } from 'react';
|
|
519
523
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
520
524
|
|
|
521
|
-
// Hydrated roots 추적 (unmount용) - 전역 초기화
|
|
522
|
-
window.__MANDU_ROOTS__ = window.__MANDU_ROOTS__ || new Map();
|
|
523
|
-
const hydratedRoots = window.__MANDU_ROOTS__;
|
|
524
|
-
|
|
525
|
-
// 서버 데이터
|
|
526
|
-
function readManduData() {
|
|
527
|
-
if (window.__MANDU_DATA__) return window.__MANDU_DATA__;
|
|
528
|
-
|
|
529
|
-
const raw = window.__MANDU_DATA_RAW__ || document.getElementById('__MANDU_DATA__')?.textContent;
|
|
530
|
-
if (!raw) {
|
|
531
|
-
window.__MANDU_DATA__ = {};
|
|
532
|
-
return window.__MANDU_DATA__;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
try {
|
|
536
|
-
window.__MANDU_DATA__ = JSON.parse(raw);
|
|
537
|
-
} catch (error) {
|
|
538
|
-
console.warn('[Mandu] Failed to parse server data:', error);
|
|
539
|
-
window.__MANDU_DATA__ = {};
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return window.__MANDU_DATA__;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const getServerData = (id) => readManduData()[id]?.serverData || {};
|
|
525
|
+
// Hydrated roots 추적 (unmount용) - 전역 초기화
|
|
526
|
+
window.__MANDU_ROOTS__ = window.__MANDU_ROOTS__ || new Map();
|
|
527
|
+
const hydratedRoots = window.__MANDU_ROOTS__;
|
|
528
|
+
|
|
529
|
+
// 서버 데이터
|
|
530
|
+
function readManduData() {
|
|
531
|
+
if (window.__MANDU_DATA__) return window.__MANDU_DATA__;
|
|
532
|
+
|
|
533
|
+
const raw = window.__MANDU_DATA_RAW__ || document.getElementById('__MANDU_DATA__')?.textContent;
|
|
534
|
+
if (!raw) {
|
|
535
|
+
window.__MANDU_DATA__ = {};
|
|
536
|
+
return window.__MANDU_DATA__;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
window.__MANDU_DATA__ = JSON.parse(raw);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
console.warn('[Mandu] Failed to parse server data:', error);
|
|
543
|
+
window.__MANDU_DATA__ = {};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return window.__MANDU_DATA__;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const getServerData = (id) => readManduData()[id]?.serverData || {};
|
|
546
550
|
|
|
547
551
|
/**
|
|
548
552
|
* Error Boundary 컴포넌트 (Class Component)
|
|
@@ -756,16 +760,16 @@ async function loadAndHydrate(element, src) {
|
|
|
756
760
|
const island = module.default;
|
|
757
761
|
let data = getServerData(id);
|
|
758
762
|
|
|
759
|
-
// Fallback: read data-props from the island root or a child element if
|
|
760
|
-
// __MANDU_DATA__ is empty. Inline partials put their serialized props on
|
|
761
|
-
// the root marker itself.
|
|
762
|
-
if (!data || Object.keys(data).length === 0) {
|
|
763
|
-
const propsEl = element.hasAttribute('data-props')
|
|
764
|
-
? element
|
|
765
|
-
: element.querySelector('[data-props]');
|
|
766
|
-
if (propsEl) {
|
|
767
|
-
try {
|
|
768
|
-
data = JSON.parse(propsEl.getAttribute('data-props'));
|
|
763
|
+
// Fallback: read data-props from the island root or a child element if
|
|
764
|
+
// __MANDU_DATA__ is empty. Inline partials put their serialized props on
|
|
765
|
+
// the root marker itself.
|
|
766
|
+
if (!data || Object.keys(data).length === 0) {
|
|
767
|
+
const propsEl = element.hasAttribute('data-props')
|
|
768
|
+
? element
|
|
769
|
+
: element.querySelector('[data-props]');
|
|
770
|
+
if (propsEl) {
|
|
771
|
+
try {
|
|
772
|
+
data = JSON.parse(propsEl.getAttribute('data-props'));
|
|
769
773
|
} catch (e) {
|
|
770
774
|
console.warn('[Mandu] Failed to parse data-props fallback:', e);
|
|
771
775
|
}
|
|
@@ -1439,63 +1443,63 @@ async function buildRouterRuntime(
|
|
|
1439
1443
|
* - Runtime이 dynamic import로 로드
|
|
1440
1444
|
* - 등록/초기화 코드 없음
|
|
1441
1445
|
*/
|
|
1442
|
-
function generateIslandEntry(routeId: string, clientModulePath: string): string {
|
|
1443
|
-
// Windows 경로의 백슬래시를 슬래시로 변환 (JS escape 문제 방지)
|
|
1444
|
-
const normalizedPath = clientModulePath.replace(/\\/g, "/");
|
|
1445
|
-
return `
|
|
1446
|
+
function generateIslandEntry(routeId: string, clientModulePath: string): string {
|
|
1447
|
+
// Windows 경로의 백슬래시를 슬래시로 변환 (JS escape 문제 방지)
|
|
1448
|
+
const normalizedPath = clientModulePath.replace(/\\/g, "/");
|
|
1449
|
+
return `
|
|
1446
1450
|
/**
|
|
1447
1451
|
* Mandu Island: ${routeId} (Generated)
|
|
1448
1452
|
* Pure export - no side effects
|
|
1449
1453
|
*/
|
|
1450
1454
|
import island from "${normalizedPath}";
|
|
1451
|
-
export default island;
|
|
1452
|
-
`;
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
function generatePartialEntry(partialId: string, partialModulePath: string): string {
|
|
1456
|
-
const normalizedPath = partialModulePath.replace(/\\/g, "/");
|
|
1457
|
-
return `
|
|
1458
|
-
/**
|
|
1459
|
-
* Mandu Partial: ${partialId} (Generated)
|
|
1460
|
-
* Exports a runtime-compatible island wrapper around a compiled partial.
|
|
1461
|
-
*/
|
|
1462
|
-
import React from "react";
|
|
1463
|
-
import * as partialModule from "${normalizedPath}";
|
|
1464
|
-
|
|
1465
|
-
function findPartial(mod) {
|
|
1466
|
-
if (mod.default && mod.default.__mandu_partial === true) return mod.default;
|
|
1467
|
-
for (const value of Object.values(mod)) {
|
|
1468
|
-
if (value && value.__mandu_partial === true) return value;
|
|
1469
|
-
}
|
|
1470
|
-
throw new Error("[Mandu Partial] ${partialId} must export a value returned by partial({ component })");
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
const partial = findPartial(partialModule);
|
|
1474
|
-
const definition = partial.definition;
|
|
1475
|
-
const component = definition.component;
|
|
1476
|
-
|
|
1477
|
-
export default {
|
|
1478
|
-
__mandu_island: true,
|
|
1479
|
-
definition: {
|
|
1480
|
-
setup(serverData) {
|
|
1481
|
-
if (serverData && typeof serverData === "object" && Object.keys(serverData).length > 0) {
|
|
1482
|
-
return serverData;
|
|
1483
|
-
}
|
|
1484
|
-
return definition.initialProps || {};
|
|
1485
|
-
},
|
|
1486
|
-
render(props) {
|
|
1487
|
-
return React.createElement(component, props);
|
|
1488
|
-
},
|
|
1489
|
-
errorBoundary: definition.errorBoundary,
|
|
1490
|
-
loading: definition.loading,
|
|
1491
|
-
},
|
|
1492
|
-
};
|
|
1493
|
-
`;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
/**
|
|
1497
|
-
* Runtime 번들 빌드
|
|
1498
|
-
*/
|
|
1455
|
+
export default island;
|
|
1456
|
+
`;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function generatePartialEntry(partialId: string, partialModulePath: string): string {
|
|
1460
|
+
const normalizedPath = partialModulePath.replace(/\\/g, "/");
|
|
1461
|
+
return `
|
|
1462
|
+
/**
|
|
1463
|
+
* Mandu Partial: ${partialId} (Generated)
|
|
1464
|
+
* Exports a runtime-compatible island wrapper around a compiled partial.
|
|
1465
|
+
*/
|
|
1466
|
+
import React from "react";
|
|
1467
|
+
import * as partialModule from "${normalizedPath}";
|
|
1468
|
+
|
|
1469
|
+
function findPartial(mod) {
|
|
1470
|
+
if (mod.default && mod.default.__mandu_partial === true) return mod.default;
|
|
1471
|
+
for (const value of Object.values(mod)) {
|
|
1472
|
+
if (value && value.__mandu_partial === true) return value;
|
|
1473
|
+
}
|
|
1474
|
+
throw new Error("[Mandu Partial] ${partialId} must export a value returned by partial({ component })");
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const partial = findPartial(partialModule);
|
|
1478
|
+
const definition = partial.definition;
|
|
1479
|
+
const component = definition.component;
|
|
1480
|
+
|
|
1481
|
+
export default {
|
|
1482
|
+
__mandu_island: true,
|
|
1483
|
+
definition: {
|
|
1484
|
+
setup(serverData) {
|
|
1485
|
+
if (serverData && typeof serverData === "object" && Object.keys(serverData).length > 0) {
|
|
1486
|
+
return serverData;
|
|
1487
|
+
}
|
|
1488
|
+
return definition.initialProps || {};
|
|
1489
|
+
},
|
|
1490
|
+
render(props) {
|
|
1491
|
+
return React.createElement(component, props);
|
|
1492
|
+
},
|
|
1493
|
+
errorBoundary: definition.errorBoundary,
|
|
1494
|
+
loading: definition.loading,
|
|
1495
|
+
},
|
|
1496
|
+
};
|
|
1497
|
+
`;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* Runtime 번들 빌드
|
|
1502
|
+
*/
|
|
1499
1503
|
async function buildRuntime(
|
|
1500
1504
|
outDir: string,
|
|
1501
1505
|
options: BundlerOptions
|
|
@@ -1776,9 +1780,9 @@ async function buildVendorShims(
|
|
|
1776
1780
|
}
|
|
1777
1781
|
}
|
|
1778
1782
|
|
|
1779
|
-
const buildShim = async (
|
|
1780
|
-
shim: { name: string; source: string; key: VendorShimKey; cacheId: string }
|
|
1781
|
-
): Promise<{ key: VendorShimKey; cacheId: string; outputName?: string; outputPath?: string; error?: string }> => {
|
|
1783
|
+
const buildShim = async (
|
|
1784
|
+
shim: { name: string; source: string; key: VendorShimKey; cacheId: string }
|
|
1785
|
+
): Promise<{ key: VendorShimKey; cacheId: string; outputName?: string; outputPath?: string; error?: string }> => {
|
|
1782
1786
|
const srcPath = path.join(outDir, `${shim.name}.src.js`);
|
|
1783
1787
|
const outputName = `${shim.name}.js`;
|
|
1784
1788
|
|
|
@@ -1814,14 +1818,14 @@ async function buildVendorShims(
|
|
|
1814
1818
|
|
|
1815
1819
|
await fs.unlink(srcPath).catch(() => {});
|
|
1816
1820
|
|
|
1817
|
-
if (!result.success) {
|
|
1818
|
-
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
1819
|
-
return {
|
|
1820
|
-
key: shim.key,
|
|
1821
|
-
cacheId: shim.cacheId,
|
|
1822
|
-
error: `Vendor shim '${shim.name}' build failed (source: ${srcPath}):\n${grouped}\n ${vendorShimFailureHint(shim.name)}`,
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1821
|
+
if (!result.success) {
|
|
1822
|
+
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
1823
|
+
return {
|
|
1824
|
+
key: shim.key,
|
|
1825
|
+
cacheId: shim.cacheId,
|
|
1826
|
+
error: `Vendor shim '${shim.name}' build failed (source: ${srcPath}):\n${grouped}\n ${vendorShimFailureHint(shim.name)}`,
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1825
1829
|
|
|
1826
1830
|
return {
|
|
1827
1831
|
key: shim.key,
|
|
@@ -1831,13 +1835,13 @@ async function buildVendorShims(
|
|
|
1831
1835
|
};
|
|
1832
1836
|
} catch (error) {
|
|
1833
1837
|
await fs.unlink(srcPath).catch(() => {});
|
|
1834
|
-
return {
|
|
1835
|
-
key: shim.key,
|
|
1836
|
-
cacheId: shim.cacheId,
|
|
1837
|
-
error: `[${shim.name}] ${String(error)}\n ${vendorShimFailureHint(shim.name)}`,
|
|
1838
|
-
};
|
|
1839
|
-
}
|
|
1840
|
-
};
|
|
1838
|
+
return {
|
|
1839
|
+
key: shim.key,
|
|
1840
|
+
cacheId: shim.cacheId,
|
|
1841
|
+
error: `[${shim.name}] ${String(error)}\n ${vendorShimFailureHint(shim.name)}`,
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1841
1845
|
|
|
1842
1846
|
const buildResults = await Promise.all(shims.map((shim) => buildShim(shim)));
|
|
1843
1847
|
const writeEntries: VendorCacheWriteEntry[] = [];
|
|
@@ -1880,14 +1884,14 @@ async function buildVendorShims(
|
|
|
1880
1884
|
fastRefreshRuntime: results.fastRefreshRuntime,
|
|
1881
1885
|
errors,
|
|
1882
1886
|
};
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
function vendorShimFailureHint(shimName: string): string {
|
|
1886
|
-
if (shimName.includes("react-refresh")) {
|
|
1887
|
-
return "Hint: install the optional dev peer dependency with `bun add -d react-refresh`.";
|
|
1888
|
-
}
|
|
1889
|
-
return "Hint: check the import paths and ensure the vendor package is installed.";
|
|
1890
|
-
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function vendorShimFailureHint(shimName: string): string {
|
|
1890
|
+
if (shimName.includes("react-refresh")) {
|
|
1891
|
+
return "Hint: install the optional dev peer dependency with `bun add -d react-refresh`.";
|
|
1892
|
+
}
|
|
1893
|
+
return "Hint: check the import paths and ensure the vendor package is installed.";
|
|
1894
|
+
}
|
|
1891
1895
|
|
|
1892
1896
|
/**
|
|
1893
1897
|
* 단일 Island 번들 빌드
|
|
@@ -1959,9 +1963,9 @@ async function buildIsland(
|
|
|
1959
1963
|
actualOutputName = outputName;
|
|
1960
1964
|
}
|
|
1961
1965
|
|
|
1962
|
-
const outputFile = Bun.file(actualOutputPath);
|
|
1963
|
-
const content = await
|
|
1964
|
-
const gzipped = Bun.gzipSync(Buffer.from(content));
|
|
1966
|
+
const outputFile = Bun.file(actualOutputPath);
|
|
1967
|
+
const content = await sanitizeGeneratedClientBundle(actualOutputPath, isDev);
|
|
1968
|
+
const gzipped = Bun.gzipSync(Buffer.from(content));
|
|
1965
1969
|
|
|
1966
1970
|
return {
|
|
1967
1971
|
routeId: route.id,
|
|
@@ -1973,22 +1977,64 @@ async function buildIsland(
|
|
|
1973
1977
|
} catch (error) {
|
|
1974
1978
|
await fs.unlink(entryPath).catch(() => {});
|
|
1975
1979
|
throw error;
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
async function sanitizeGeneratedClientBundle(outputPath: string, isDev: boolean): Promise<string> {
|
|
1984
|
+
const source = await Bun.file(outputPath).text();
|
|
1985
|
+
if (!isDev) return source;
|
|
1986
|
+
|
|
1987
|
+
const sanitized = removeFastRefreshSelfAliases(source);
|
|
1988
|
+
if (sanitized === source) return source;
|
|
1989
|
+
|
|
1990
|
+
await Bun.write(outputPath, sanitized);
|
|
1991
|
+
await validateGeneratedClientBundle(outputPath);
|
|
1992
|
+
return sanitized;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
function removeFastRefreshSelfAliases(source: string): string {
|
|
1996
|
+
return source.replace(FAST_REFRESH_SELF_ALIAS_PATTERN, (line, localName: string, offset: number) => {
|
|
1997
|
+
const priorSource = source.slice(0, offset);
|
|
1998
|
+
const declarationPattern = new RegExp(`(?:const|let|class|function)\\s+${escapeRegExp(localName)}\\b`);
|
|
1999
|
+
return declarationPattern.test(priorSource) ? "" : line;
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
async function validateGeneratedClientBundle(outputPath: string): Promise<void> {
|
|
2004
|
+
const tempOutDir = await fs.mkdtemp(path.join(path.dirname(outputPath), ".validate-"));
|
|
2005
|
+
try {
|
|
2006
|
+
const result = await safeBuild({
|
|
2007
|
+
entrypoints: [outputPath],
|
|
2008
|
+
outdir: tempOutDir,
|
|
2009
|
+
target: "browser",
|
|
2010
|
+
external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "react/jsx-dev-runtime"],
|
|
2011
|
+
});
|
|
2012
|
+
if (result.success) return;
|
|
2013
|
+
|
|
2014
|
+
const grouped = result.logs.map((log) => ` - ${log.message}`).join("\n");
|
|
2015
|
+
throw new Error(`Generated client bundle failed syntax validation (source: ${outputPath}):\n${grouped}`);
|
|
2016
|
+
} finally {
|
|
2017
|
+
await fs.rm(tempOutDir, { recursive: true, force: true }).catch(() => {});
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
function escapeRegExp(value: string): string {
|
|
2022
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/**
|
|
2026
|
+
* 번들 매니페스트 생성
|
|
1981
2027
|
*/
|
|
1982
|
-
function createBundleManifest(
|
|
1983
|
-
outputs: BundleOutput[],
|
|
1984
|
-
routes: RouteSpec[],
|
|
1985
|
-
runtimePath: string,
|
|
1986
|
-
vendorResult: VendorBuildResult,
|
|
1987
|
-
routerPath: string,
|
|
1988
|
-
env: "development" | "production",
|
|
1989
|
-
islandBundles?: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }>,
|
|
1990
|
-
partialBundles?: Array<{ name: string; js: string; priority: PartialFileEntry["priority"] }>,
|
|
1991
|
-
): BundleManifest {
|
|
2028
|
+
function createBundleManifest(
|
|
2029
|
+
outputs: BundleOutput[],
|
|
2030
|
+
routes: RouteSpec[],
|
|
2031
|
+
runtimePath: string,
|
|
2032
|
+
vendorResult: VendorBuildResult,
|
|
2033
|
+
routerPath: string,
|
|
2034
|
+
env: "development" | "production",
|
|
2035
|
+
islandBundles?: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }>,
|
|
2036
|
+
partialBundles?: Array<{ name: string; js: string; priority: PartialFileEntry["priority"] }>,
|
|
2037
|
+
): BundleManifest {
|
|
1992
2038
|
const bundles: BundleManifest["bundles"] = {};
|
|
1993
2039
|
|
|
1994
2040
|
for (const output of outputs) {
|
|
@@ -2004,27 +2050,27 @@ function createBundleManifest(
|
|
|
2004
2050
|
|
|
2005
2051
|
// Per-island bundles (code splitting)
|
|
2006
2052
|
let islands: BundleManifest["islands"];
|
|
2007
|
-
if (islandBundles && islandBundles.length > 0) {
|
|
2008
|
-
islands = {};
|
|
2009
|
-
for (const ib of islandBundles) {
|
|
2010
|
-
islands[ib.name] = {
|
|
2011
|
-
js: ib.js,
|
|
2053
|
+
if (islandBundles && islandBundles.length > 0) {
|
|
2054
|
+
islands = {};
|
|
2055
|
+
for (const ib of islandBundles) {
|
|
2056
|
+
islands[ib.name] = {
|
|
2057
|
+
js: ib.js,
|
|
2012
2058
|
route: ib.route,
|
|
2013
2059
|
priority: ib.priority,
|
|
2014
|
-
};
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
let partials: BundleManifest["partials"];
|
|
2019
|
-
if (partialBundles && partialBundles.length > 0) {
|
|
2020
|
-
partials = {};
|
|
2021
|
-
for (const partial of partialBundles) {
|
|
2022
|
-
partials[partial.name] = {
|
|
2023
|
-
js: partial.js,
|
|
2024
|
-
priority: partial.priority,
|
|
2025
|
-
};
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
let partials: BundleManifest["partials"];
|
|
2065
|
+
if (partialBundles && partialBundles.length > 0) {
|
|
2066
|
+
partials = {};
|
|
2067
|
+
for (const partial of partialBundles) {
|
|
2068
|
+
partials[partial.name] = {
|
|
2069
|
+
js: partial.js,
|
|
2070
|
+
priority: partial.priority,
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2028
2074
|
|
|
2029
2075
|
// Phase 7.1 B-2: expose Fast Refresh dev bundles so the HTML
|
|
2030
2076
|
// preamble can inject a dynamic import pointing at them. Only
|
|
@@ -2040,11 +2086,11 @@ function createBundleManifest(
|
|
|
2040
2086
|
return {
|
|
2041
2087
|
version: 1,
|
|
2042
2088
|
buildTime: new Date().toISOString(),
|
|
2043
|
-
env,
|
|
2044
|
-
bundles,
|
|
2045
|
-
...(islands ? { islands } : {}),
|
|
2046
|
-
...(partials ? { partials } : {}),
|
|
2047
|
-
shared: {
|
|
2089
|
+
env,
|
|
2090
|
+
bundles,
|
|
2091
|
+
...(islands ? { islands } : {}),
|
|
2092
|
+
...(partials ? { partials } : {}),
|
|
2093
|
+
shared: {
|
|
2048
2094
|
runtime: runtimePath,
|
|
2049
2095
|
vendor: vendorResult.react, // primary vendor for backwards compatibility
|
|
2050
2096
|
router: routerPath, // Client-side Router
|
|
@@ -2065,32 +2111,32 @@ function createBundleManifest(
|
|
|
2065
2111
|
/**
|
|
2066
2112
|
* 번들 통계 계산
|
|
2067
2113
|
*/
|
|
2068
|
-
function calculateStats(
|
|
2069
|
-
outputs: BundleOutput[],
|
|
2070
|
-
startTime: number,
|
|
2071
|
-
extraOutputs: Array<{ routeId: string; size: number; gzipSize: number }> = [],
|
|
2072
|
-
): BundleStats {
|
|
2073
|
-
let totalSize = 0;
|
|
2074
|
-
let totalGzipSize = 0;
|
|
2075
|
-
let largestBundle = { routeId: "", size: 0 };
|
|
2076
|
-
|
|
2077
|
-
for (const output of [...outputs, ...extraOutputs]) {
|
|
2078
|
-
totalSize += output.size;
|
|
2079
|
-
totalGzipSize += output.gzipSize;
|
|
2080
|
-
|
|
2081
|
-
if (output.size > largestBundle.size) {
|
|
2082
|
-
largestBundle = { routeId: output.routeId, size: output.size };
|
|
2114
|
+
function calculateStats(
|
|
2115
|
+
outputs: BundleOutput[],
|
|
2116
|
+
startTime: number,
|
|
2117
|
+
extraOutputs: Array<{ routeId: string; size: number; gzipSize: number }> = [],
|
|
2118
|
+
): BundleStats {
|
|
2119
|
+
let totalSize = 0;
|
|
2120
|
+
let totalGzipSize = 0;
|
|
2121
|
+
let largestBundle = { routeId: "", size: 0 };
|
|
2122
|
+
|
|
2123
|
+
for (const output of [...outputs, ...extraOutputs]) {
|
|
2124
|
+
totalSize += output.size;
|
|
2125
|
+
totalGzipSize += output.gzipSize;
|
|
2126
|
+
|
|
2127
|
+
if (output.size > largestBundle.size) {
|
|
2128
|
+
largestBundle = { routeId: output.routeId, size: output.size };
|
|
2083
2129
|
}
|
|
2084
2130
|
}
|
|
2085
2131
|
|
|
2086
2132
|
return {
|
|
2087
2133
|
totalSize,
|
|
2088
|
-
totalGzipSize,
|
|
2089
|
-
largestBundle,
|
|
2090
|
-
buildTime: performance.now() - startTime,
|
|
2091
|
-
bundleCount: outputs.length + extraOutputs.length,
|
|
2092
|
-
};
|
|
2093
|
-
}
|
|
2134
|
+
totalGzipSize,
|
|
2135
|
+
largestBundle,
|
|
2136
|
+
buildTime: performance.now() - startTime,
|
|
2137
|
+
bundleCount: outputs.length + extraOutputs.length,
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2094
2140
|
|
|
2095
2141
|
/**
|
|
2096
2142
|
* 클라이언트 번들 빌드
|
|
@@ -2134,47 +2180,47 @@ export async function buildClientBundles(
|
|
|
2134
2180
|
errors.push(`onBundleComplete[${e.source}]: ${e.error.message}`);
|
|
2135
2181
|
}
|
|
2136
2182
|
};
|
|
2137
|
-
const env = resolveBundlerMode(options);
|
|
2138
|
-
|
|
2139
|
-
// 1. Hydration이 필요한 라우트 필터링
|
|
2140
|
-
const invalidClientRouteIds = new Set<string>();
|
|
2141
|
-
const runtimeRoutes = manifest.routes.filter((route) => route.kind === "page" && needsHydration(route));
|
|
2142
|
-
const partialFiles = runtimeRoutes.length > 0 ? await scanPartialFiles(rootDir) : [];
|
|
2143
|
-
const routesMissingClientModule = getHydrationRoutesMissingClientModule(manifest);
|
|
2144
|
-
if (routesMissingClientModule.length > 0) {
|
|
2145
|
-
const missingClientErrors = await Promise.all(
|
|
2146
|
-
routesMissingClientModule.map((route) =>
|
|
2147
|
-
describeMissingHydrationClientModule(route, rootDir, {
|
|
2148
|
-
allowPartialOnly: partialFiles.length > 0,
|
|
2149
|
-
})
|
|
2150
|
-
)
|
|
2151
|
-
);
|
|
2152
|
-
errors.push(...missingClientErrors.filter((error): error is string => error !== null));
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
let hydratedRoutes = getHydratedRoutes(manifest);
|
|
2156
|
-
if (hydratedRoutes.length > 0) {
|
|
2157
|
-
const validRoutes: RouteSpec[] = [];
|
|
2158
|
-
for (const route of hydratedRoutes) {
|
|
2159
|
-
const validationError = await validateClientModuleForBrowserBundle(route, rootDir);
|
|
2160
|
-
if (validationError) {
|
|
2161
|
-
invalidClientRouteIds.add(route.id);
|
|
2162
|
-
errors.push(validationError);
|
|
2163
|
-
continue;
|
|
2164
|
-
}
|
|
2165
|
-
validRoutes.push(route);
|
|
2166
|
-
}
|
|
2167
|
-
hydratedRoutes = validRoutes;
|
|
2168
|
-
}
|
|
2169
|
-
// 2. 출력 디렉토리 생성 (항상 필요 - 매니페스트 저장용)
|
|
2183
|
+
const env = resolveBundlerMode(options);
|
|
2184
|
+
|
|
2185
|
+
// 1. Hydration이 필요한 라우트 필터링
|
|
2186
|
+
const invalidClientRouteIds = new Set<string>();
|
|
2187
|
+
const runtimeRoutes = manifest.routes.filter((route) => route.kind === "page" && needsHydration(route));
|
|
2188
|
+
const partialFiles = runtimeRoutes.length > 0 ? await scanPartialFiles(rootDir) : [];
|
|
2189
|
+
const routesMissingClientModule = getHydrationRoutesMissingClientModule(manifest);
|
|
2190
|
+
if (routesMissingClientModule.length > 0) {
|
|
2191
|
+
const missingClientErrors = await Promise.all(
|
|
2192
|
+
routesMissingClientModule.map((route) =>
|
|
2193
|
+
describeMissingHydrationClientModule(route, rootDir, {
|
|
2194
|
+
allowPartialOnly: partialFiles.length > 0,
|
|
2195
|
+
})
|
|
2196
|
+
)
|
|
2197
|
+
);
|
|
2198
|
+
errors.push(...missingClientErrors.filter((error): error is string => error !== null));
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
let hydratedRoutes = getHydratedRoutes(manifest);
|
|
2202
|
+
if (hydratedRoutes.length > 0) {
|
|
2203
|
+
const validRoutes: RouteSpec[] = [];
|
|
2204
|
+
for (const route of hydratedRoutes) {
|
|
2205
|
+
const validationError = await validateClientModuleForBrowserBundle(route, rootDir);
|
|
2206
|
+
if (validationError) {
|
|
2207
|
+
invalidClientRouteIds.add(route.id);
|
|
2208
|
+
errors.push(validationError);
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
validRoutes.push(route);
|
|
2212
|
+
}
|
|
2213
|
+
hydratedRoutes = validRoutes;
|
|
2214
|
+
}
|
|
2215
|
+
// 2. 출력 디렉토리 생성 (항상 필요 - 매니페스트 저장용)
|
|
2170
2216
|
const outDir = resolveClientOutDir(rootDir, options.outDir);
|
|
2171
2217
|
await fs.mkdir(outDir, { recursive: true });
|
|
2172
2218
|
|
|
2173
2219
|
// Hydration 라우트가 없어도 빈 매니페스트를 저장해야 함
|
|
2174
2220
|
// (이전 빌드의 stale 매니페스트 참조 방지)
|
|
2175
|
-
if (hydratedRoutes.length === 0 && partialFiles.length === 0) {
|
|
2221
|
+
if (hydratedRoutes.length === 0 && partialFiles.length === 0) {
|
|
2176
2222
|
// #185: skipFrameworkBundles 모드에서는 기존 manifest를 그대로 유지 (devtools 재빌드도 스킵)
|
|
2177
|
-
if (options.skipFrameworkBundles && errors.length === 0) {
|
|
2223
|
+
if (options.skipFrameworkBundles && errors.length === 0) {
|
|
2178
2224
|
const manifestPath = path.join(rootDir, ".mandu/manifest.json");
|
|
2179
2225
|
try {
|
|
2180
2226
|
const manifestRaw = await fs.readFile(manifestPath, "utf-8");
|
|
@@ -2225,11 +2271,11 @@ export async function buildClientBundles(
|
|
|
2225
2271
|
path.join(rootDir, ".mandu/manifest.json"),
|
|
2226
2272
|
JSON.stringify(emptyManifest, null, 2)
|
|
2227
2273
|
);
|
|
2228
|
-
return {
|
|
2229
|
-
success: errors.length === 0,
|
|
2230
|
-
outputs: [],
|
|
2231
|
-
errors,
|
|
2232
|
-
manifest: emptyManifest,
|
|
2274
|
+
return {
|
|
2275
|
+
success: errors.length === 0,
|
|
2276
|
+
outputs: [],
|
|
2277
|
+
errors,
|
|
2278
|
+
manifest: emptyManifest,
|
|
2233
2279
|
stats: {
|
|
2234
2280
|
totalSize: 0,
|
|
2235
2281
|
totalGzipSize: 0,
|
|
@@ -2268,14 +2314,14 @@ export async function buildClientBundles(
|
|
|
2268
2314
|
return buildClientBundles(manifest, rootDir, { ...options, targetRouteIds: undefined });
|
|
2269
2315
|
}
|
|
2270
2316
|
|
|
2271
|
-
// Only update manifest with successfully built outputs (#10: preserve previous good manifest on failure)
|
|
2272
|
-
for (const routeId of invalidClientRouteIds) {
|
|
2273
|
-
delete existingManifest.bundles[routeId];
|
|
2274
|
-
}
|
|
2275
|
-
if (outputs.length > 0 || invalidClientRouteIds.size > 0) {
|
|
2276
|
-
for (const output of outputs) {
|
|
2277
|
-
if (existingManifest.bundles[output.routeId]) {
|
|
2278
|
-
existingManifest.bundles[output.routeId].js = output.outputPath;
|
|
2317
|
+
// Only update manifest with successfully built outputs (#10: preserve previous good manifest on failure)
|
|
2318
|
+
for (const routeId of invalidClientRouteIds) {
|
|
2319
|
+
delete existingManifest.bundles[routeId];
|
|
2320
|
+
}
|
|
2321
|
+
if (outputs.length > 0 || invalidClientRouteIds.size > 0) {
|
|
2322
|
+
for (const output of outputs) {
|
|
2323
|
+
if (existingManifest.bundles[output.routeId]) {
|
|
2324
|
+
existingManifest.bundles[output.routeId].js = output.outputPath;
|
|
2279
2325
|
} else {
|
|
2280
2326
|
const route = targetRoutes.find((r) => r.id === output.routeId);
|
|
2281
2327
|
const hydration = route ? getRouteHydration(route) : null;
|
|
@@ -2287,11 +2333,11 @@ export async function buildClientBundles(
|
|
|
2287
2333
|
}
|
|
2288
2334
|
}
|
|
2289
2335
|
|
|
2290
|
-
await fs.writeFile(
|
|
2291
|
-
path.join(rootDir, ".mandu/manifest.json"),
|
|
2292
|
-
JSON.stringify(existingManifest, null, 2)
|
|
2293
|
-
);
|
|
2294
|
-
}
|
|
2336
|
+
await fs.writeFile(
|
|
2337
|
+
path.join(rootDir, ".mandu/manifest.json"),
|
|
2338
|
+
JSON.stringify(existingManifest, null, 2)
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2295
2341
|
// When all builds failed, do NOT overwrite manifest — keep previous good state
|
|
2296
2342
|
|
|
2297
2343
|
const stats = calculateStats(outputs, startTime);
|
|
@@ -2327,13 +2373,13 @@ export async function buildClientBundles(
|
|
|
2327
2373
|
"[Mandu] Existing manifest missing required fields (shared/bundles), falling back to full build",
|
|
2328
2374
|
);
|
|
2329
2375
|
return buildClientBundles(manifest, rootDir, { ...options, skipFrameworkBundles: false });
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
for (const routeId of invalidClientRouteIds) {
|
|
2333
|
-
delete existingManifest.bundles[routeId];
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
// Pre-build validation + 병렬 island 빌드 (framework 번들은 스킵)
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
for (const routeId of invalidClientRouteIds) {
|
|
2379
|
+
delete existingManifest.bundles[routeId];
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// Pre-build validation + 병렬 island 빌드 (framework 번들은 스킵)
|
|
2337
2383
|
for (const route of hydratedRoutes) {
|
|
2338
2384
|
if (!route.clientModule) continue;
|
|
2339
2385
|
const clientModulePath = path.join(rootDir, route.clientModule);
|
|
@@ -2398,62 +2444,62 @@ export async function buildClientBundles(
|
|
|
2398
2444
|
};
|
|
2399
2445
|
}
|
|
2400
2446
|
}
|
|
2401
|
-
if (perIslandBundles.length > 0) {
|
|
2402
|
-
existingManifest.islands = existingManifest.islands || {};
|
|
2403
|
-
for (const ib of perIslandBundles) {
|
|
2404
|
-
existingManifest.islands[ib.name] = {
|
|
2405
|
-
js: ib.js,
|
|
2447
|
+
if (perIslandBundles.length > 0) {
|
|
2448
|
+
existingManifest.islands = existingManifest.islands || {};
|
|
2449
|
+
for (const ib of perIslandBundles) {
|
|
2450
|
+
existingManifest.islands[ib.name] = {
|
|
2451
|
+
js: ib.js,
|
|
2406
2452
|
route: ib.route,
|
|
2407
2453
|
priority: ib.priority,
|
|
2408
|
-
};
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
const partialBundles: PartialBundleBuild[] = [];
|
|
2413
|
-
if (partialFiles.length > 0) {
|
|
2414
|
-
const partialResults = await Promise.all(
|
|
2415
|
-
partialFiles.map(async (entry) => {
|
|
2416
|
-
try {
|
|
2417
|
-
return await buildPartialBundle(entry, outDir, options);
|
|
2418
|
-
} catch (error) {
|
|
2419
|
-
errors.push(`[partial:${entry.name}] ${String(error)}`);
|
|
2420
|
-
return null;
|
|
2421
|
-
}
|
|
2422
|
-
}),
|
|
2423
|
-
);
|
|
2424
|
-
for (const result of partialResults) {
|
|
2425
|
-
if (result) partialBundles.push(result);
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
if (partialFiles.length > 0 || existingManifest.partials) {
|
|
2429
|
-
existingManifest.partials = {};
|
|
2430
|
-
for (const partial of partialBundles) {
|
|
2431
|
-
existingManifest.partials[partial.name] = {
|
|
2432
|
-
js: partial.js,
|
|
2433
|
-
priority: partial.priority,
|
|
2434
|
-
};
|
|
2435
|
-
}
|
|
2436
|
-
if (Object.keys(existingManifest.partials).length === 0) {
|
|
2437
|
-
delete existingManifest.partials;
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
await fs.writeFile(
|
|
2442
|
-
path.join(rootDir, ".mandu/manifest.json"),
|
|
2443
|
-
JSON.stringify(existingManifest, null, 2),
|
|
2444
|
-
);
|
|
2445
|
-
|
|
2446
|
-
const stats = calculateStats(
|
|
2447
|
-
outputs,
|
|
2448
|
-
startTime,
|
|
2449
|
-
partialBundles.map((partial) => ({
|
|
2450
|
-
routeId: `partial:${partial.name}`,
|
|
2451
|
-
size: partial.size,
|
|
2452
|
-
gzipSize: partial.gzipSize,
|
|
2453
|
-
})),
|
|
2454
|
-
);
|
|
2455
|
-
return { success: errors.length === 0, outputs, errors, manifest: existingManifest, stats };
|
|
2456
|
-
}
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
const partialBundles: PartialBundleBuild[] = [];
|
|
2459
|
+
if (partialFiles.length > 0) {
|
|
2460
|
+
const partialResults = await Promise.all(
|
|
2461
|
+
partialFiles.map(async (entry) => {
|
|
2462
|
+
try {
|
|
2463
|
+
return await buildPartialBundle(entry, outDir, options);
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
errors.push(`[partial:${entry.name}] ${String(error)}`);
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2468
|
+
}),
|
|
2469
|
+
);
|
|
2470
|
+
for (const result of partialResults) {
|
|
2471
|
+
if (result) partialBundles.push(result);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
if (partialFiles.length > 0 || existingManifest.partials) {
|
|
2475
|
+
existingManifest.partials = {};
|
|
2476
|
+
for (const partial of partialBundles) {
|
|
2477
|
+
existingManifest.partials[partial.name] = {
|
|
2478
|
+
js: partial.js,
|
|
2479
|
+
priority: partial.priority,
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
if (Object.keys(existingManifest.partials).length === 0) {
|
|
2483
|
+
delete existingManifest.partials;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
await fs.writeFile(
|
|
2488
|
+
path.join(rootDir, ".mandu/manifest.json"),
|
|
2489
|
+
JSON.stringify(existingManifest, null, 2),
|
|
2490
|
+
);
|
|
2491
|
+
|
|
2492
|
+
const stats = calculateStats(
|
|
2493
|
+
outputs,
|
|
2494
|
+
startTime,
|
|
2495
|
+
partialBundles.map((partial) => ({
|
|
2496
|
+
routeId: `partial:${partial.name}`,
|
|
2497
|
+
size: partial.size,
|
|
2498
|
+
gzipSize: partial.gzipSize,
|
|
2499
|
+
})),
|
|
2500
|
+
);
|
|
2501
|
+
return { success: errors.length === 0, outputs, errors, manifest: existingManifest, stats };
|
|
2502
|
+
}
|
|
2457
2503
|
|
|
2458
2504
|
// 3-4. Runtime, Router, Vendor, DevTools 번들 병렬 빌드 (서로 독립적)
|
|
2459
2505
|
const isDev = env === "development";
|
|
@@ -2539,11 +2585,11 @@ export async function buildClientBundles(
|
|
|
2539
2585
|
const islandFiles = await scanIslandFiles(hydratedRoutes, rootDir);
|
|
2540
2586
|
const islandBundles: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> = [];
|
|
2541
2587
|
|
|
2542
|
-
if (islandFiles.length > 0) {
|
|
2543
|
-
const islandResults = await Promise.all(
|
|
2544
|
-
islandFiles.map(async (entry) => {
|
|
2545
|
-
try {
|
|
2546
|
-
return await buildPerIslandBundle(entry, outDir, options);
|
|
2588
|
+
if (islandFiles.length > 0) {
|
|
2589
|
+
const islandResults = await Promise.all(
|
|
2590
|
+
islandFiles.map(async (entry) => {
|
|
2591
|
+
try {
|
|
2592
|
+
return await buildPerIslandBundle(entry, outDir, options);
|
|
2547
2593
|
} catch (error) {
|
|
2548
2594
|
errors.push(`[island:${entry.name}] ${String(error)}`);
|
|
2549
2595
|
return null;
|
|
@@ -2551,38 +2597,38 @@ export async function buildClientBundles(
|
|
|
2551
2597
|
})
|
|
2552
2598
|
);
|
|
2553
2599
|
for (const result of islandResults) {
|
|
2554
|
-
if (result) islandBundles.push(result);
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
const partialBundles: PartialBundleBuild[] = [];
|
|
2559
|
-
if (partialFiles.length > 0) {
|
|
2560
|
-
const partialResults = await Promise.all(
|
|
2561
|
-
partialFiles.map(async (entry) => {
|
|
2562
|
-
try {
|
|
2563
|
-
return await buildPartialBundle(entry, outDir, options);
|
|
2564
|
-
} catch (error) {
|
|
2565
|
-
errors.push(`[partial:${entry.name}] ${String(error)}`);
|
|
2566
|
-
return null;
|
|
2567
|
-
}
|
|
2568
|
-
}),
|
|
2569
|
-
);
|
|
2570
|
-
for (const result of partialResults) {
|
|
2571
|
-
if (result) partialBundles.push(result);
|
|
2572
|
-
}
|
|
2573
|
-
}
|
|
2574
|
-
|
|
2575
|
-
// 6. 번들 매니페스트 생성
|
|
2576
|
-
const bundleManifest = createBundleManifest(
|
|
2577
|
-
outputs,
|
|
2578
|
-
hydratedRoutes,
|
|
2579
|
-
runtimeResult.outputPath,
|
|
2580
|
-
vendorResult,
|
|
2581
|
-
routerResult.outputPath,
|
|
2582
|
-
env,
|
|
2583
|
-
islandBundles,
|
|
2584
|
-
partialBundles,
|
|
2585
|
-
);
|
|
2600
|
+
if (result) islandBundles.push(result);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
const partialBundles: PartialBundleBuild[] = [];
|
|
2605
|
+
if (partialFiles.length > 0) {
|
|
2606
|
+
const partialResults = await Promise.all(
|
|
2607
|
+
partialFiles.map(async (entry) => {
|
|
2608
|
+
try {
|
|
2609
|
+
return await buildPartialBundle(entry, outDir, options);
|
|
2610
|
+
} catch (error) {
|
|
2611
|
+
errors.push(`[partial:${entry.name}] ${String(error)}`);
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
}),
|
|
2615
|
+
);
|
|
2616
|
+
for (const result of partialResults) {
|
|
2617
|
+
if (result) partialBundles.push(result);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// 6. 번들 매니페스트 생성
|
|
2622
|
+
const bundleManifest = createBundleManifest(
|
|
2623
|
+
outputs,
|
|
2624
|
+
hydratedRoutes,
|
|
2625
|
+
runtimeResult.outputPath,
|
|
2626
|
+
vendorResult,
|
|
2627
|
+
routerResult.outputPath,
|
|
2628
|
+
env,
|
|
2629
|
+
islandBundles,
|
|
2630
|
+
partialBundles,
|
|
2631
|
+
);
|
|
2586
2632
|
|
|
2587
2633
|
await fs.writeFile(
|
|
2588
2634
|
path.join(rootDir, ".mandu/manifest.json"),
|
|
@@ -2590,15 +2636,15 @@ export async function buildClientBundles(
|
|
|
2590
2636
|
);
|
|
2591
2637
|
|
|
2592
2638
|
// 7. 통계 계산
|
|
2593
|
-
const stats = calculateStats(
|
|
2594
|
-
outputs,
|
|
2595
|
-
startTime,
|
|
2596
|
-
partialBundles.map((partial) => ({
|
|
2597
|
-
routeId: `partial:${partial.name}`,
|
|
2598
|
-
size: partial.size,
|
|
2599
|
-
gzipSize: partial.gzipSize,
|
|
2600
|
-
})),
|
|
2601
|
-
);
|
|
2639
|
+
const stats = calculateStats(
|
|
2640
|
+
outputs,
|
|
2641
|
+
startTime,
|
|
2642
|
+
partialBundles.map((partial) => ({
|
|
2643
|
+
routeId: `partial:${partial.name}`,
|
|
2644
|
+
size: partial.size,
|
|
2645
|
+
gzipSize: partial.gzipSize,
|
|
2646
|
+
})),
|
|
2647
|
+
);
|
|
2602
2648
|
|
|
2603
2649
|
// Phase 18.τ — fire onBundleComplete(stats) before return.
|
|
2604
2650
|
await fireOnBundleComplete(stats);
|
|
@@ -2626,14 +2672,14 @@ export function formatSize(bytes: number): string {
|
|
|
2626
2672
|
* 번들 결과 요약 출력
|
|
2627
2673
|
*/
|
|
2628
2674
|
export function printBundleStats(result: BundleResult): void {
|
|
2629
|
-
console.log("\n📦 Mandu Client Bundles");
|
|
2630
|
-
console.log("=".repeat(50));
|
|
2631
|
-
|
|
2632
|
-
const partialCount = Object.keys(result.manifest.partials ?? {}).length;
|
|
2633
|
-
if (result.outputs.length === 0 && partialCount === 0) {
|
|
2634
|
-
console.log("No islands or partials to bundle (hydration: none or no client entry)");
|
|
2635
|
-
return;
|
|
2636
|
-
}
|
|
2675
|
+
console.log("\n📦 Mandu Client Bundles");
|
|
2676
|
+
console.log("=".repeat(50));
|
|
2677
|
+
|
|
2678
|
+
const partialCount = Object.keys(result.manifest.partials ?? {}).length;
|
|
2679
|
+
if (result.outputs.length === 0 && partialCount === 0) {
|
|
2680
|
+
console.log("No islands or partials to bundle (hydration: none or no client entry)");
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2637
2683
|
|
|
2638
2684
|
console.log(`Environment: ${result.manifest.env}`);
|
|
2639
2685
|
console.log(`Bundles: ${result.stats.bundleCount}`);
|
|
@@ -2642,15 +2688,15 @@ export function printBundleStats(result: BundleResult): void {
|
|
|
2642
2688
|
console.log(`Build Time: ${result.stats.buildTime.toFixed(0)}ms`);
|
|
2643
2689
|
console.log("");
|
|
2644
2690
|
|
|
2645
|
-
// 각 번들 정보
|
|
2646
|
-
for (const output of result.outputs) {
|
|
2647
|
-
console.log(
|
|
2648
|
-
` ${output.routeId}: ${formatSize(output.size)} (gzip: ${formatSize(output.gzipSize)})`
|
|
2649
|
-
);
|
|
2650
|
-
}
|
|
2651
|
-
if (partialCount > 0) {
|
|
2652
|
-
console.log(` Partials: ${partialCount}`);
|
|
2653
|
-
}
|
|
2691
|
+
// 각 번들 정보
|
|
2692
|
+
for (const output of result.outputs) {
|
|
2693
|
+
console.log(
|
|
2694
|
+
` ${output.routeId}: ${formatSize(output.size)} (gzip: ${formatSize(output.gzipSize)})`
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
if (partialCount > 0) {
|
|
2698
|
+
console.log(` Partials: ${partialCount}`);
|
|
2699
|
+
}
|
|
2654
2700
|
|
|
2655
2701
|
if (result.errors.length > 0) {
|
|
2656
2702
|
console.log("\n⚠️ Errors:");
|