@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/runtime/ssr.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { serializeProps } from "../client/serialize";
|
|
|
3
3
|
import { createRequire } from "module";
|
|
4
4
|
import type { ReactElement } from "react";
|
|
5
5
|
import type { BundleManifest } from "../bundler/types";
|
|
6
|
+
import { isSafeManduUrl } from "../bundler/manifest-schema";
|
|
6
7
|
import type { HydrationConfig, HydrationPriority } from "../spec/schema";
|
|
7
8
|
import { PORTS, TIMEOUTS } from "../constants";
|
|
8
9
|
import { escapeHtmlAttr, escapeHtmlText, escapeJsonForInlineScript } from "./escape";
|
|
9
10
|
import { REACT_INTERNALS_SHIM_SCRIPT } from "./shims";
|
|
11
|
+
import { generateFastRefreshPreamble } from "../bundler/dev";
|
|
10
12
|
|
|
11
13
|
// Re-export streaming SSR utilities
|
|
12
14
|
export {
|
|
@@ -50,6 +52,23 @@ export interface SSROptions {
|
|
|
50
52
|
cssPath?: string | false;
|
|
51
53
|
/** Island 래핑이 이미 React 엘리먼트 레벨에서 완료됨 (중복 래핑 방지) */
|
|
52
54
|
islandPreWrapped?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Phase 7.2 R1 Agent C (H1) — Content-Security-Policy nonce for the
|
|
57
|
+
* Fast Refresh inline preamble. Three accepted shapes:
|
|
58
|
+
* - `true` → auto-generate a fresh 128-bit base64 nonce
|
|
59
|
+
* per-render and insert it as the `nonce`
|
|
60
|
+
* attribute on the preamble `<script>` tag.
|
|
61
|
+
* - non-empty string → use the caller-provided nonce verbatim
|
|
62
|
+
* (e.g. one already produced by the
|
|
63
|
+
* `secure()` middleware and stashed on
|
|
64
|
+
* `ctx.get('csp-nonce')`).
|
|
65
|
+
* - `false` / unset → legacy behavior; no nonce attribute, no
|
|
66
|
+
* CSP header emitted. Also the effective
|
|
67
|
+
* behavior when env `MANDU_CSP_NONCE=0`.
|
|
68
|
+
* Only takes effect in dev mode with a populated `shared.fastRefresh`
|
|
69
|
+
* manifest entry — prod builds never emit the preamble at all.
|
|
70
|
+
*/
|
|
71
|
+
cspNonce?: string | boolean;
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
let projectRenderToString: ((element: ReactElement) => string) | null | undefined;
|
|
@@ -181,6 +200,184 @@ export function wrapWithIsland(
|
|
|
181
200
|
return `<div data-mandu-island="${escapeHtmlAttr(routeId)}"${srcAttr} data-mandu-priority="${escapeHtmlAttr(priority)}" style="display:contents">${content}</div>`;
|
|
182
201
|
}
|
|
183
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Phase 7.1 R2 Agent D — Fast Refresh preamble emission helper.
|
|
205
|
+
* Phase 7.2 R1 Agent C (H1) — optional CSP nonce injection.
|
|
206
|
+
*
|
|
207
|
+
* Emits the `<script>` block returned by `generateFastRefreshPreamble`
|
|
208
|
+
* ONLY when all three preconditions hold:
|
|
209
|
+
*
|
|
210
|
+
* 1. `isDev === true` — the preamble and its glue import rely on the
|
|
211
|
+
* `_fast-refresh-runtime.js` / `_vendor-react-refresh.js` assets
|
|
212
|
+
* which are only emitted by the dev bundler path.
|
|
213
|
+
* 2. `manifest.shared.fastRefresh` is populated — which happens only
|
|
214
|
+
* in dev mode (see `build.ts:1548`). Missing here implies either a
|
|
215
|
+
* prod manifest, a failed vendor shim build, or a unit test that
|
|
216
|
+
* stubbed the manifest. All three short-circuit to empty output.
|
|
217
|
+
* 3. Both the `glue` and `runtime` fields are non-empty strings —
|
|
218
|
+
* `generateFastRefreshPreamble` itself re-checks and emits a
|
|
219
|
+
* defensive stub comment if either is missing.
|
|
220
|
+
*
|
|
221
|
+
* Returned as a string so the caller can position it inside `<head>`
|
|
222
|
+
* BEFORE any `<script type="module">` runs. This matters because
|
|
223
|
+
* `reactFastRefresh: true`-transformed islands call `$RefreshReg$` at
|
|
224
|
+
* the top of the module; the stubs installed inside the preamble must
|
|
225
|
+
* exist before those calls execute, otherwise the island throws a
|
|
226
|
+
* `ReferenceError` during evaluation and never hydrates.
|
|
227
|
+
*
|
|
228
|
+
* Production builds see `fastRefresh` as `undefined` on the manifest
|
|
229
|
+
* (build.ts omits it), so this function returns `""` and the HTML
|
|
230
|
+
* remains byte-identical to pre-7.1 prod output.
|
|
231
|
+
*
|
|
232
|
+
* When `nonce` is a non-empty string, the returned `<script>` opening
|
|
233
|
+
* tag is rewritten to `<script nonce="...">`. This is the single
|
|
234
|
+
* modification point for CSP compliance — the inner body comes verbatim
|
|
235
|
+
* from `generateFastRefreshPreamble` (owned by dev.ts) so the bundler
|
|
236
|
+
* and SSR sides stay decoupled.
|
|
237
|
+
*/
|
|
238
|
+
function generateFastRefreshPreambleTag(
|
|
239
|
+
isDev: boolean,
|
|
240
|
+
manifest: BundleManifest | undefined,
|
|
241
|
+
nonce?: string,
|
|
242
|
+
): string {
|
|
243
|
+
if (!isDev) return "";
|
|
244
|
+
const fr = manifest?.shared?.fastRefresh;
|
|
245
|
+
if (!fr) return "";
|
|
246
|
+
if (!fr.glue || !fr.runtime) return "";
|
|
247
|
+
// Phase 7.2.R3 M-01 — manifest wire-up. `isSafeManduUrl` rejects
|
|
248
|
+
// tampered entries (protocol, traversal, non-/.mandu paths, >2KB).
|
|
249
|
+
// Fail closed: if either URL is suspect, skip the preamble entirely
|
|
250
|
+
// — dev refresh breaks loudly instead of injecting an attacker-
|
|
251
|
+
// controlled <script src> via a tampered manifest.
|
|
252
|
+
if (!isSafeManduUrl(fr.glue) || !isSafeManduUrl(fr.runtime)) return "";
|
|
253
|
+
const raw = generateFastRefreshPreamble(fr.glue, fr.runtime);
|
|
254
|
+
if (!nonce) return raw;
|
|
255
|
+
// Inject nonce attribute onto the first <script> tag ONLY. Matches
|
|
256
|
+
// exactly `<script>` (the shape emitted by `generateFastRefreshPreamble`)
|
|
257
|
+
// to avoid accidentally nonce-ing a `<script src=...>` or malformed
|
|
258
|
+
// variant. If the upstream function changes shape, the regex still
|
|
259
|
+
// fails closed (returns raw) — unit-tested.
|
|
260
|
+
const nonceEscaped = escapeHtmlAttr(nonce);
|
|
261
|
+
return raw.replace(/<script>/, `<script nonce="${nonceEscaped}">`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Phase 7.2 R1 Agent C (H1) — Resolve the CSP nonce to use for the Fast
|
|
266
|
+
* Refresh preamble. Three layered precedences (highest first):
|
|
267
|
+
*
|
|
268
|
+
* 1. `MANDU_CSP_NONCE=0` env var forces off (opt-out escape hatch for
|
|
269
|
+
* projects with an existing Content-Security-Policy pipeline that
|
|
270
|
+
* would collide with ours).
|
|
271
|
+
* 2. Explicit `options.cspNonce`:
|
|
272
|
+
* - string → use verbatim
|
|
273
|
+
* - true → auto-generate
|
|
274
|
+
* - false → off
|
|
275
|
+
* 3. No option → off (preserves legacy behavior byte-identical).
|
|
276
|
+
*
|
|
277
|
+
* Auto-generation uses `crypto.getRandomValues` with 16 bytes → 128 bits
|
|
278
|
+
* of entropy, matching OWASP guidance and the existing
|
|
279
|
+
* `@mandujs/core/middleware/secure` CSP nonce generator.
|
|
280
|
+
*
|
|
281
|
+
* Only invoked when we actually intend to emit a preamble (i.e. dev mode
|
|
282
|
+
* + `needsHydration` + populated `shared.fastRefresh`). Returns
|
|
283
|
+
* `undefined` when CSP nonce emission is disabled; the caller short-
|
|
284
|
+
* circuits the `nonce=` attribute and skips the response CSP header.
|
|
285
|
+
*/
|
|
286
|
+
function resolveFastRefreshCspNonce(
|
|
287
|
+
opt: SSROptions["cspNonce"],
|
|
288
|
+
): string | undefined {
|
|
289
|
+
// Opt-out: env var takes absolute precedence. Read lazily because
|
|
290
|
+
// process is not available in some edge runtimes.
|
|
291
|
+
try {
|
|
292
|
+
if (typeof process !== "undefined" && process.env && process.env.MANDU_CSP_NONCE === "0") {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
/* some runtimes lock down `process` access — treat as unset */
|
|
297
|
+
}
|
|
298
|
+
if (opt === false || opt === undefined) return undefined;
|
|
299
|
+
if (typeof opt === "string" && opt.length > 0) return opt;
|
|
300
|
+
if (opt === true) return generateCspNonce();
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Phase 7.2 R1 Agent C (H1) — Generate a fresh CSP nonce.
|
|
306
|
+
*
|
|
307
|
+
* 16 bytes (128 bits) of cryptographic entropy, base64-encoded. Matches
|
|
308
|
+
* `packages/core/src/middleware/secure/csp.ts#resolveNonce`. The same
|
|
309
|
+
* encoding keeps the nonce attribute short (≤24 base64 chars) and
|
|
310
|
+
* compatible with OWASP CSP3 guidance.
|
|
311
|
+
*
|
|
312
|
+
* Exported as `_testOnly_generateCspNonce` below for tests to verify
|
|
313
|
+
* entropy and encoding without depending on the specific platform.
|
|
314
|
+
*/
|
|
315
|
+
function generateCspNonce(): string {
|
|
316
|
+
const bytes = new Uint8Array(16);
|
|
317
|
+
crypto.getRandomValues(bytes);
|
|
318
|
+
// Bun / Node 20+ both expose btoa + String.fromCharCode; avoid the
|
|
319
|
+
// Buffer dependency for portability. Node has `btoa` since v16.
|
|
320
|
+
let bin = "";
|
|
321
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
322
|
+
return btoa(bin);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Phase 7.2 R1 Agent C (H1) — Build the `Content-Security-Policy`
|
|
327
|
+
* header value to pair with a nonce-bearing preamble.
|
|
328
|
+
*
|
|
329
|
+
* Minimal and permissive by design — we are specifically writing a
|
|
330
|
+
* DEV-mode header that lets the Fast Refresh preamble + dynamic
|
|
331
|
+
* imports execute. We deliberately do NOT emit a blanket CSP for the
|
|
332
|
+
* entire page; that is the job of `@mandujs/core/middleware/secure` or
|
|
333
|
+
* the project's edge. Our sole concern here is: the inline preamble
|
|
334
|
+
* MUST be authorized without forcing the user to hand-write
|
|
335
|
+
* `'unsafe-inline'` in their policy.
|
|
336
|
+
*
|
|
337
|
+
* Header shape:
|
|
338
|
+
* `script-src 'self' 'nonce-<n>' 'strict-dynamic'`
|
|
339
|
+
*
|
|
340
|
+
* Why `'strict-dynamic'`: once the nonced preamble runs, it loads the
|
|
341
|
+
* Fast Refresh runtime via `import('/.mandu/client/...')`. Without
|
|
342
|
+
* `'strict-dynamic'` the module graph would need to be nonce-tagged
|
|
343
|
+
* end-to-end, which Bun.build does not support today.
|
|
344
|
+
*/
|
|
345
|
+
function buildFastRefreshCspHeader(nonce: string): string {
|
|
346
|
+
// Nonces are already URL-safe / base64 but we defensively strip any
|
|
347
|
+
// stray quotes — should be impossible given the generator, but this
|
|
348
|
+
// closes the door on a caller-supplied nonce that slipped a quote in.
|
|
349
|
+
const safe = nonce.replace(/["\\\r\n]/g, "");
|
|
350
|
+
return `script-src 'self' 'nonce-${safe}' 'strict-dynamic'`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** @internal test helper — exposed only so unit tests can inspect the generator. */
|
|
354
|
+
export const _testOnly_generateCspNonce = generateCspNonce;
|
|
355
|
+
/** @internal test helper — exposed only so unit tests can inspect the resolver. */
|
|
356
|
+
export const _testOnly_resolveFastRefreshCspNonce = resolveFastRefreshCspNonce;
|
|
357
|
+
/** @internal test helper — exposed only so unit tests can inspect the header builder. */
|
|
358
|
+
export const _testOnly_buildFastRefreshCspHeader = buildFastRefreshCspHeader;
|
|
359
|
+
/** @internal test helper — exposed only so unit tests can inspect the preamble tag emitter. */
|
|
360
|
+
export const _testOnly_generateFastRefreshPreambleTag = generateFastRefreshPreambleTag;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Phase 7.2 R1 Agent C (H1) — Internal WeakMap that ferries the
|
|
364
|
+
* CSP nonce chosen during a `renderToHTML` call back to the caller
|
|
365
|
+
* (`renderSSR` / `renderWithHydration`) so they can emit the matching
|
|
366
|
+
* `Content-Security-Policy` response header.
|
|
367
|
+
*
|
|
368
|
+
* Keyed by the `options` object identity — the only caller that cares
|
|
369
|
+
* reuses the same options instance it handed in. External consumers
|
|
370
|
+
* of `renderToHTML` (tests, advanced users) are unaffected: they
|
|
371
|
+
* simply never look up the map and no memory accumulates because
|
|
372
|
+
* WeakMap entries collect when their key reference dies.
|
|
373
|
+
*/
|
|
374
|
+
const OPTIONS_TO_NONCE = new WeakMap<object, string>();
|
|
375
|
+
|
|
376
|
+
/** @internal surface for unit tests — do not use in application code. */
|
|
377
|
+
export function _testOnly_getAttachedCspNonce(options: SSROptions): string | undefined {
|
|
378
|
+
return OPTIONS_TO_NONCE.get(options as object);
|
|
379
|
+
}
|
|
380
|
+
|
|
184
381
|
export function renderToHTML(element: ReactElement, options: SSROptions = {}): string {
|
|
185
382
|
const {
|
|
186
383
|
title = "Mandu App",
|
|
@@ -276,6 +473,25 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
|
|
|
276
473
|
hmrScript = generateHMRScript(hmrPort);
|
|
277
474
|
}
|
|
278
475
|
|
|
476
|
+
// Phase 7.1 R2 Agent D: Fast Refresh preamble. Must land in <head>
|
|
477
|
+
// BEFORE any island `<script type="module">` evaluates — the stubs it
|
|
478
|
+
// installs for `$RefreshReg$` / `$RefreshSig$` are required by every
|
|
479
|
+
// module the bundler transformed with `reactFastRefresh: true`. Dev
|
|
480
|
+
// mode only; prod manifests omit `shared.fastRefresh` so the helper
|
|
481
|
+
// returns "".
|
|
482
|
+
// Phase 7.2 R1 Agent C (H1): resolve nonce up-front so the same
|
|
483
|
+
// value is reused for the <script> tag AND surfaced to the caller
|
|
484
|
+
// (via `renderToHTMLWithMeta`) for the response CSP header.
|
|
485
|
+
const resolvedCspNonce = needsHydration && isDev
|
|
486
|
+
? resolveFastRefreshCspNonce(options.cspNonce)
|
|
487
|
+
: undefined;
|
|
488
|
+
if (resolvedCspNonce) {
|
|
489
|
+
OPTIONS_TO_NONCE.set(options as object, resolvedCspNonce);
|
|
490
|
+
}
|
|
491
|
+
const fastRefreshPreamble = needsHydration
|
|
492
|
+
? generateFastRefreshPreambleTag(isDev, bundleManifest, resolvedCspNonce)
|
|
493
|
+
: "";
|
|
494
|
+
|
|
279
495
|
// DevTools 번들 로드 (개발 모드)
|
|
280
496
|
let devtoolsScript = "";
|
|
281
497
|
if (isDev) {
|
|
@@ -303,6 +519,7 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
|
|
|
303
519
|
${hoistedLinkTags}
|
|
304
520
|
${headTags}
|
|
305
521
|
${collectedHeadTags}
|
|
522
|
+
${fastRefreshPreamble}
|
|
306
523
|
</head>
|
|
307
524
|
<body>
|
|
308
525
|
<div id="root">${bodyContent}</div>
|
|
@@ -363,6 +580,17 @@ function generateClientRouterScript(manifest: BundleManifest): string {
|
|
|
363
580
|
|
|
364
581
|
/**
|
|
365
582
|
* HMR 스크립트 생성
|
|
583
|
+
*
|
|
584
|
+
* Phase 7.2 Agent B — HDR (Hot Data Revalidation) extension.
|
|
585
|
+
* When the CLI broadcasts `{type: "vite", payload: {type: "custom",
|
|
586
|
+
* event: "mandu:slot-refetch", data: {routeId, slotPath, ...}}}` (via
|
|
587
|
+
* `hmrServer.broadcastVite`) we handle it here without remounting
|
|
588
|
+
* the React tree: fetch the current URL with `X-Mandu-HDR: 1`,
|
|
589
|
+
* receive JSON loader data, and hand it to the router's
|
|
590
|
+
* `applyHDRUpdate` hook (installed by `initializeRouter`). If the
|
|
591
|
+
* route doesn't match, the router hook is missing, or the fetch
|
|
592
|
+
* fails — we fall back to `location.reload()`. See the detailed
|
|
593
|
+
* design notes in `bundler/dev.ts:generateHMRClientScript` docstring.
|
|
366
594
|
*/
|
|
367
595
|
function generateHMRScript(port: number): string {
|
|
368
596
|
const hmrPort = port + PORTS.HMR_OFFSET;
|
|
@@ -382,6 +610,66 @@ window.__MANDU_HMR_PORT__ = ${hmrPort};
|
|
|
382
610
|
}
|
|
383
611
|
}
|
|
384
612
|
|
|
613
|
+
function hdrCurrentRouteId() {
|
|
614
|
+
var rs = window.__MANDU_ROUTER_STATE__;
|
|
615
|
+
if (rs && rs.currentRoute && rs.currentRoute.id) return String(rs.currentRoute.id);
|
|
616
|
+
var r = window.__MANDU_ROUTE__;
|
|
617
|
+
if (r && r.id) return String(r.id);
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function hdrFallback(reason) {
|
|
622
|
+
console.log('[Mandu HDR] Fallback full reload' + (reason ? ' (' + reason + ')' : ''));
|
|
623
|
+
location.reload();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function hdrMark(name, data) {
|
|
627
|
+
try {
|
|
628
|
+
if (window.__MANDU_HDR__ && typeof window.__MANDU_HDR__.perfMark === 'function') {
|
|
629
|
+
window.__MANDU_HDR__.perfMark(name, data);
|
|
630
|
+
}
|
|
631
|
+
} catch (_) {}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function handleSlotRefetch(data) {
|
|
635
|
+
var routeId = data && typeof data.routeId === 'string' ? data.routeId : null;
|
|
636
|
+
if (!routeId) { hdrFallback('no-routeId'); return; }
|
|
637
|
+
if (window.__MANDU_HDR_DISABLED__ === true) { hdrFallback('disabled'); return; }
|
|
638
|
+
var currentId = hdrCurrentRouteId();
|
|
639
|
+
if (currentId !== routeId) {
|
|
640
|
+
console.log('[Mandu HDR] slot-refetch for ' + routeId + ' ignored (current route: ' + currentId + ')');
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
var started = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
|
|
644
|
+
hdrMark('hdr:refetch-start', { routeId: routeId, slotPath: data.slotPath });
|
|
645
|
+
var url = window.location.pathname + window.location.search;
|
|
646
|
+
var sep = url.indexOf('?') >= 0 ? '&' : '?';
|
|
647
|
+
var dataUrl = url + sep + '_data=1';
|
|
648
|
+
fetch(dataUrl, { credentials: 'same-origin', headers: { 'X-Mandu-HDR': '1' } })
|
|
649
|
+
.then(function (res) {
|
|
650
|
+
if (!res.ok) { hdrFallback('status-' + res.status); return null; }
|
|
651
|
+
return res.json();
|
|
652
|
+
})
|
|
653
|
+
.then(function (payload) {
|
|
654
|
+
if (!payload) return;
|
|
655
|
+
var revalidate = window.__MANDU_ROUTER_REVALIDATE__;
|
|
656
|
+
if (typeof revalidate !== 'function') { hdrFallback('no-router'); return; }
|
|
657
|
+
try {
|
|
658
|
+
revalidate(routeId, payload.loaderData);
|
|
659
|
+
var elapsed = (typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now()) - started;
|
|
660
|
+
console.log('[Mandu HDR] Applied loader data for ' + routeId + ' in ' + elapsed.toFixed(0) + 'ms');
|
|
661
|
+
hdrMark('hdr:refetch', { routeId: routeId, slotPath: data.slotPath, elapsed: elapsed });
|
|
662
|
+
} catch (err) {
|
|
663
|
+
console.error('[Mandu HDR] Revalidate threw:', err);
|
|
664
|
+
hdrFallback('revalidate-throw');
|
|
665
|
+
}
|
|
666
|
+
})
|
|
667
|
+
.catch(function (err) {
|
|
668
|
+
console.error('[Mandu HDR] Fetch failed:', err);
|
|
669
|
+
hdrFallback('fetch-failed');
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
385
673
|
function connect() {
|
|
386
674
|
try {
|
|
387
675
|
ws = new WebSocket('ws://' + window.location.hostname + ':${hmrPort}');
|
|
@@ -392,9 +680,26 @@ window.__MANDU_HMR_PORT__ = ${hmrPort};
|
|
|
392
680
|
ws.onmessage = function(e) {
|
|
393
681
|
try {
|
|
394
682
|
var msg = JSON.parse(e.data);
|
|
395
|
-
|
|
683
|
+
// Vite-compat envelope: custom event for HDR.
|
|
684
|
+
if ((msg.type === 'vite' || msg.type === 'vite-replay') && msg.payload) {
|
|
685
|
+
if (msg.payload.type === 'custom' && msg.payload.event === 'mandu:slot-refetch') {
|
|
686
|
+
handleSlotRefetch(msg.payload.data || {});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
// Other Vite payloads fall through to the legacy branches
|
|
690
|
+
// when possible.
|
|
691
|
+
if (msg.payload.type === 'full-reload') {
|
|
692
|
+
location.reload();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (msg.type === 'reload' || msg.type === 'island-update' || msg.type === 'full-reload' || msg.type === 'layout-update' || msg.type === 'invalidate') {
|
|
396
697
|
console.log('[Mandu HMR] Reloading...');
|
|
397
698
|
location.reload();
|
|
699
|
+
} else if (msg.type === 'slot-refetch') {
|
|
700
|
+
// Legacy Mandu-internal path (not yet used by the server — kept
|
|
701
|
+
// for forward-compat).
|
|
702
|
+
handleSlotRefetch(msg.data || {});
|
|
398
703
|
} else if (msg.type === 'css-update') {
|
|
399
704
|
var cssPath = (msg.data && msg.data.cssPath) || '/.mandu/client/globals.css';
|
|
400
705
|
var links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
@@ -431,18 +736,38 @@ function generateDevtoolsScript(): string {
|
|
|
431
736
|
return `<script type="module" src="/.mandu/client/_devtools.js"></script>`;
|
|
432
737
|
}
|
|
433
738
|
|
|
434
|
-
export function createHTMLResponse(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
739
|
+
export function createHTMLResponse(
|
|
740
|
+
html: string,
|
|
741
|
+
status: number = 200,
|
|
742
|
+
/**
|
|
743
|
+
* Phase 7.2 R1 Agent C (H1) — extra headers to merge in alongside
|
|
744
|
+
* the default `Content-Type`. Used for the Fast Refresh CSP header
|
|
745
|
+
* when a nonce was produced; can be repurposed for future
|
|
746
|
+
* per-response metadata without changing callsites.
|
|
747
|
+
*/
|
|
748
|
+
extraHeaders?: Record<string, string>,
|
|
749
|
+
): Response {
|
|
750
|
+
const headers: Record<string, string> = {
|
|
751
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
752
|
+
};
|
|
753
|
+
if (extraHeaders) {
|
|
754
|
+
for (const [k, v] of Object.entries(extraHeaders)) {
|
|
755
|
+
headers[k] = v;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return new Response(html, { status, headers });
|
|
441
759
|
}
|
|
442
760
|
|
|
443
761
|
export function renderSSR(element: ReactElement, options: SSROptions = {}): Response {
|
|
444
762
|
const html = renderToHTML(element, options);
|
|
445
|
-
|
|
763
|
+
// Phase 7.2 R1 Agent C (H1): if a CSP nonce was produced during
|
|
764
|
+
// rendering (dev + fast-refresh path), emit the matching header so
|
|
765
|
+
// the inline preamble is authorized under strict CSP.
|
|
766
|
+
const nonce = _testOnly_getAttachedCspNonce(options);
|
|
767
|
+
const extra = nonce
|
|
768
|
+
? { "Content-Security-Policy": buildFastRefreshCspHeader(nonce) }
|
|
769
|
+
: undefined;
|
|
770
|
+
return createHTMLResponse(html, 200, extra);
|
|
446
771
|
}
|
|
447
772
|
|
|
448
773
|
/**
|
|
@@ -472,5 +797,10 @@ export async function renderWithHydration(
|
|
|
472
797
|
}
|
|
473
798
|
): Promise<Response> {
|
|
474
799
|
const html = renderToHTML(element, options);
|
|
475
|
-
|
|
800
|
+
// Phase 7.2 R1 Agent C (H1) — same CSP header logic as renderSSR.
|
|
801
|
+
const nonce = _testOnly_getAttachedCspNonce(options);
|
|
802
|
+
const extra = nonce
|
|
803
|
+
? { "Content-Security-Policy": buildFastRefreshCspHeader(nonce) }
|
|
804
|
+
: undefined;
|
|
805
|
+
return createHTMLResponse(html, 200, extra);
|
|
476
806
|
}
|