@mandujs/core 0.54.10 → 0.54.11
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 +200 -200
- 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 +120 -88
- package/src/bundler/build.ts +511 -511
- 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,
|
|
@@ -191,46 +191,46 @@ async function scanIslandFiles(routes: RouteSpec[], rootDir: string): Promise<Is
|
|
|
191
191
|
*
|
|
192
192
|
* @internal
|
|
193
193
|
*/
|
|
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;
|
|
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;
|
|
234
234
|
|
|
235
235
|
/**
|
|
236
236
|
* Test-only accessor for the hydrated-routes filter. Mirrors the rationale
|
|
@@ -239,9 +239,9 @@ export const _testOnly_scanPartialFiles = scanPartialFiles;
|
|
|
239
239
|
*
|
|
240
240
|
* @internal
|
|
241
241
|
*/
|
|
242
|
-
export const _testOnly_getHydratedRoutes = getHydratedRoutes;
|
|
243
|
-
export const _testOnly_getHydrationRoutesMissingClientModule =
|
|
244
|
-
getHydrationRoutesMissingClientModule;
|
|
242
|
+
export const _testOnly_getHydratedRoutes = getHydratedRoutes;
|
|
243
|
+
export const _testOnly_getHydrationRoutesMissingClientModule =
|
|
244
|
+
getHydrationRoutesMissingClientModule;
|
|
245
245
|
|
|
246
246
|
/**
|
|
247
247
|
* Issue #240 Phase 2 — collect every file the React Compiler plugin
|
|
@@ -260,42 +260,42 @@ export const _testOnly_getHydrationRoutesMissingClientModule =
|
|
|
260
260
|
* Paths are absolute and deduplicated. Returns `[]` when the manifest
|
|
261
261
|
* has no hydrated routes.
|
|
262
262
|
*/
|
|
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
|
-
}
|
|
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
|
+
}
|
|
294
294
|
|
|
295
295
|
/** Build a single per-island bundle. */
|
|
296
|
-
async function buildPerIslandBundle(
|
|
297
|
-
entry: IslandFileEntry, outDir: string, options: BundlerOptions
|
|
298
|
-
): Promise<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> {
|
|
296
|
+
async function buildPerIslandBundle(
|
|
297
|
+
entry: IslandFileEntry, outDir: string, options: BundlerOptions
|
|
298
|
+
): Promise<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> {
|
|
299
299
|
const entryPath = path.join(outDir, `_entry_island_${entry.name}.js`);
|
|
300
300
|
const outputName = `${entry.name}.island.js`;
|
|
301
301
|
// Phase 7.1 B-1/B-4: wire Bun's native React Fast Refresh transform +
|
|
@@ -324,66 +324,66 @@ async function buildPerIslandBundle(
|
|
|
324
324
|
return { name: entry.name, js: `/.mandu/client/${outputName}`, route: entry.routeId, priority: entry.priority };
|
|
325
325
|
} catch (error) {
|
|
326
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
|
-
if (!result.success) {
|
|
366
|
-
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
367
|
-
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 })\`.`);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const outputPath = path.join(outDir, outputName);
|
|
371
|
-
const outputFile = Bun.file(outputPath);
|
|
372
|
-
const content = await outputFile.text();
|
|
373
|
-
const gzipped = Bun.gzipSync(Buffer.from(content));
|
|
374
|
-
|
|
375
|
-
return {
|
|
376
|
-
name: entry.name,
|
|
377
|
-
js: `/.mandu/client/${outputName}`,
|
|
378
|
-
priority: entry.priority,
|
|
379
|
-
size: outputFile.size,
|
|
380
|
-
gzipSize: gzipped.length,
|
|
381
|
-
};
|
|
382
|
-
} catch (error) {
|
|
383
|
-
await fs.unlink(entryPath).catch(() => {});
|
|
384
|
-
throw error;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
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
|
+
if (!result.success) {
|
|
366
|
+
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
367
|
+
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 })\`.`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const outputPath = path.join(outDir, outputName);
|
|
371
|
+
const outputFile = Bun.file(outputPath);
|
|
372
|
+
const content = await outputFile.text();
|
|
373
|
+
const gzipped = Bun.gzipSync(Buffer.from(content));
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
name: entry.name,
|
|
377
|
+
js: `/.mandu/client/${outputName}`,
|
|
378
|
+
priority: entry.priority,
|
|
379
|
+
size: outputFile.size,
|
|
380
|
+
gzipSize: gzipped.length,
|
|
381
|
+
};
|
|
382
|
+
} catch (error) {
|
|
383
|
+
await fs.unlink(entryPath).catch(() => {});
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
387
|
|
|
388
388
|
/**
|
|
389
389
|
* 빈 매니페스트 생성
|
|
@@ -407,23 +407,23 @@ function createEmptyManifest(env: "development" | "production"): BundleManifest
|
|
|
407
407
|
/**
|
|
408
408
|
* Hydration이 필요한 라우트 필터링
|
|
409
409
|
*/
|
|
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
|
-
}
|
|
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
|
+
}
|
|
427
427
|
|
|
428
428
|
const REACT_SHIM_EXPORTS = [
|
|
429
429
|
"Activity",
|
|
@@ -518,31 +518,31 @@ function generateRuntimeSource(): string {
|
|
|
518
518
|
import React, { useState, useEffect, Component } from 'react';
|
|
519
519
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
520
520
|
|
|
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 || {};
|
|
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 || {};
|
|
546
546
|
|
|
547
547
|
/**
|
|
548
548
|
* Error Boundary 컴포넌트 (Class Component)
|
|
@@ -756,16 +756,16 @@ async function loadAndHydrate(element, src) {
|
|
|
756
756
|
const island = module.default;
|
|
757
757
|
let data = getServerData(id);
|
|
758
758
|
|
|
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'));
|
|
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'));
|
|
769
769
|
} catch (e) {
|
|
770
770
|
console.warn('[Mandu] Failed to parse data-props fallback:', e);
|
|
771
771
|
}
|
|
@@ -1439,63 +1439,63 @@ async function buildRouterRuntime(
|
|
|
1439
1439
|
* - Runtime이 dynamic import로 로드
|
|
1440
1440
|
* - 등록/초기화 코드 없음
|
|
1441
1441
|
*/
|
|
1442
|
-
function generateIslandEntry(routeId: string, clientModulePath: string): string {
|
|
1443
|
-
// Windows 경로의 백슬래시를 슬래시로 변환 (JS escape 문제 방지)
|
|
1444
|
-
const normalizedPath = clientModulePath.replace(/\\/g, "/");
|
|
1445
|
-
return `
|
|
1442
|
+
function generateIslandEntry(routeId: string, clientModulePath: string): string {
|
|
1443
|
+
// Windows 경로의 백슬래시를 슬래시로 변환 (JS escape 문제 방지)
|
|
1444
|
+
const normalizedPath = clientModulePath.replace(/\\/g, "/");
|
|
1445
|
+
return `
|
|
1446
1446
|
/**
|
|
1447
1447
|
* Mandu Island: ${routeId} (Generated)
|
|
1448
1448
|
* Pure export - no side effects
|
|
1449
1449
|
*/
|
|
1450
1450
|
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
|
-
*/
|
|
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
|
+
*/
|
|
1499
1499
|
async function buildRuntime(
|
|
1500
1500
|
outDir: string,
|
|
1501
1501
|
options: BundlerOptions
|
|
@@ -1776,9 +1776,9 @@ async function buildVendorShims(
|
|
|
1776
1776
|
}
|
|
1777
1777
|
}
|
|
1778
1778
|
|
|
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 }> => {
|
|
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 }> => {
|
|
1782
1782
|
const srcPath = path.join(outDir, `${shim.name}.src.js`);
|
|
1783
1783
|
const outputName = `${shim.name}.js`;
|
|
1784
1784
|
|
|
@@ -1814,14 +1814,14 @@ async function buildVendorShims(
|
|
|
1814
1814
|
|
|
1815
1815
|
await fs.unlink(srcPath).catch(() => {});
|
|
1816
1816
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
1825
1825
|
|
|
1826
1826
|
return {
|
|
1827
1827
|
key: shim.key,
|
|
@@ -1831,13 +1831,13 @@ async function buildVendorShims(
|
|
|
1831
1831
|
};
|
|
1832
1832
|
} catch (error) {
|
|
1833
1833
|
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
|
-
};
|
|
1834
|
+
return {
|
|
1835
|
+
key: shim.key,
|
|
1836
|
+
cacheId: shim.cacheId,
|
|
1837
|
+
error: `[${shim.name}] ${String(error)}\n ${vendorShimFailureHint(shim.name)}`,
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
1841
|
|
|
1842
1842
|
const buildResults = await Promise.all(shims.map((shim) => buildShim(shim)));
|
|
1843
1843
|
const writeEntries: VendorCacheWriteEntry[] = [];
|
|
@@ -1880,14 +1880,14 @@ async function buildVendorShims(
|
|
|
1880
1880
|
fastRefreshRuntime: results.fastRefreshRuntime,
|
|
1881
1881
|
errors,
|
|
1882
1882
|
};
|
|
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
|
-
}
|
|
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
|
+
}
|
|
1891
1891
|
|
|
1892
1892
|
/**
|
|
1893
1893
|
* 단일 Island 번들 빌드
|
|
@@ -1979,16 +1979,16 @@ async function buildIsland(
|
|
|
1979
1979
|
/**
|
|
1980
1980
|
* 번들 매니페스트 생성
|
|
1981
1981
|
*/
|
|
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 {
|
|
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 {
|
|
1992
1992
|
const bundles: BundleManifest["bundles"] = {};
|
|
1993
1993
|
|
|
1994
1994
|
for (const output of outputs) {
|
|
@@ -2004,27 +2004,27 @@ function createBundleManifest(
|
|
|
2004
2004
|
|
|
2005
2005
|
// Per-island bundles (code splitting)
|
|
2006
2006
|
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,
|
|
2007
|
+
if (islandBundles && islandBundles.length > 0) {
|
|
2008
|
+
islands = {};
|
|
2009
|
+
for (const ib of islandBundles) {
|
|
2010
|
+
islands[ib.name] = {
|
|
2011
|
+
js: ib.js,
|
|
2012
2012
|
route: ib.route,
|
|
2013
2013
|
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
|
-
}
|
|
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
|
+
}
|
|
2028
2028
|
|
|
2029
2029
|
// Phase 7.1 B-2: expose Fast Refresh dev bundles so the HTML
|
|
2030
2030
|
// preamble can inject a dynamic import pointing at them. Only
|
|
@@ -2040,11 +2040,11 @@ function createBundleManifest(
|
|
|
2040
2040
|
return {
|
|
2041
2041
|
version: 1,
|
|
2042
2042
|
buildTime: new Date().toISOString(),
|
|
2043
|
-
env,
|
|
2044
|
-
bundles,
|
|
2045
|
-
...(islands ? { islands } : {}),
|
|
2046
|
-
...(partials ? { partials } : {}),
|
|
2047
|
-
shared: {
|
|
2043
|
+
env,
|
|
2044
|
+
bundles,
|
|
2045
|
+
...(islands ? { islands } : {}),
|
|
2046
|
+
...(partials ? { partials } : {}),
|
|
2047
|
+
shared: {
|
|
2048
2048
|
runtime: runtimePath,
|
|
2049
2049
|
vendor: vendorResult.react, // primary vendor for backwards compatibility
|
|
2050
2050
|
router: routerPath, // Client-side Router
|
|
@@ -2065,32 +2065,32 @@ function createBundleManifest(
|
|
|
2065
2065
|
/**
|
|
2066
2066
|
* 번들 통계 계산
|
|
2067
2067
|
*/
|
|
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 };
|
|
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 };
|
|
2083
2083
|
}
|
|
2084
2084
|
}
|
|
2085
2085
|
|
|
2086
2086
|
return {
|
|
2087
2087
|
totalSize,
|
|
2088
|
-
totalGzipSize,
|
|
2089
|
-
largestBundle,
|
|
2090
|
-
buildTime: performance.now() - startTime,
|
|
2091
|
-
bundleCount: outputs.length + extraOutputs.length,
|
|
2092
|
-
};
|
|
2093
|
-
}
|
|
2088
|
+
totalGzipSize,
|
|
2089
|
+
largestBundle,
|
|
2090
|
+
buildTime: performance.now() - startTime,
|
|
2091
|
+
bundleCount: outputs.length + extraOutputs.length,
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
2094
|
|
|
2095
2095
|
/**
|
|
2096
2096
|
* 클라이언트 번들 빌드
|
|
@@ -2134,47 +2134,47 @@ export async function buildClientBundles(
|
|
|
2134
2134
|
errors.push(`onBundleComplete[${e.source}]: ${e.error.message}`);
|
|
2135
2135
|
}
|
|
2136
2136
|
};
|
|
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. 출력 디렉토리 생성 (항상 필요 - 매니페스트 저장용)
|
|
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. 출력 디렉토리 생성 (항상 필요 - 매니페스트 저장용)
|
|
2170
2170
|
const outDir = resolveClientOutDir(rootDir, options.outDir);
|
|
2171
2171
|
await fs.mkdir(outDir, { recursive: true });
|
|
2172
2172
|
|
|
2173
2173
|
// Hydration 라우트가 없어도 빈 매니페스트를 저장해야 함
|
|
2174
2174
|
// (이전 빌드의 stale 매니페스트 참조 방지)
|
|
2175
|
-
if (hydratedRoutes.length === 0 && partialFiles.length === 0) {
|
|
2175
|
+
if (hydratedRoutes.length === 0 && partialFiles.length === 0) {
|
|
2176
2176
|
// #185: skipFrameworkBundles 모드에서는 기존 manifest를 그대로 유지 (devtools 재빌드도 스킵)
|
|
2177
|
-
if (options.skipFrameworkBundles && errors.length === 0) {
|
|
2177
|
+
if (options.skipFrameworkBundles && errors.length === 0) {
|
|
2178
2178
|
const manifestPath = path.join(rootDir, ".mandu/manifest.json");
|
|
2179
2179
|
try {
|
|
2180
2180
|
const manifestRaw = await fs.readFile(manifestPath, "utf-8");
|
|
@@ -2225,11 +2225,11 @@ export async function buildClientBundles(
|
|
|
2225
2225
|
path.join(rootDir, ".mandu/manifest.json"),
|
|
2226
2226
|
JSON.stringify(emptyManifest, null, 2)
|
|
2227
2227
|
);
|
|
2228
|
-
return {
|
|
2229
|
-
success: errors.length === 0,
|
|
2230
|
-
outputs: [],
|
|
2231
|
-
errors,
|
|
2232
|
-
manifest: emptyManifest,
|
|
2228
|
+
return {
|
|
2229
|
+
success: errors.length === 0,
|
|
2230
|
+
outputs: [],
|
|
2231
|
+
errors,
|
|
2232
|
+
manifest: emptyManifest,
|
|
2233
2233
|
stats: {
|
|
2234
2234
|
totalSize: 0,
|
|
2235
2235
|
totalGzipSize: 0,
|
|
@@ -2268,14 +2268,14 @@ export async function buildClientBundles(
|
|
|
2268
2268
|
return buildClientBundles(manifest, rootDir, { ...options, targetRouteIds: undefined });
|
|
2269
2269
|
}
|
|
2270
2270
|
|
|
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;
|
|
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;
|
|
2279
2279
|
} else {
|
|
2280
2280
|
const route = targetRoutes.find((r) => r.id === output.routeId);
|
|
2281
2281
|
const hydration = route ? getRouteHydration(route) : null;
|
|
@@ -2287,11 +2287,11 @@ export async function buildClientBundles(
|
|
|
2287
2287
|
}
|
|
2288
2288
|
}
|
|
2289
2289
|
|
|
2290
|
-
await fs.writeFile(
|
|
2291
|
-
path.join(rootDir, ".mandu/manifest.json"),
|
|
2292
|
-
JSON.stringify(existingManifest, null, 2)
|
|
2293
|
-
);
|
|
2294
|
-
}
|
|
2290
|
+
await fs.writeFile(
|
|
2291
|
+
path.join(rootDir, ".mandu/manifest.json"),
|
|
2292
|
+
JSON.stringify(existingManifest, null, 2)
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
2295
|
// When all builds failed, do NOT overwrite manifest — keep previous good state
|
|
2296
2296
|
|
|
2297
2297
|
const stats = calculateStats(outputs, startTime);
|
|
@@ -2327,13 +2327,13 @@ export async function buildClientBundles(
|
|
|
2327
2327
|
"[Mandu] Existing manifest missing required fields (shared/bundles), falling back to full build",
|
|
2328
2328
|
);
|
|
2329
2329
|
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 번들은 스킵)
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
for (const routeId of invalidClientRouteIds) {
|
|
2333
|
+
delete existingManifest.bundles[routeId];
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// Pre-build validation + 병렬 island 빌드 (framework 번들은 스킵)
|
|
2337
2337
|
for (const route of hydratedRoutes) {
|
|
2338
2338
|
if (!route.clientModule) continue;
|
|
2339
2339
|
const clientModulePath = path.join(rootDir, route.clientModule);
|
|
@@ -2398,62 +2398,62 @@ export async function buildClientBundles(
|
|
|
2398
2398
|
};
|
|
2399
2399
|
}
|
|
2400
2400
|
}
|
|
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,
|
|
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,
|
|
2406
2406
|
route: ib.route,
|
|
2407
2407
|
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
|
-
}
|
|
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
|
+
}
|
|
2457
2457
|
|
|
2458
2458
|
// 3-4. Runtime, Router, Vendor, DevTools 번들 병렬 빌드 (서로 독립적)
|
|
2459
2459
|
const isDev = env === "development";
|
|
@@ -2539,11 +2539,11 @@ export async function buildClientBundles(
|
|
|
2539
2539
|
const islandFiles = await scanIslandFiles(hydratedRoutes, rootDir);
|
|
2540
2540
|
const islandBundles: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> = [];
|
|
2541
2541
|
|
|
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);
|
|
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);
|
|
2547
2547
|
} catch (error) {
|
|
2548
2548
|
errors.push(`[island:${entry.name}] ${String(error)}`);
|
|
2549
2549
|
return null;
|
|
@@ -2551,38 +2551,38 @@ export async function buildClientBundles(
|
|
|
2551
2551
|
})
|
|
2552
2552
|
);
|
|
2553
2553
|
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
|
-
);
|
|
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
|
+
);
|
|
2586
2586
|
|
|
2587
2587
|
await fs.writeFile(
|
|
2588
2588
|
path.join(rootDir, ".mandu/manifest.json"),
|
|
@@ -2590,15 +2590,15 @@ export async function buildClientBundles(
|
|
|
2590
2590
|
);
|
|
2591
2591
|
|
|
2592
2592
|
// 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
|
-
);
|
|
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
|
+
);
|
|
2602
2602
|
|
|
2603
2603
|
// Phase 18.τ — fire onBundleComplete(stats) before return.
|
|
2604
2604
|
await fireOnBundleComplete(stats);
|
|
@@ -2626,14 +2626,14 @@ export function formatSize(bytes: number): string {
|
|
|
2626
2626
|
* 번들 결과 요약 출력
|
|
2627
2627
|
*/
|
|
2628
2628
|
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
|
-
}
|
|
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
|
+
}
|
|
2637
2637
|
|
|
2638
2638
|
console.log(`Environment: ${result.manifest.env}`);
|
|
2639
2639
|
console.log(`Bundles: ${result.stats.bundleCount}`);
|
|
@@ -2642,15 +2642,15 @@ export function printBundleStats(result: BundleResult): void {
|
|
|
2642
2642
|
console.log(`Build Time: ${result.stats.buildTime.toFixed(0)}ms`);
|
|
2643
2643
|
console.log("");
|
|
2644
2644
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
2654
2654
|
|
|
2655
2655
|
if (result.errors.length > 0) {
|
|
2656
2656
|
console.log("\n⚠️ Errors:");
|