@mandujs/core 0.20.10 → 0.22.0
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/README.md +2 -1
- package/package.json +28 -3
- package/src/auth/__tests__/login.test.ts +419 -0
- package/src/auth/__tests__/password.test.ts +122 -0
- package/src/auth/__tests__/reset.test.ts +296 -0
- package/src/auth/__tests__/tokens.test.ts +274 -0
- package/src/auth/__tests__/verification.test.ts +274 -0
- package/src/auth/index.ts +76 -0
- package/src/auth/login.ts +225 -0
- package/src/auth/password.ts +120 -0
- package/src/auth/reset.ts +243 -0
- package/src/auth/tokens.ts +612 -0
- package/src/auth/verification.ts +253 -0
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
- package/src/bundler/__tests__/cold-start.test.ts +504 -0
- package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
- package/src/bundler/__tests__/extended-watch.test.ts +710 -0
- package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
- package/src/bundler/__tests__/hdr.test.ts +353 -0
- package/src/bundler/__tests__/hmr-client.test.ts +532 -0
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
- package/src/bundler/build.test.ts +8 -1
- package/src/bundler/build.ts +495 -37
- package/src/bundler/css.ts +326 -323
- package/src/bundler/dev.ts +1671 -80
- package/src/bundler/fast-refresh-plugin.ts +307 -0
- package/src/bundler/hmr-types.ts +252 -0
- package/src/bundler/manifest-schema.ts +301 -0
- package/src/bundler/safe-build.test.ts +128 -0
- package/src/bundler/safe-build.ts +77 -0
- package/src/bundler/scenario-matrix.ts +229 -0
- package/src/bundler/types.ts +19 -0
- package/src/bundler/vendor-cache-types.ts +130 -0
- package/src/bundler/vendor-cache.ts +526 -0
- package/src/client/router.ts +214 -56
- package/src/config/validate.ts +1 -0
- package/src/db/__tests__/db.test.ts +485 -0
- package/src/db/index.ts +513 -0
- package/src/db/migrations/__tests__/runner.test.ts +661 -0
- package/src/db/migrations/history-table.ts +345 -0
- package/src/db/migrations/lock.ts +269 -0
- package/src/db/migrations/runner.ts +633 -0
- package/src/desktop/__tests__/smoke.test.ts +100 -0
- package/src/desktop/__tests__/window.test.ts +172 -0
- package/src/desktop/__tests__/worker.test.ts +266 -0
- package/src/desktop/index.ts +43 -0
- package/src/desktop/types.ts +158 -0
- package/src/desktop/window.ts +492 -0
- package/src/desktop/worker.ts +180 -0
- package/src/devtools/ai/mcp-connector.ts +18 -16
- package/src/devtools/client/components/mandu-character.tsx +4 -1
- package/src/devtools/client/components/panel/panel-container.tsx +20 -5
- package/src/email/__tests__/email.test.ts +355 -0
- package/src/email/index.ts +282 -0
- package/src/email/resend.ts +163 -0
- package/src/email/smtp.ts +64 -0
- package/src/filling/__tests__/session-sqlite.test.ts +454 -0
- package/src/filling/context.ts +72 -78
- package/src/filling/cookie-codec.ts +299 -0
- package/src/filling/deps.ts +25 -1
- package/src/filling/filling.ts +28 -3
- package/src/filling/session-sqlite.ts +617 -0
- package/src/filling/session.ts +265 -216
- package/src/guard/decision-memory.test.ts +52 -22
- package/src/id/__tests__/id.test.ts +120 -0
- package/src/id/index.ts +105 -0
- package/src/kitchen/index.ts +2 -2
- package/src/kitchen/kitchen-handler.ts +86 -0
- package/src/kitchen/stream/activity-sse.ts +2 -1
- package/src/middleware/csrf.ts +328 -0
- package/src/middleware/index.ts +40 -0
- package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
- package/src/middleware/oauth/index.ts +505 -0
- package/src/middleware/oauth/providers.ts +115 -0
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
- package/src/middleware/rate-limit/index.ts +522 -0
- package/src/middleware/rate-limit/sqlite-store.ts +382 -0
- package/src/middleware/secure/__tests__/secure.test.ts +360 -0
- package/src/middleware/secure/csp.ts +193 -0
- package/src/middleware/secure/index.ts +417 -0
- package/src/middleware/session.ts +174 -0
- package/src/observability/event-bus.ts +81 -79
- package/src/paths.ts +37 -0
- package/src/perf/hmr-markers.ts +215 -0
- package/src/perf/index.ts +104 -0
- package/src/resource/__tests__/generator.test.ts +603 -2
- package/src/resource/ddl/__tests__/diff.test.ts +639 -0
- package/src/resource/ddl/__tests__/emit.test.ts +799 -0
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
- package/src/resource/ddl/diff.ts +392 -0
- package/src/resource/ddl/emit.ts +548 -0
- package/src/resource/ddl/persistence-types.ts +218 -0
- package/src/resource/ddl/snapshot.ts +447 -0
- package/src/resource/ddl/type-map.ts +223 -0
- package/src/resource/ddl/types.ts +232 -0
- package/src/resource/generator-repo.ts +610 -0
- package/src/resource/generator-schema.ts +476 -0
- package/src/resource/generator.ts +117 -1
- package/src/resource/index.ts +17 -1
- package/src/resource/schema.ts +30 -0
- package/src/router/fs-scanner.ts +3 -0
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
- package/src/runtime/__tests__/hdr-client.test.ts +223 -0
- package/src/runtime/__tests__/http-errors.test.ts +117 -0
- package/src/runtime/__tests__/not-found.test.ts +152 -0
- package/src/runtime/boundary.tsx +21 -1
- package/src/runtime/fast-refresh-runtime.ts +322 -0
- package/src/runtime/fast-refresh-types.ts +128 -0
- package/src/runtime/hmr-client.ts +409 -0
- package/src/runtime/http-errors.ts +113 -0
- package/src/runtime/index.ts +6 -0
- package/src/runtime/logger.ts +678 -677
- package/src/runtime/not-found.ts +93 -0
- package/src/runtime/redirect.ts +133 -0
- package/src/runtime/server.ts +679 -23
- package/src/runtime/ssr.ts +340 -10
- package/src/runtime/streaming-ssr.ts +222 -19
- package/src/scheduler/__tests__/scheduler.test.ts +514 -0
- package/src/scheduler/index.ts +343 -0
- package/src/storage/s3/__tests__/s3.test.ts +479 -0
- package/src/storage/s3/index.ts +412 -0
- package/src/testing/index.ts +247 -189
package/src/bundler/build.ts
CHANGED
|
@@ -14,15 +14,52 @@ import type {
|
|
|
14
14
|
IslandFileEntry,
|
|
15
15
|
} from "./types";
|
|
16
16
|
import { HYDRATION } from "../constants";
|
|
17
|
+
import { safeBuild } from "./safe-build";
|
|
18
|
+
import { fastRefreshPlugin } from "./fast-refresh-plugin";
|
|
19
|
+
import { mark, measure } from "../perf";
|
|
20
|
+
import { HMR_PERF } from "../perf/hmr-markers";
|
|
21
|
+
import {
|
|
22
|
+
readVendorCache,
|
|
23
|
+
writeVendorCache,
|
|
24
|
+
restoreVendorCache,
|
|
25
|
+
resolveVendorCacheKeys,
|
|
26
|
+
type VendorCacheKeyInput,
|
|
27
|
+
type VendorCacheWriteEntry,
|
|
28
|
+
} from "./vendor-cache";
|
|
17
29
|
import path from "path";
|
|
18
30
|
import fs from "fs/promises";
|
|
19
31
|
|
|
20
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Scan for *.island.tsx / *.island.ts files across hydrated route directories.
|
|
34
|
+
*
|
|
35
|
+
* Phase 7.1 R1 Agent C — per-island conditional skip defence.
|
|
36
|
+
*
|
|
37
|
+
* Callers (the two `buildClientBundles` paths at L1643 and L1817) feed us
|
|
38
|
+
* `hydratedRoutes` (from `getHydratedRoutes`), which already filters for
|
|
39
|
+
* `route.kind === "page" && route.clientModule && needsHydration(route)`.
|
|
40
|
+
* We re-assert the `needsHydration` predicate here for two reasons:
|
|
41
|
+
*
|
|
42
|
+
* 1. Defence-in-depth — if a future caller forgets the filter, we still
|
|
43
|
+
* skip routes with `hydration.strategy === "none"`. Each skipped route
|
|
44
|
+
* saves one `fs.readdir` + O(files) regex matches (~1-2 ms on Windows
|
|
45
|
+
* NTFS per skipped dir, per diagnostic R0.3).
|
|
46
|
+
*
|
|
47
|
+
* 2. Cold-start regression guard — the R0 → R3 F breakdown attributes
|
|
48
|
+
* +40-80 ms of cold start to per-island splitting (commit b503c36).
|
|
49
|
+
* The unit test `per-island-scan-skips-non-hydrated` pins this so a
|
|
50
|
+
* refactor cannot silently re-introduce the scan overhead for routes
|
|
51
|
+
* that opt out of hydration.
|
|
52
|
+
*/
|
|
21
53
|
async function scanIslandFiles(routes: RouteSpec[], rootDir: string): Promise<IslandFileEntry[]> {
|
|
22
54
|
const entries: IslandFileEntry[] = [];
|
|
23
55
|
const seenDirs = new Set<string>();
|
|
24
56
|
|
|
25
57
|
for (const route of routes) {
|
|
58
|
+
// Defensive guard — see the block comment above for rationale. Without
|
|
59
|
+
// this, a hypothetical caller passing `manifest.routes` directly would
|
|
60
|
+
// readdir every page route, including pure-SSR ones.
|
|
61
|
+
if (!needsHydration(route)) continue;
|
|
62
|
+
|
|
26
63
|
const dir = path.dirname(path.join(rootDir, route.componentModule ?? route.module));
|
|
27
64
|
if (seenDirs.has(dir)) continue;
|
|
28
65
|
seenDirs.add(dir);
|
|
@@ -45,21 +82,46 @@ async function scanIslandFiles(routes: RouteSpec[], rootDir: string): Promise<Is
|
|
|
45
82
|
return entries;
|
|
46
83
|
}
|
|
47
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Test-only accessor for the island-file scanner. Allows the cold-start
|
|
87
|
+
* test suite (`__tests__/cold-start.test.ts`) to assert the per-island
|
|
88
|
+
* conditional skip without having to spin up a full `buildClientBundles`
|
|
89
|
+
* invocation (which would also run `safeBuild`).
|
|
90
|
+
*
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
export const _testOnly_scanIslandFiles = scanIslandFiles;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Test-only accessor for the hydrated-routes filter. Mirrors the rationale
|
|
97
|
+
* above — lets tests verify that `getHydratedRoutes` really does drop
|
|
98
|
+
* `hydration.strategy: "none"` routes without rebuilding its logic.
|
|
99
|
+
*
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
export const _testOnly_getHydratedRoutes = getHydratedRoutes;
|
|
103
|
+
|
|
48
104
|
/** Build a single per-island bundle. */
|
|
49
105
|
async function buildPerIslandBundle(
|
|
50
106
|
entry: IslandFileEntry, outDir: string, options: BundlerOptions
|
|
51
107
|
): Promise<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> {
|
|
52
108
|
const entryPath = path.join(outDir, `_entry_island_${entry.name}.js`);
|
|
53
109
|
const outputName = `${entry.name}.island.js`;
|
|
110
|
+
// Phase 7.1 B-1/B-4: wire Bun's native React Fast Refresh transform +
|
|
111
|
+
// Mandu's boundary injection plugin — but only in dev. Production
|
|
112
|
+
// bundles stay clean of `$RefreshReg$` / `$RefreshSig$` stubs.
|
|
113
|
+
const isDev = (options.minify ?? process.env.NODE_ENV === "production") === false;
|
|
54
114
|
try {
|
|
55
115
|
await Bun.write(entryPath, generateIslandEntry(entry.name, entry.filePath));
|
|
56
|
-
const result = await
|
|
116
|
+
const result = await safeBuild({
|
|
57
117
|
entrypoints: [entryPath],
|
|
58
118
|
outdir: outDir,
|
|
59
119
|
naming: outputName,
|
|
60
120
|
minify: options.minify ?? process.env.NODE_ENV === "production",
|
|
61
121
|
sourcemap: options.sourcemap ? "external" : "none",
|
|
62
122
|
target: "browser",
|
|
123
|
+
...(isDev ? { reactFastRefresh: true } : {}),
|
|
124
|
+
plugins: isDev ? [fastRefreshPlugin()] : [],
|
|
63
125
|
external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
|
|
64
126
|
define: { "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"), ...options.define },
|
|
65
127
|
});
|
|
@@ -971,7 +1033,7 @@ if (typeof window !== 'undefined') {
|
|
|
971
1033
|
try {
|
|
972
1034
|
await Bun.write(srcPath, source);
|
|
973
1035
|
|
|
974
|
-
const result = await
|
|
1036
|
+
const result = await safeBuild({
|
|
975
1037
|
entrypoints: [srcPath],
|
|
976
1038
|
outdir: outDir,
|
|
977
1039
|
naming: outputName,
|
|
@@ -1025,7 +1087,7 @@ async function buildRouterRuntime(
|
|
|
1025
1087
|
try {
|
|
1026
1088
|
await Bun.write(routerPath, generateRouterRuntimeSource());
|
|
1027
1089
|
|
|
1028
|
-
const result = await
|
|
1090
|
+
const result = await safeBuild({
|
|
1029
1091
|
entrypoints: [routerPath],
|
|
1030
1092
|
outdir: outDir,
|
|
1031
1093
|
naming: outputName,
|
|
@@ -1100,7 +1162,7 @@ async function buildRuntime(
|
|
|
1100
1162
|
await Bun.write(runtimePath, generateRuntimeSource());
|
|
1101
1163
|
|
|
1102
1164
|
// 빌드
|
|
1103
|
-
const result = await
|
|
1165
|
+
const result = await safeBuild({
|
|
1104
1166
|
entrypoints: [runtimePath],
|
|
1105
1167
|
outdir: outDir,
|
|
1106
1168
|
naming: outputName,
|
|
@@ -1161,38 +1223,216 @@ interface VendorBuildResult {
|
|
|
1161
1223
|
reactDomClient: string;
|
|
1162
1224
|
jsxRuntime: string;
|
|
1163
1225
|
jsxDevRuntime: string;
|
|
1226
|
+
/**
|
|
1227
|
+
* Phase 7.1 B-2: bundled `react-refresh/runtime` — emitted ONLY in
|
|
1228
|
+
* dev mode. Empty string in production. Consumed by the HTML
|
|
1229
|
+
* preamble (`bundler/dev.ts`) via a dynamic import.
|
|
1230
|
+
*/
|
|
1231
|
+
reactRefreshRuntime: string;
|
|
1232
|
+
/**
|
|
1233
|
+
* Phase 7.1 B-2: Mandu's `__MANDU_HMR__` glue module — also dev-only.
|
|
1234
|
+
* Imported once by the HTML preamble which then calls `installGlobal`
|
|
1235
|
+
* with the already-loaded refresh runtime.
|
|
1236
|
+
*/
|
|
1237
|
+
fastRefreshRuntime: string;
|
|
1164
1238
|
errors: string[];
|
|
1165
1239
|
}
|
|
1166
1240
|
|
|
1241
|
+
/**
|
|
1242
|
+
* Phase 7.1 B-2 — source generator for the `react-refresh/runtime` shim.
|
|
1243
|
+
*
|
|
1244
|
+
* The shim simply re-exports the upstream module under a default export
|
|
1245
|
+
* that `fast-refresh-runtime.ts`'s `installGlobal({ runtimeImport })`
|
|
1246
|
+
* can consume. Mirrors the pattern Vite uses for its `/@react-refresh`
|
|
1247
|
+
* endpoint.
|
|
1248
|
+
*/
|
|
1249
|
+
function generateReactRefreshRuntimeShimSource(): string {
|
|
1250
|
+
return `
|
|
1251
|
+
/**
|
|
1252
|
+
* Mandu React Refresh Runtime Shim (Generated, dev-only)
|
|
1253
|
+
* Re-exports the upstream react-refresh/runtime so the bundler can
|
|
1254
|
+
* pre-bundle it without leaking the CommonJS entry into the app graph.
|
|
1255
|
+
*/
|
|
1256
|
+
import * as Runtime from 'react-refresh/runtime';
|
|
1257
|
+
export const injectIntoGlobalHook = Runtime.injectIntoGlobalHook;
|
|
1258
|
+
export const register = Runtime.register;
|
|
1259
|
+
export const createSignatureFunctionForTransform = Runtime.createSignatureFunctionForTransform;
|
|
1260
|
+
export const performReactRefresh = Runtime.performReactRefresh;
|
|
1261
|
+
export default {
|
|
1262
|
+
injectIntoGlobalHook,
|
|
1263
|
+
register,
|
|
1264
|
+
createSignatureFunctionForTransform,
|
|
1265
|
+
performReactRefresh,
|
|
1266
|
+
};
|
|
1267
|
+
`;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Phase 7.1 B-2 — source generator for the Mandu Fast Refresh glue.
|
|
1272
|
+
*
|
|
1273
|
+
* This re-exports `installGlobal` / `manduHMR` / helpers from
|
|
1274
|
+
* `runtime/fast-refresh-runtime.ts`. By routing through a generated
|
|
1275
|
+
* shim we keep the absolute path to core resolved once at build time
|
|
1276
|
+
* (same pattern as `_devtools.js`).
|
|
1277
|
+
*/
|
|
1278
|
+
function generateFastRefreshRuntimeShimSource(): string {
|
|
1279
|
+
const runtimePath = path
|
|
1280
|
+
.resolve(import.meta.dir, "..", "runtime", "fast-refresh-runtime.ts")
|
|
1281
|
+
.replace(/\\/g, "/");
|
|
1282
|
+
return `
|
|
1283
|
+
/**
|
|
1284
|
+
* Mandu Fast Refresh Runtime Shim (Generated, dev-only)
|
|
1285
|
+
* Wires the react-refresh runtime to window.__MANDU_HMR__.
|
|
1286
|
+
*/
|
|
1287
|
+
export * from "${runtimePath}";
|
|
1288
|
+
import { installGlobal } from "${runtimePath}";
|
|
1289
|
+
export { installGlobal };
|
|
1290
|
+
`;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1167
1293
|
/**
|
|
1168
1294
|
* Vendor shim 번들 빌드
|
|
1169
1295
|
* React, ReactDOM, ReactDOMClient를 각각의 shim으로 빌드
|
|
1296
|
+
*
|
|
1297
|
+
* Phase 7.2.S2: caches the built shim outputs on disk under
|
|
1298
|
+
* `.mandu/vendor-cache/` keyed by Bun + React + ReactDOM + react-refresh +
|
|
1299
|
+
* @mandujs/core versions. Warm boots reuse the cached files instead of
|
|
1300
|
+
* re-running Bun.build, eliminating ~80-120 ms from cold start.
|
|
1170
1301
|
*/
|
|
1171
1302
|
async function buildVendorShims(
|
|
1303
|
+
rootDir: string,
|
|
1172
1304
|
outDir: string,
|
|
1173
1305
|
options: BundlerOptions
|
|
1174
1306
|
): Promise<VendorBuildResult> {
|
|
1175
1307
|
const errors: string[] = [];
|
|
1176
|
-
type VendorShimKey =
|
|
1308
|
+
type VendorShimKey =
|
|
1309
|
+
| "react"
|
|
1310
|
+
| "reactDom"
|
|
1311
|
+
| "reactDomClient"
|
|
1312
|
+
| "jsxRuntime"
|
|
1313
|
+
| "jsxDevRuntime"
|
|
1314
|
+
| "reactRefreshRuntime"
|
|
1315
|
+
| "fastRefreshRuntime";
|
|
1177
1316
|
const results: Record<VendorShimKey, string> = {
|
|
1178
1317
|
react: "",
|
|
1179
1318
|
reactDom: "",
|
|
1180
1319
|
reactDomClient: "",
|
|
1181
1320
|
jsxRuntime: "",
|
|
1182
1321
|
jsxDevRuntime: "",
|
|
1322
|
+
reactRefreshRuntime: "",
|
|
1323
|
+
fastRefreshRuntime: "",
|
|
1183
1324
|
};
|
|
1184
1325
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1326
|
+
// Phase 7.1 B-2: dev-only Fast Refresh shims. In production we skip
|
|
1327
|
+
// them entirely so `react-refresh/runtime` is never bundled and the
|
|
1328
|
+
// attack surface / bundle size regressions stay zero for deploys.
|
|
1329
|
+
const isDev =
|
|
1330
|
+
(options.minify ?? process.env.NODE_ENV === "production") === false;
|
|
1331
|
+
|
|
1332
|
+
const shims: Array<{ name: string; source: string; key: VendorShimKey; cacheId: string }> = [
|
|
1333
|
+
{ name: "_react", source: generateReactShimSource(), key: "react", cacheId: "react" },
|
|
1334
|
+
{ name: "_react-dom", source: generateReactDOMShimSource(), key: "reactDom", cacheId: "react-dom" },
|
|
1335
|
+
{ name: "_react-dom-client", source: generateReactDOMClientShimSource(), key: "reactDomClient", cacheId: "react-dom-client" },
|
|
1336
|
+
{ name: "_jsx-runtime", source: generateJsxRuntimeShimSource(), key: "jsxRuntime", cacheId: "jsx-runtime" },
|
|
1337
|
+
{ name: "_jsx-dev-runtime", source: generateJsxDevRuntimeShimSource(), key: "jsxDevRuntime", cacheId: "jsx-dev-runtime" },
|
|
1191
1338
|
];
|
|
1339
|
+
if (isDev) {
|
|
1340
|
+
shims.push(
|
|
1341
|
+
{
|
|
1342
|
+
name: "_vendor-react-refresh",
|
|
1343
|
+
source: generateReactRefreshRuntimeShimSource(),
|
|
1344
|
+
key: "reactRefreshRuntime",
|
|
1345
|
+
cacheId: "react-refresh-runtime",
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
name: "_fast-refresh-runtime",
|
|
1349
|
+
source: generateFastRefreshRuntimeShimSource(),
|
|
1350
|
+
key: "fastRefreshRuntime",
|
|
1351
|
+
cacheId: "fast-refresh-glue",
|
|
1352
|
+
},
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Phase 7.2.S2 — Tier 2 disk cache consultation. Production builds
|
|
1357
|
+
// (non-dev, minified) still rebuild fresh because (a) the shim set is
|
|
1358
|
+
// smaller (no fast-refresh) and (b) production is one-shot — there's no
|
|
1359
|
+
// warm workflow to amortize the cache cost against. Dev warm restarts
|
|
1360
|
+
// are where the cache pays off.
|
|
1361
|
+
//
|
|
1362
|
+
// `MANDU_VENDOR_CACHE=0` disables the cache (escape hatch for debugging
|
|
1363
|
+
// cache-invalidation bugs without touching code).
|
|
1364
|
+
const cacheEnabled = isDev && process.env.MANDU_VENDOR_CACHE !== "0";
|
|
1365
|
+
let cacheKeys: VendorCacheKeyInput | null = null;
|
|
1366
|
+
|
|
1367
|
+
if (cacheEnabled) {
|
|
1368
|
+
try {
|
|
1369
|
+
cacheKeys = await resolveVendorCacheKeys(rootDir);
|
|
1370
|
+
const hitOrMiss = await readVendorCache(rootDir, cacheKeys);
|
|
1371
|
+
|
|
1372
|
+
if (hitOrMiss.kind === "hit") {
|
|
1373
|
+
// Attempt to restore every shim file to outDir.
|
|
1374
|
+
const restored = await restoreVendorCache(
|
|
1375
|
+
rootDir,
|
|
1376
|
+
hitOrMiss.manifest,
|
|
1377
|
+
outDir,
|
|
1378
|
+
);
|
|
1379
|
+
if (restored !== null) {
|
|
1380
|
+
// Map the restored files into the result shape. If a shim is
|
|
1381
|
+
// not in the manifest (eg. an older cache from before we added
|
|
1382
|
+
// fast-refresh) we rebuild just that one — but the simpler
|
|
1383
|
+
// policy is to require the manifest to cover every entry the
|
|
1384
|
+
// current shim list wants, so we only accept hits that include
|
|
1385
|
+
// everything. Otherwise fall through to rebuild.
|
|
1386
|
+
const expected = new Set(shims.map((s) => s.cacheId));
|
|
1387
|
+
const present = new Set(restored.keys());
|
|
1388
|
+
let allPresent = true;
|
|
1389
|
+
for (const id of expected) {
|
|
1390
|
+
if (!present.has(id)) {
|
|
1391
|
+
allPresent = false;
|
|
1392
|
+
break;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (allPresent) {
|
|
1396
|
+
// Populate result + return. Every shim's outputPath references
|
|
1397
|
+
// the freshly-restored file in `outDir`.
|
|
1398
|
+
for (const shim of shims) {
|
|
1399
|
+
const dst = restored.get(shim.cacheId);
|
|
1400
|
+
if (dst) {
|
|
1401
|
+
const fileName = path.basename(dst);
|
|
1402
|
+
results[shim.key] = `/.mandu/client/${fileName}`;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
mark(HMR_PERF.VENDOR_CACHE_HIT);
|
|
1406
|
+
measure(HMR_PERF.VENDOR_CACHE_HIT, HMR_PERF.VENDOR_CACHE_HIT);
|
|
1407
|
+
return {
|
|
1408
|
+
success: true,
|
|
1409
|
+
react: results.react,
|
|
1410
|
+
reactDom: results.reactDom,
|
|
1411
|
+
reactDomClient: results.reactDomClient,
|
|
1412
|
+
jsxRuntime: results.jsxRuntime,
|
|
1413
|
+
jsxDevRuntime: results.jsxDevRuntime,
|
|
1414
|
+
reactRefreshRuntime: results.reactRefreshRuntime,
|
|
1415
|
+
fastRefreshRuntime: results.fastRefreshRuntime,
|
|
1416
|
+
errors,
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
// Restored but missing one of the expected shims — fall through.
|
|
1420
|
+
}
|
|
1421
|
+
// Restore failed — fall through to rebuild.
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Miss / failed restore: record the miss marker for perf logs.
|
|
1425
|
+
mark(HMR_PERF.VENDOR_CACHE_MISS);
|
|
1426
|
+
measure(HMR_PERF.VENDOR_CACHE_MISS, HMR_PERF.VENDOR_CACHE_MISS);
|
|
1427
|
+
} catch {
|
|
1428
|
+
// Any cache failure falls through to the full rebuild path — cache
|
|
1429
|
+
// is strictly an optimisation.
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1192
1432
|
|
|
1193
1433
|
const buildShim = async (
|
|
1194
|
-
shim: { name: string; source: string; key: VendorShimKey }
|
|
1195
|
-
): Promise<{ key: VendorShimKey; outputPath?: string; error?: string }> => {
|
|
1434
|
+
shim: { name: string; source: string; key: VendorShimKey; cacheId: string }
|
|
1435
|
+
): Promise<{ key: VendorShimKey; cacheId: string; outputName?: string; outputPath?: string; error?: string }> => {
|
|
1196
1436
|
const srcPath = path.join(outDir, `${shim.name}.src.js`);
|
|
1197
1437
|
const outputName = `${shim.name}.js`;
|
|
1198
1438
|
|
|
@@ -1207,8 +1447,11 @@ async function buildVendorShims(
|
|
|
1207
1447
|
} else if (shim.name === "_jsx-runtime" || shim.name === "_jsx-dev-runtime") {
|
|
1208
1448
|
shimExternal = ["react"];
|
|
1209
1449
|
}
|
|
1450
|
+
// `_vendor-react-refresh` and `_fast-refresh-runtime` are
|
|
1451
|
+
// self-contained: we WANT react-refresh bundled in so the
|
|
1452
|
+
// preamble's single dynamic import pulls the whole graph.
|
|
1210
1453
|
|
|
1211
|
-
const result = await
|
|
1454
|
+
const result = await safeBuild({
|
|
1212
1455
|
entrypoints: [srcPath],
|
|
1213
1456
|
outdir: outDir,
|
|
1214
1457
|
naming: outputName,
|
|
@@ -1228,32 +1471,57 @@ async function buildVendorShims(
|
|
|
1228
1471
|
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
1229
1472
|
return {
|
|
1230
1473
|
key: shim.key,
|
|
1474
|
+
cacheId: shim.cacheId,
|
|
1231
1475
|
error: `Vendor shim '${shim.name}' build failed (source: ${srcPath}):\n${grouped}\n Hint: Check the import paths and ensure the vendor package is installed.`,
|
|
1232
1476
|
};
|
|
1233
1477
|
}
|
|
1234
1478
|
|
|
1235
1479
|
return {
|
|
1236
1480
|
key: shim.key,
|
|
1481
|
+
cacheId: shim.cacheId,
|
|
1482
|
+
outputName,
|
|
1237
1483
|
outputPath: `/.mandu/client/${outputName}`,
|
|
1238
1484
|
};
|
|
1239
1485
|
} catch (error) {
|
|
1240
1486
|
await fs.unlink(srcPath).catch(() => {});
|
|
1241
1487
|
return {
|
|
1242
1488
|
key: shim.key,
|
|
1489
|
+
cacheId: shim.cacheId,
|
|
1243
1490
|
error: `[${shim.name}] ${String(error)}`,
|
|
1244
1491
|
};
|
|
1245
1492
|
}
|
|
1246
1493
|
};
|
|
1247
1494
|
|
|
1248
1495
|
const buildResults = await Promise.all(shims.map((shim) => buildShim(shim)));
|
|
1496
|
+
const writeEntries: VendorCacheWriteEntry[] = [];
|
|
1249
1497
|
for (const result of buildResults) {
|
|
1250
1498
|
if (result.error) {
|
|
1251
1499
|
errors.push(result.error);
|
|
1252
|
-
} else if (result.outputPath) {
|
|
1500
|
+
} else if (result.outputPath && result.outputName) {
|
|
1253
1501
|
results[result.key] = result.outputPath;
|
|
1502
|
+
writeEntries.push({
|
|
1503
|
+
logicalId: result.cacheId,
|
|
1504
|
+
absPath: path.join(outDir, result.outputName),
|
|
1505
|
+
});
|
|
1254
1506
|
}
|
|
1255
1507
|
}
|
|
1256
1508
|
|
|
1509
|
+
// Phase 7.2.S2 — persist freshly-built shims for next boot. Best-effort;
|
|
1510
|
+
// a write failure is logged via perf markers (opt-in) but never breaks
|
|
1511
|
+
// the build. Only write when every shim succeeded — partial manifests
|
|
1512
|
+
// would still satisfy the expected-set check on next boot but we
|
|
1513
|
+
// prefer writing only complete manifests.
|
|
1514
|
+
if (cacheEnabled && cacheKeys && errors.length === 0 && writeEntries.length > 0) {
|
|
1515
|
+
// Fire-and-forget so the fast path isn't blocked on disk. The
|
|
1516
|
+
// returned VendorBuildResult does not depend on the write outcome.
|
|
1517
|
+
mark(HMR_PERF.VENDOR_CACHE_WRITE);
|
|
1518
|
+
void writeVendorCache(rootDir, cacheKeys, writeEntries)
|
|
1519
|
+
.then(() => {
|
|
1520
|
+
measure(HMR_PERF.VENDOR_CACHE_WRITE, HMR_PERF.VENDOR_CACHE_WRITE);
|
|
1521
|
+
})
|
|
1522
|
+
.catch(() => {});
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1257
1525
|
return {
|
|
1258
1526
|
success: errors.length === 0,
|
|
1259
1527
|
react: results.react,
|
|
@@ -1261,6 +1529,8 @@ async function buildVendorShims(
|
|
|
1261
1529
|
reactDomClient: results.reactDomClient,
|
|
1262
1530
|
jsxRuntime: results.jsxRuntime,
|
|
1263
1531
|
jsxDevRuntime: results.jsxDevRuntime,
|
|
1532
|
+
reactRefreshRuntime: results.reactRefreshRuntime,
|
|
1533
|
+
fastRefreshRuntime: results.fastRefreshRuntime,
|
|
1264
1534
|
errors,
|
|
1265
1535
|
};
|
|
1266
1536
|
}
|
|
@@ -1278,13 +1548,17 @@ async function buildIsland(
|
|
|
1278
1548
|
const entryPath = path.join(outDir, `_entry_${route.id}.js`);
|
|
1279
1549
|
const outputName = `${route.id}.island.js`;
|
|
1280
1550
|
|
|
1551
|
+
// Phase 7.1 B-1/B-4: wire native Fast Refresh transform + Mandu's
|
|
1552
|
+
// boundary injection plugin. Dev-only; prod bundles remain clean.
|
|
1553
|
+
const isDev =
|
|
1554
|
+
(options.minify ?? process.env.NODE_ENV === "production") === false;
|
|
1281
1555
|
try {
|
|
1282
1556
|
// 엔트리 래퍼 생성
|
|
1283
1557
|
await Bun.write(entryPath, generateIslandEntry(route.id, clientModulePath));
|
|
1284
1558
|
|
|
1285
1559
|
// 빌드
|
|
1286
1560
|
// splitting 옵션: true면 공통 코드를 별도 청크로 추출
|
|
1287
|
-
const result = await
|
|
1561
|
+
const result = await safeBuild({
|
|
1288
1562
|
entrypoints: [entryPath],
|
|
1289
1563
|
outdir: outDir,
|
|
1290
1564
|
naming: options.splitting ? "[name]-[hash].js" : outputName,
|
|
@@ -1292,6 +1566,8 @@ async function buildIsland(
|
|
|
1292
1566
|
sourcemap: options.sourcemap ? "external" : "none",
|
|
1293
1567
|
target: "browser",
|
|
1294
1568
|
splitting: options.splitting ?? (process.env.NODE_ENV === "production"),
|
|
1569
|
+
...(isDev ? { reactFastRefresh: true } : {}),
|
|
1570
|
+
plugins: isDev ? [fastRefreshPlugin()] : [],
|
|
1295
1571
|
external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
|
|
1296
1572
|
define: {
|
|
1297
1573
|
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
|
|
@@ -1385,6 +1661,17 @@ function createBundleManifest(
|
|
|
1385
1661
|
}
|
|
1386
1662
|
}
|
|
1387
1663
|
|
|
1664
|
+
// Phase 7.1 B-2: expose Fast Refresh dev bundles so the HTML
|
|
1665
|
+
// preamble can inject a dynamic import pointing at them. Only
|
|
1666
|
+
// populated when buildVendorShims ran in dev mode.
|
|
1667
|
+
const fastRefresh =
|
|
1668
|
+
vendorResult.reactRefreshRuntime && vendorResult.fastRefreshRuntime
|
|
1669
|
+
? {
|
|
1670
|
+
runtime: vendorResult.reactRefreshRuntime,
|
|
1671
|
+
glue: vendorResult.fastRefreshRuntime,
|
|
1672
|
+
}
|
|
1673
|
+
: undefined;
|
|
1674
|
+
|
|
1388
1675
|
return {
|
|
1389
1676
|
version: 1,
|
|
1390
1677
|
buildTime: new Date().toISOString(),
|
|
@@ -1395,6 +1682,7 @@ function createBundleManifest(
|
|
|
1395
1682
|
runtime: runtimePath,
|
|
1396
1683
|
vendor: vendorResult.react, // primary vendor for backwards compatibility
|
|
1397
1684
|
router: routerPath, // Client-side Router
|
|
1685
|
+
...(fastRefresh ? { fastRefresh } : {}),
|
|
1398
1686
|
},
|
|
1399
1687
|
importMap: {
|
|
1400
1688
|
imports: {
|
|
@@ -1456,6 +1744,7 @@ export async function buildClientBundles(
|
|
|
1456
1744
|
rootDir: string,
|
|
1457
1745
|
options: BundlerOptions = {}
|
|
1458
1746
|
): Promise<BundleResult> {
|
|
1747
|
+
mark("bundler:full");
|
|
1459
1748
|
const startTime = performance.now();
|
|
1460
1749
|
const outputs: BundleOutput[] = [];
|
|
1461
1750
|
const errors: string[] = [];
|
|
@@ -1473,6 +1762,44 @@ export async function buildClientBundles(
|
|
|
1473
1762
|
// Hydration 라우트가 없어도 빈 매니페스트를 저장해야 함
|
|
1474
1763
|
// (이전 빌드의 stale 매니페스트 참조 방지)
|
|
1475
1764
|
if (hydratedRoutes.length === 0) {
|
|
1765
|
+
// #185: skipFrameworkBundles 모드에서는 기존 manifest를 그대로 유지 (devtools 재빌드도 스킵)
|
|
1766
|
+
if (options.skipFrameworkBundles) {
|
|
1767
|
+
const manifestPath = path.join(rootDir, ".mandu/manifest.json");
|
|
1768
|
+
try {
|
|
1769
|
+
const manifestRaw = await fs.readFile(manifestPath, "utf-8");
|
|
1770
|
+
let existing: BundleManifest;
|
|
1771
|
+
try {
|
|
1772
|
+
existing = JSON.parse(manifestRaw) as BundleManifest;
|
|
1773
|
+
} catch (parseError) {
|
|
1774
|
+
// #186 hardening: corrupt JSON이면 silent overwrite 대신 경고 + full build로 fallback
|
|
1775
|
+
console.warn(
|
|
1776
|
+
`[Mandu] Existing manifest is corrupt, falling back to full build: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
1777
|
+
);
|
|
1778
|
+
throw parseError;
|
|
1779
|
+
}
|
|
1780
|
+
// #186 hardening: 필수 필드 검증 (shared / bundles 누락 시 fallback)
|
|
1781
|
+
if (!existing || typeof existing !== "object" || !existing.shared || !existing.bundles) {
|
|
1782
|
+
console.warn("[Mandu] Existing manifest missing required fields, falling back to full build");
|
|
1783
|
+
throw new Error("invalid manifest shape");
|
|
1784
|
+
}
|
|
1785
|
+
return {
|
|
1786
|
+
success: true,
|
|
1787
|
+
outputs: [],
|
|
1788
|
+
errors: [],
|
|
1789
|
+
manifest: existing,
|
|
1790
|
+
stats: {
|
|
1791
|
+
totalSize: 0,
|
|
1792
|
+
totalGzipSize: 0,
|
|
1793
|
+
largestBundle: { routeId: "", size: 0 },
|
|
1794
|
+
buildTime: 0,
|
|
1795
|
+
bundleCount: 0,
|
|
1796
|
+
},
|
|
1797
|
+
};
|
|
1798
|
+
} catch {
|
|
1799
|
+
// 기존 manifest 없음/corrupt/invalid → full path로 fallback
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1476
1803
|
// Dev 모드에서는 DevTools 번들 빌드 (island 없어도 동작해야 함)
|
|
1477
1804
|
const isDev = env === "development";
|
|
1478
1805
|
if (isDev) {
|
|
@@ -1506,13 +1833,18 @@ export async function buildClientBundles(
|
|
|
1506
1833
|
if (options.targetRouteIds && options.targetRouteIds.length > 0) {
|
|
1507
1834
|
const targetRoutes = hydratedRoutes.filter((r) => options.targetRouteIds!.includes(r.id));
|
|
1508
1835
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1836
|
+
const targetResults = await Promise.all(
|
|
1837
|
+
targetRoutes.map(async (route) => {
|
|
1838
|
+
try {
|
|
1839
|
+
return { ok: true as const, result: await buildIsland(route, rootDir, outDir, options) };
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
return { ok: false as const, routeId: route.id, error: String(error) };
|
|
1842
|
+
}
|
|
1843
|
+
}),
|
|
1844
|
+
);
|
|
1845
|
+
for (const r of targetResults) {
|
|
1846
|
+
if (r.ok) outputs.push(r.result);
|
|
1847
|
+
else errors.push(`[${r.routeId}] ${r.error}`);
|
|
1516
1848
|
}
|
|
1517
1849
|
|
|
1518
1850
|
// 기존 매니페스트를 읽어 변경된 Island만 갱신
|
|
@@ -1552,11 +1884,129 @@ export async function buildClientBundles(
|
|
|
1552
1884
|
return { success: errors.length === 0, outputs, errors, manifest: existingManifest, stats };
|
|
1553
1885
|
}
|
|
1554
1886
|
|
|
1887
|
+
// #185: Framework-internal 번들 스킵 모드
|
|
1888
|
+
// 사용자 코드(src/shared 등) 변경 시 runtime/router/vendor/devtools 재빌드는 낭비.
|
|
1889
|
+
// 기존 매니페스트를 로드해 framework 출력 경로만 재사용하고 사용자 island만 재빌드.
|
|
1890
|
+
if (options.skipFrameworkBundles) {
|
|
1891
|
+
let existingManifest: BundleManifest;
|
|
1892
|
+
try {
|
|
1893
|
+
const manifestData = await fs.readFile(path.join(rootDir, ".mandu/manifest.json"), "utf-8");
|
|
1894
|
+
existingManifest = JSON.parse(manifestData) as BundleManifest;
|
|
1895
|
+
} catch (parseError) {
|
|
1896
|
+
// 기존 매니페스트 없음/corrupt → 경고 후 full build로 fallback
|
|
1897
|
+
if (parseError instanceof SyntaxError) {
|
|
1898
|
+
console.warn(
|
|
1899
|
+
`[Mandu] Existing manifest is corrupt, falling back to full build: ${parseError.message}`,
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
return buildClientBundles(manifest, rootDir, { ...options, skipFrameworkBundles: false });
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// #186 hardening: 필수 필드 검증 — 누락 시 full build로 fallback
|
|
1906
|
+
if (
|
|
1907
|
+
!existingManifest ||
|
|
1908
|
+
typeof existingManifest !== "object" ||
|
|
1909
|
+
!existingManifest.shared ||
|
|
1910
|
+
!existingManifest.bundles
|
|
1911
|
+
) {
|
|
1912
|
+
console.warn(
|
|
1913
|
+
"[Mandu] Existing manifest missing required fields (shared/bundles), falling back to full build",
|
|
1914
|
+
);
|
|
1915
|
+
return buildClientBundles(manifest, rootDir, { ...options, skipFrameworkBundles: false });
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// Pre-build validation + 병렬 island 빌드 (framework 번들은 스킵)
|
|
1919
|
+
for (const route of hydratedRoutes) {
|
|
1920
|
+
if (!route.clientModule) continue;
|
|
1921
|
+
const clientModulePath = path.join(rootDir, route.clientModule);
|
|
1922
|
+
try {
|
|
1923
|
+
const source = await fs.readFile(clientModulePath, "utf-8");
|
|
1924
|
+
const wrongImportPattern = /(?:import|from)\s+['"]@mandujs\/core['"]|require\s*\(\s*['"]@mandujs\/core['"]\s*\)/;
|
|
1925
|
+
if (wrongImportPattern.test(source)) {
|
|
1926
|
+
errors.push(
|
|
1927
|
+
`[${route.id}] Island file "${route.clientModule}" imports from "@mandujs/core" which is a server-side module.\n` +
|
|
1928
|
+
` Fix: Change the import to "@mandujs/core/client".`,
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
} catch {
|
|
1932
|
+
// 파일 읽기 실패는 나중 빌드에서 catch됨
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
const islandResults = await Promise.all(
|
|
1937
|
+
hydratedRoutes.map(async (route) => {
|
|
1938
|
+
try {
|
|
1939
|
+
return { ok: true as const, result: await buildIsland(route, rootDir, outDir, options) };
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
return { ok: false as const, routeId: route.id, error: String(error) };
|
|
1942
|
+
}
|
|
1943
|
+
}),
|
|
1944
|
+
);
|
|
1945
|
+
for (const r of islandResults) {
|
|
1946
|
+
if (r.ok) outputs.push(r.result);
|
|
1947
|
+
else errors.push(`[${r.routeId}] ${r.error}`);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// Per-island bundle 재빌드 (이미 병렬)
|
|
1951
|
+
const islandFiles = await scanIslandFiles(hydratedRoutes, rootDir);
|
|
1952
|
+
const perIslandBundles: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> = [];
|
|
1953
|
+
if (islandFiles.length > 0) {
|
|
1954
|
+
const perIslandResults = await Promise.all(
|
|
1955
|
+
islandFiles.map(async (entry) => {
|
|
1956
|
+
try {
|
|
1957
|
+
return await buildPerIslandBundle(entry, outDir, options);
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
errors.push(`[island:${entry.name}] ${String(error)}`);
|
|
1960
|
+
return null;
|
|
1961
|
+
}
|
|
1962
|
+
}),
|
|
1963
|
+
);
|
|
1964
|
+
for (const result of perIslandResults) {
|
|
1965
|
+
if (result) perIslandBundles.push(result);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// 기존 manifest를 기반으로 bundles / islands 엔트리만 교체 (framework 경로는 유지)
|
|
1970
|
+
for (const output of outputs) {
|
|
1971
|
+
if (existingManifest.bundles[output.routeId]) {
|
|
1972
|
+
existingManifest.bundles[output.routeId].js = output.outputPath;
|
|
1973
|
+
} else {
|
|
1974
|
+
const route = hydratedRoutes.find((r) => r.id === output.routeId);
|
|
1975
|
+
const hydration = route ? getRouteHydration(route) : null;
|
|
1976
|
+
existingManifest.bundles[output.routeId] = {
|
|
1977
|
+
js: output.outputPath,
|
|
1978
|
+
dependencies: ["_runtime", "_react"],
|
|
1979
|
+
priority: hydration?.priority || HYDRATION.DEFAULT_PRIORITY,
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
if (perIslandBundles.length > 0) {
|
|
1984
|
+
existingManifest.islands = existingManifest.islands || {};
|
|
1985
|
+
for (const ib of perIslandBundles) {
|
|
1986
|
+
existingManifest.islands[ib.name] = {
|
|
1987
|
+
js: ib.js,
|
|
1988
|
+
route: ib.route,
|
|
1989
|
+
priority: ib.priority,
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
await fs.writeFile(
|
|
1995
|
+
path.join(rootDir, ".mandu/manifest.json"),
|
|
1996
|
+
JSON.stringify(existingManifest, null, 2),
|
|
1997
|
+
);
|
|
1998
|
+
|
|
1999
|
+
const stats = calculateStats(outputs, startTime);
|
|
2000
|
+
return { success: errors.length === 0, outputs, errors, manifest: existingManifest, stats };
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1555
2003
|
// 3-4. Runtime, Router, Vendor, DevTools 번들 병렬 빌드 (서로 독립적)
|
|
1556
2004
|
const isDev = env === "development";
|
|
1557
2005
|
const runtimePromise = buildRuntime(outDir, options);
|
|
1558
2006
|
const routerPromise = buildRouterRuntime(outDir, options);
|
|
1559
|
-
|
|
2007
|
+
// Phase 7.2.S2 — `rootDir` is now threaded through so buildVendorShims can
|
|
2008
|
+
// consult `.mandu/vendor-cache/` for warm-boot shim reuse.
|
|
2009
|
+
const vendorPromise = buildVendorShims(rootDir, outDir, options);
|
|
1560
2010
|
const devtoolsPromise = isDev ? buildDevtoolsBundle(outDir, options) : null;
|
|
1561
2011
|
|
|
1562
2012
|
const [runtimeResult, routerResult, vendorResult, devtoolsResult] = await Promise.all([
|
|
@@ -1602,23 +2052,30 @@ export async function buildClientBundles(
|
|
|
1602
2052
|
}
|
|
1603
2053
|
}
|
|
1604
2054
|
|
|
1605
|
-
// 5. 각 Island 번들 빌드
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
2055
|
+
// 5. 각 Island 번들 병렬 빌드 (#185: L1631의 per-island와 일관성 확보)
|
|
2056
|
+
const fullIslandResults = await Promise.all(
|
|
2057
|
+
hydratedRoutes.map(async (route) => {
|
|
2058
|
+
try {
|
|
2059
|
+
return { ok: true as const, result: await buildIsland(route, rootDir, outDir, options) };
|
|
2060
|
+
} catch (error) {
|
|
2061
|
+
return { ok: false as const, route, error: String(error) };
|
|
2062
|
+
}
|
|
2063
|
+
}),
|
|
2064
|
+
);
|
|
2065
|
+
for (const r of fullIslandResults) {
|
|
2066
|
+
if (r.ok) {
|
|
2067
|
+
outputs.push(r.result);
|
|
2068
|
+
} else {
|
|
2069
|
+
const errorStr = r.error;
|
|
1613
2070
|
if (errorStr.includes("AggregateError") || errorStr.includes("Could not resolve")) {
|
|
1614
|
-
const clientModule = route.clientModule || "";
|
|
2071
|
+
const clientModule = r.route.clientModule || "";
|
|
1615
2072
|
errors.push(
|
|
1616
|
-
`[${route.id}] ${errorStr}\n` +
|
|
2073
|
+
`[${r.route.id}] ${errorStr}\n` +
|
|
1617
2074
|
` 💡 Hint: If your island imports from "@mandujs/core", change it to "@mandujs/core/client".\n` +
|
|
1618
|
-
` Client islands cannot use server-side modules. File: ${clientModule}
|
|
2075
|
+
` Client islands cannot use server-side modules. File: ${clientModule}`,
|
|
1619
2076
|
);
|
|
1620
2077
|
} else {
|
|
1621
|
-
errors.push(`[${route.id}] ${errorStr}`);
|
|
2078
|
+
errors.push(`[${r.route.id}] ${errorStr}`);
|
|
1622
2079
|
}
|
|
1623
2080
|
}
|
|
1624
2081
|
}
|
|
@@ -1662,6 +2119,7 @@ export async function buildClientBundles(
|
|
|
1662
2119
|
// 7. 통계 계산
|
|
1663
2120
|
const stats = calculateStats(outputs, startTime);
|
|
1664
2121
|
|
|
2122
|
+
measure("bundler:full", "bundler:full");
|
|
1665
2123
|
return {
|
|
1666
2124
|
success: errors.length === 0,
|
|
1667
2125
|
outputs,
|