@mandujs/core 0.21.0 → 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/package.json +94 -69
- 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 +310 -18
- package/src/bundler/css.ts +326 -323
- package/src/bundler/dev.ts +1611 -59
- 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 +11 -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/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/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 +518 -20
- 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 +58 -0
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[] = [];
|
|
@@ -1715,7 +2004,9 @@ export async function buildClientBundles(
|
|
|
1715
2004
|
const isDev = env === "development";
|
|
1716
2005
|
const runtimePromise = buildRuntime(outDir, options);
|
|
1717
2006
|
const routerPromise = buildRouterRuntime(outDir, options);
|
|
1718
|
-
|
|
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);
|
|
1719
2010
|
const devtoolsPromise = isDev ? buildDevtoolsBundle(outDir, options) : null;
|
|
1720
2011
|
|
|
1721
2012
|
const [runtimeResult, routerResult, vendorResult, devtoolsResult] = await Promise.all([
|
|
@@ -1828,6 +2119,7 @@ export async function buildClientBundles(
|
|
|
1828
2119
|
// 7. 통계 계산
|
|
1829
2120
|
const stats = calculateStats(outputs, startTime);
|
|
1830
2121
|
|
|
2122
|
+
measure("bundler:full", "bundler:full");
|
|
1831
2123
|
return {
|
|
1832
2124
|
success: errors.length === 0,
|
|
1833
2125
|
outputs,
|