@mandujs/core 0.13.0 β 0.13.1
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.ko.md +4 -4
- package/README.md +653 -653
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/client/Link.tsx +227 -227
- package/src/client/globals.ts +44 -44
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +5 -5
- package/src/client/island.ts +8 -8
- package/src/client/router.ts +435 -435
- package/src/client/runtime.ts +23 -23
- package/src/client/serialize.ts +404 -404
- package/src/client/window-state.ts +101 -101
- package/src/config/mandu.ts +9 -0
- package/src/config/validate.ts +12 -0
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/content-layer.ts +314 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/data-store.ts +245 -245
- package/src/content/digest.ts +133 -133
- package/src/content/index.ts +164 -164
- package/src/content/loader-context.ts +172 -172
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +169 -169
- package/src/content/loaders/glob.ts +252 -252
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/client.ts +16 -16
- package/src/contract/define.ts +459 -459
- package/src/contract/handler.ts +10 -10
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +404 -404
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/registry.ts +568 -568
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/contract/validator.ts +32 -32
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +465 -465
- package/src/devtools/client/catchers/error-catcher.ts +327 -327
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/catchers/network-proxy.ts +363 -363
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/kitchen-root.tsx +362 -362
- package/src/devtools/client/components/mandu-character.tsx +241 -241
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
- package/src/devtools/client/components/panel/index.ts +32 -32
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
- package/src/devtools/client/components/panel/network-panel.tsx +292 -292
- package/src/devtools/client/components/panel/panel-container.tsx +259 -259
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/client/state-manager.ts +478 -478
- package/src/devtools/design-tokens.ts +263 -263
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +266 -266
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +444 -444
- package/src/devtools/types.ts +319 -319
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +222 -222
- package/src/devtools/worker/worker-manager.ts +409 -409
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +46 -46
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +24 -1
- package/src/filling/deps.ts +238 -238
- package/src/filling/index.ts +2 -0
- package/src/filling/sse.test.ts +168 -0
- package/src/filling/sse.ts +162 -0
- package/src/generator/index.ts +3 -3
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -291
- package/src/guard/reporter.ts +445 -445
- package/src/guard/rules.ts +12 -12
- package/src/guard/statistics.ts +578 -578
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/types.ts +348 -348
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +6 -1
- package/src/intent/index.ts +310 -310
- package/src/island/index.ts +304 -304
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-scanner.ts +497 -497
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/escape.ts +44 -0
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/logger.ts +677 -677
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/router.ts +105 -105
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +257 -0
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +16 -21
- package/src/runtime/streaming-ssr.ts +24 -33
- package/src/runtime/trace.ts +144 -144
- package/src/seo/index.ts +214 -214
- package/src/seo/integration/ssr.ts +307 -307
- package/src/seo/render/basic.ts +427 -427
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +191 -191
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +126 -126
- package/src/seo/resolve/index.ts +353 -353
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +97 -97
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +575 -575
- package/src/slot/validator.ts +39 -39
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
- package/src/utils/bun.ts +8 -8
- package/src/utils/lru-cache.ts +75 -75
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
package/src/runtime/ssr.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ReactElement } from "react";
|
|
|
4
4
|
import type { BundleManifest } from "../bundler/types";
|
|
5
5
|
import type { HydrationConfig, HydrationPriority } from "../spec/schema";
|
|
6
6
|
import { PORTS, TIMEOUTS } from "../constants";
|
|
7
|
+
import { escapeHtmlAttr, escapeJsonForInlineScript } from "./escape";
|
|
7
8
|
|
|
8
9
|
// Re-export streaming SSR utilities
|
|
9
10
|
export {
|
|
@@ -53,11 +54,7 @@ export interface SSROptions {
|
|
|
53
54
|
*/
|
|
54
55
|
function serializeServerData(data: Record<string, unknown>): string {
|
|
55
56
|
// serializePropsλ‘ κ³ κΈ μ§λ ¬ν (Date, Map, Set λ± μ§μ)
|
|
56
|
-
const json = serializeProps(data)
|
|
57
|
-
.replace(/</g, "\\u003c")
|
|
58
|
-
.replace(/>/g, "\\u003e")
|
|
59
|
-
.replace(/&/g, "\\u0026")
|
|
60
|
-
.replace(/'/g, "\\u0027");
|
|
57
|
+
const json = escapeJsonForInlineScript(serializeProps(data));
|
|
61
58
|
|
|
62
59
|
return `<script id="__MANDU_DATA__" type="application/json">${json}</script>
|
|
63
60
|
<script>window.__MANDU_DATA_RAW__ = document.getElementById('__MANDU_DATA__').textContent;</script>`;
|
|
@@ -71,7 +68,7 @@ function generateImportMap(manifest: BundleManifest): string {
|
|
|
71
68
|
return "";
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
const importMapJson = JSON.stringify(manifest.importMap, null, 2);
|
|
71
|
+
const importMapJson = escapeJsonForInlineScript(JSON.stringify(manifest.importMap, null, 2));
|
|
75
72
|
return `<script type="importmap">${importMapJson}</script>`;
|
|
76
73
|
}
|
|
77
74
|
|
|
@@ -93,33 +90,33 @@ function generateHydrationScripts(
|
|
|
93
90
|
|
|
94
91
|
// Vendor modulepreload (React, ReactDOM λ± - μΊμ ν¨μ¨ κ·Ήλν)
|
|
95
92
|
if (manifest.shared.vendor) {
|
|
96
|
-
scripts.push(`<link rel="modulepreload" href="${manifest.shared.vendor}">`);
|
|
93
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(manifest.shared.vendor)}">`);
|
|
97
94
|
}
|
|
98
95
|
if (manifest.importMap?.imports) {
|
|
99
96
|
const imports = manifest.importMap.imports;
|
|
100
97
|
// react-dom, react-dom/client λ± μΆκ° preload
|
|
101
98
|
if (imports["react-dom"] && imports["react-dom"] !== manifest.shared.vendor) {
|
|
102
|
-
scripts.push(`<link rel="modulepreload" href="${imports["react-dom"]}">`);
|
|
99
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(imports["react-dom"])}">`);
|
|
103
100
|
}
|
|
104
101
|
if (imports["react-dom/client"]) {
|
|
105
|
-
scripts.push(`<link rel="modulepreload" href="${imports["react-dom/client"]}">`);
|
|
102
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(imports["react-dom/client"])}">`);
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
// Runtime modulepreload (hydration μ€ν μ 미리 λ‘λ)
|
|
110
107
|
if (manifest.shared.runtime) {
|
|
111
|
-
scripts.push(`<link rel="modulepreload" href="${manifest.shared.runtime}">`);
|
|
108
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(manifest.shared.runtime)}">`);
|
|
112
109
|
}
|
|
113
110
|
|
|
114
111
|
// Island λ²λ€ modulepreload (μ±λ₯ μ΅μ ν - prefetch only)
|
|
115
112
|
const bundle = manifest.bundles[routeId];
|
|
116
113
|
if (bundle) {
|
|
117
|
-
scripts.push(`<link rel="modulepreload" href="${bundle.js}">`);
|
|
114
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(bundle.js)}">`);
|
|
118
115
|
}
|
|
119
116
|
|
|
120
117
|
// Runtime λ‘λ (hydrateIslands μ€ν - dynamic import μ¬μ©)
|
|
121
118
|
if (manifest.shared.runtime) {
|
|
122
|
-
scripts.push(`<script type="module" src="${manifest.shared.runtime}"></script>`);
|
|
119
|
+
scripts.push(`<script type="module" src="${escapeHtmlAttr(manifest.shared.runtime)}"></script>`);
|
|
123
120
|
}
|
|
124
121
|
|
|
125
122
|
return scripts.join("\n");
|
|
@@ -135,8 +132,8 @@ export function wrapWithIsland(
|
|
|
135
132
|
priority: HydrationPriority = "visible",
|
|
136
133
|
bundleSrc?: string
|
|
137
134
|
): string {
|
|
138
|
-
const srcAttr = bundleSrc ? ` data-mandu-src="${bundleSrc}"` : "";
|
|
139
|
-
return `<div data-mandu-island="${routeId}"${srcAttr} data-mandu-priority="${priority}">${content}</div>`;
|
|
135
|
+
const srcAttr = bundleSrc ? ` data-mandu-src="${escapeHtmlAttr(bundleSrc)}"` : "";
|
|
136
|
+
return `<div data-mandu-island="${escapeHtmlAttr(routeId)}"${srcAttr} data-mandu-priority="${escapeHtmlAttr(priority)}">${content}</div>`;
|
|
140
137
|
}
|
|
141
138
|
|
|
142
139
|
export function renderToHTML(element: ReactElement, options: SSROptions = {}): string {
|
|
@@ -160,7 +157,7 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
|
|
|
160
157
|
// - cssPathκ° stringμ΄λ©΄ ν΄λΉ κ²½λ‘ μ¬μ©
|
|
161
158
|
// - cssPathκ° false λλ undefinedμ΄λ©΄ λ§ν¬ λ―Έμ½μ
(404 λ°©μ§)
|
|
162
159
|
const cssLinkTag = cssPath && cssPath !== false
|
|
163
|
-
? `<link rel="stylesheet" href="${cssPath}${isDev ? `?t=${Date.now()}` : ""}">`
|
|
160
|
+
? `<link rel="stylesheet" href="${escapeHtmlAttr(`${cssPath}${isDev ? `?t=${Date.now()}` : ""}`)}">`
|
|
164
161
|
: "";
|
|
165
162
|
|
|
166
163
|
let content = renderToString(element);
|
|
@@ -213,11 +210,11 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
|
|
|
213
210
|
}
|
|
214
211
|
|
|
215
212
|
return `<!doctype html>
|
|
216
|
-
<html lang="${lang}">
|
|
213
|
+
<html lang="${escapeHtmlAttr(lang)}">
|
|
217
214
|
<head>
|
|
218
215
|
<meta charset="UTF-8">
|
|
219
216
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
220
|
-
<title>${title}</title>
|
|
217
|
+
<title>${escapeHtmlAttr(title)}</title>
|
|
221
218
|
${cssLinkTag}
|
|
222
219
|
${headTags}
|
|
223
220
|
</head>
|
|
@@ -247,9 +244,7 @@ function generateRouteScript(
|
|
|
247
244
|
params: extractParamsFromUrl(pattern),
|
|
248
245
|
};
|
|
249
246
|
|
|
250
|
-
const json = JSON.stringify(routeInfo)
|
|
251
|
-
.replace(/</g, "\\u003c")
|
|
252
|
-
.replace(/>/g, "\\u003e");
|
|
247
|
+
const json = escapeJsonForInlineScript(JSON.stringify(routeInfo));
|
|
253
248
|
|
|
254
249
|
return `<script>window.__MANDU_ROUTE__ = ${json};</script>`;
|
|
255
250
|
}
|
|
@@ -272,7 +267,7 @@ function generateClientRouterScript(manifest: BundleManifest): string {
|
|
|
272
267
|
|
|
273
268
|
// λΌμ°ν° λ²λ€μ΄ μμΌλ©΄ λ‘λ
|
|
274
269
|
if (manifest.shared?.router) {
|
|
275
|
-
scripts.push(`<script type="module" src="${manifest.shared.router}"></script>`);
|
|
270
|
+
scripts.push(`<script type="module" src="${escapeHtmlAttr(manifest.shared.router)}"></script>`);
|
|
276
271
|
}
|
|
277
272
|
|
|
278
273
|
return scripts.join("\n");
|
|
@@ -20,6 +20,7 @@ import { serializeProps } from "../client/serialize";
|
|
|
20
20
|
import type { Metadata, MetadataItem } from "../seo/types";
|
|
21
21
|
import { injectSEOIntoOptions, resolveSEO, type SEOOptions } from "../seo/integration/ssr";
|
|
22
22
|
import { PORTS, TIMEOUTS } from "../constants";
|
|
23
|
+
import { escapeHtmlAttr, escapeJsonForInlineScript, escapeJsString } from "./escape";
|
|
23
24
|
|
|
24
25
|
// ========== Types ==========
|
|
25
26
|
|
|
@@ -257,18 +258,13 @@ function warnStreamingCaveats(isDev: boolean): void {
|
|
|
257
258
|
* Shell μ΄ν μλ¬λ μ΄ λ°©μμΌλ‘ ν΄λΌμ΄μΈνΈμ μ λ¬
|
|
258
259
|
*/
|
|
259
260
|
function generateErrorScript(error: Error, routeId: string): string {
|
|
260
|
-
const safeMessage = error.message
|
|
261
|
-
|
|
262
|
-
.replace(/\n/g, "\\n") // μ€λ°κΏ
|
|
263
|
-
.replace(/\r/g, "\\r") // μΊλ¦¬μ§ 리ν΄
|
|
264
|
-
.replace(/</g, "\\u003c") // XSS λ°©μ§
|
|
265
|
-
.replace(/>/g, "\\u003e")
|
|
266
|
-
.replace(/"/g, "\\u0022");
|
|
261
|
+
const safeMessage = escapeJsString(error.message);
|
|
262
|
+
const safeRouteId = escapeJsString(routeId);
|
|
267
263
|
|
|
268
264
|
return `<script>
|
|
269
265
|
(function() {
|
|
270
266
|
window.__MANDU_STREAMING_ERROR__ = {
|
|
271
|
-
routeId: "${
|
|
267
|
+
routeId: "${safeRouteId}",
|
|
272
268
|
message: "${safeMessage}",
|
|
273
269
|
timestamp: ${Date.now()}
|
|
274
270
|
};
|
|
@@ -377,13 +373,13 @@ function generateHTMLShell(options: StreamingSSROptions): string {
|
|
|
377
373
|
// - cssPathκ° stringμ΄λ©΄ ν΄λΉ κ²½λ‘ μ¬μ©
|
|
378
374
|
// - cssPathκ° false λλ undefinedμ΄λ©΄ λ§ν¬ λ―Έμ½μ
(404 λ°©μ§)
|
|
379
375
|
const cssLinkTag = cssPath && cssPath !== false
|
|
380
|
-
? `<link rel="stylesheet" href="${cssPath}${isDev ? `?t=${Date.now()}` : ""}">`
|
|
376
|
+
? `<link rel="stylesheet" href="${escapeHtmlAttr(`${cssPath}${isDev ? `?t=${Date.now()}` : ""}`)}">`
|
|
381
377
|
: "";
|
|
382
378
|
|
|
383
379
|
// Import map (module scripts μ μ μμΉν΄μΌ ν¨)
|
|
384
380
|
let importMapScript = "";
|
|
385
381
|
if (bundleManifest?.importMap && Object.keys(bundleManifest.importMap.imports).length > 0) {
|
|
386
|
-
const importMapJson = JSON.stringify(bundleManifest.importMap, null, 2);
|
|
382
|
+
const importMapJson = escapeJsonForInlineScript(JSON.stringify(bundleManifest.importMap, null, 2));
|
|
387
383
|
importMapScript = `<script type="importmap">${importMapJson}</script>`;
|
|
388
384
|
}
|
|
389
385
|
|
|
@@ -415,16 +411,16 @@ function generateHTMLShell(options: StreamingSSROptions): string {
|
|
|
415
411
|
const bundle = bundleManifest.bundles[routeId];
|
|
416
412
|
const bundleSrc = bundle?.js || "";
|
|
417
413
|
const priority = hydration.priority || "visible";
|
|
418
|
-
islandOpenTag = `<div data-mandu-island="${routeId}" data-mandu-src="${bundleSrc}" data-mandu-priority="${priority}">`;
|
|
414
|
+
islandOpenTag = `<div data-mandu-island="${escapeHtmlAttr(routeId)}" data-mandu-src="${escapeHtmlAttr(bundleSrc)}" data-mandu-priority="${escapeHtmlAttr(priority)}">`;
|
|
419
415
|
}
|
|
420
416
|
|
|
421
417
|
// Import mapμ module μ€ν¬λ¦½νΈλ³΄λ€ λ¨Όμ μ μλμ΄μΌ bare specifier ν΄μ κ°λ₯
|
|
422
418
|
return `<!DOCTYPE html>
|
|
423
|
-
<html lang="${lang}">
|
|
419
|
+
<html lang="${escapeHtmlAttr(lang)}">
|
|
424
420
|
<head>
|
|
425
421
|
<meta charset="UTF-8">
|
|
426
422
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
427
|
-
<title>${title}</title>
|
|
423
|
+
<title>${escapeHtmlAttr(title)}</title>
|
|
428
424
|
${cssLinkTag}
|
|
429
425
|
${loadingStyles}
|
|
430
426
|
${importMapScript}
|
|
@@ -461,10 +457,7 @@ function generateHTMLTailContent(options: StreamingSSROptions): string {
|
|
|
461
457
|
streaming: true,
|
|
462
458
|
},
|
|
463
459
|
};
|
|
464
|
-
const json = serializeProps(wrappedData)
|
|
465
|
-
.replace(/</g, "\\u003c")
|
|
466
|
-
.replace(/>/g, "\\u003e")
|
|
467
|
-
.replace(/&/g, "\\u0026");
|
|
460
|
+
const json = escapeJsonForInlineScript(serializeProps(wrappedData));
|
|
468
461
|
scripts.push(`<script id="__MANDU_DATA__" type="application/json">${json}</script>`);
|
|
469
462
|
scripts.push(`<script>window.__MANDU_DATA_RAW__ = document.getElementById('__MANDU_DATA__').textContent;</script>`);
|
|
470
463
|
}
|
|
@@ -477,9 +470,7 @@ function generateHTMLTailContent(options: StreamingSSROptions): string {
|
|
|
477
470
|
params: {},
|
|
478
471
|
streaming: true,
|
|
479
472
|
};
|
|
480
|
-
const json = JSON.stringify(routeInfo)
|
|
481
|
-
.replace(/</g, "\\u003c")
|
|
482
|
-
.replace(/>/g, "\\u003e");
|
|
473
|
+
const json = escapeJsonForInlineScript(JSON.stringify(routeInfo));
|
|
483
474
|
scripts.push(`<script>window.__MANDU_ROUTE__ = ${json};</script>`);
|
|
484
475
|
}
|
|
485
476
|
|
|
@@ -488,39 +479,39 @@ function generateHTMLTailContent(options: StreamingSSROptions): string {
|
|
|
488
479
|
|
|
489
480
|
// 4. Vendor modulepreload (React, ReactDOM λ± - μΊμ ν¨μ¨ κ·Ήλν)
|
|
490
481
|
if (bundleManifest?.shared.vendor) {
|
|
491
|
-
scripts.push(`<link rel="modulepreload" href="${bundleManifest.shared.vendor}">`);
|
|
482
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(bundleManifest.shared.vendor)}">`);
|
|
492
483
|
}
|
|
493
484
|
if (bundleManifest?.importMap?.imports) {
|
|
494
485
|
const imports = bundleManifest.importMap.imports;
|
|
495
486
|
if (imports["react-dom"] && imports["react-dom"] !== bundleManifest.shared.vendor) {
|
|
496
|
-
scripts.push(`<link rel="modulepreload" href="${imports["react-dom"]}">`);
|
|
487
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(imports["react-dom"])}">`);
|
|
497
488
|
}
|
|
498
489
|
if (imports["react-dom/client"]) {
|
|
499
|
-
scripts.push(`<link rel="modulepreload" href="${imports["react-dom/client"]}">`);
|
|
490
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(imports["react-dom/client"])}">`);
|
|
500
491
|
}
|
|
501
492
|
}
|
|
502
493
|
|
|
503
494
|
// 5. Runtime modulepreload (hydration μ€ν μ 미리 λ‘λ)
|
|
504
495
|
if (bundleManifest?.shared.runtime) {
|
|
505
|
-
scripts.push(`<link rel="modulepreload" href="${bundleManifest.shared.runtime}">`);
|
|
496
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(bundleManifest.shared.runtime)}">`);
|
|
506
497
|
}
|
|
507
498
|
|
|
508
499
|
// 6. Island modulepreload
|
|
509
500
|
if (bundleManifest && routeId) {
|
|
510
501
|
const bundle = bundleManifest.bundles[routeId];
|
|
511
502
|
if (bundle) {
|
|
512
|
-
scripts.push(`<link rel="modulepreload" href="${bundle.js}">`);
|
|
503
|
+
scripts.push(`<link rel="modulepreload" href="${escapeHtmlAttr(bundle.js)}">`);
|
|
513
504
|
}
|
|
514
505
|
}
|
|
515
506
|
|
|
516
507
|
// 7. Runtime λ‘λ
|
|
517
508
|
if (bundleManifest?.shared.runtime) {
|
|
518
|
-
scripts.push(`<script type="module" src="${bundleManifest.shared.runtime}"></script>`);
|
|
509
|
+
scripts.push(`<script type="module" src="${escapeHtmlAttr(bundleManifest.shared.runtime)}"></script>`);
|
|
519
510
|
}
|
|
520
511
|
|
|
521
512
|
// 8. Router μ€ν¬λ¦½νΈ
|
|
522
513
|
if (enableClientRouter && bundleManifest?.shared?.router) {
|
|
523
|
-
scripts.push(`<script type="module" src="${bundleManifest.shared.router}"></script>`);
|
|
514
|
+
scripts.push(`<script type="module" src="${escapeHtmlAttr(bundleManifest.shared.router)}"></script>`);
|
|
524
515
|
}
|
|
525
516
|
|
|
526
517
|
// 9. HMR μ€ν¬λ¦½νΈ (κ°λ° λͺ¨λ)
|
|
@@ -559,16 +550,16 @@ function generateHTMLTail(options: StreamingSSROptions): string {
|
|
|
559
550
|
* Streaming μ€μ λ°μ΄ν° λμ°© μ DOMμ μ£Όμ
|
|
560
551
|
*/
|
|
561
552
|
function generateDeferredDataScript(routeId: string, key: string, data: unknown): string {
|
|
562
|
-
const json = serializeProps({ [key]: data })
|
|
563
|
-
|
|
564
|
-
|
|
553
|
+
const json = escapeJsonForInlineScript(serializeProps({ [key]: data }));
|
|
554
|
+
const safeRouteId = escapeJsString(routeId);
|
|
555
|
+
const safeKey = escapeJsString(key);
|
|
565
556
|
|
|
566
557
|
return `<script>
|
|
567
558
|
(function() {
|
|
568
559
|
window.__MANDU_DEFERRED__ = window.__MANDU_DEFERRED__ || {};
|
|
569
|
-
window.__MANDU_DEFERRED__["${
|
|
570
|
-
Object.assign(window.__MANDU_DEFERRED__["${
|
|
571
|
-
window.dispatchEvent(new CustomEvent('mandu:deferred-data', { detail: { routeId: "${
|
|
560
|
+
window.__MANDU_DEFERRED__["${safeRouteId}"] = window.__MANDU_DEFERRED__["${safeRouteId}"] || {};
|
|
561
|
+
Object.assign(window.__MANDU_DEFERRED__["${safeRouteId}"], ${json});
|
|
562
|
+
window.dispatchEvent(new CustomEvent('mandu:deferred-data', { detail: { routeId: "${safeRouteId}", key: "${safeKey}" } }));
|
|
572
563
|
})();
|
|
573
564
|
</script>`;
|
|
574
565
|
}
|
package/src/runtime/trace.ts
CHANGED
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Trace π§
|
|
3
|
-
* Lifecycle λ¨κ³λ³ μΆμ (μ΅μ
)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ManduContext } from "../filling/context";
|
|
7
|
-
|
|
8
|
-
export type TraceEvent =
|
|
9
|
-
| "request"
|
|
10
|
-
| "parse"
|
|
11
|
-
| "transform"
|
|
12
|
-
| "beforeHandle"
|
|
13
|
-
| "handle"
|
|
14
|
-
| "afterHandle"
|
|
15
|
-
| "mapResponse"
|
|
16
|
-
| "afterResponse"
|
|
17
|
-
| "error";
|
|
18
|
-
|
|
19
|
-
export type TracePhase = "begin" | "end" | "error";
|
|
20
|
-
|
|
21
|
-
export interface TraceEntry {
|
|
22
|
-
event: TraceEvent;
|
|
23
|
-
phase: TracePhase;
|
|
24
|
-
time: number;
|
|
25
|
-
name?: string;
|
|
26
|
-
error?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface TraceCollector {
|
|
30
|
-
records: TraceEntry[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface TraceReportEntry {
|
|
34
|
-
event: TraceEvent;
|
|
35
|
-
name?: string;
|
|
36
|
-
start: number;
|
|
37
|
-
end: number;
|
|
38
|
-
duration: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface TraceReport {
|
|
42
|
-
entries: TraceReportEntry[];
|
|
43
|
-
errors: TraceEntry[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const TRACE_KEY = "__mandu_trace";
|
|
47
|
-
|
|
48
|
-
const now = (): number => {
|
|
49
|
-
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
50
|
-
return performance.now();
|
|
51
|
-
}
|
|
52
|
-
return Date.now();
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export function enableTrace(ctx: ManduContext): TraceCollector {
|
|
56
|
-
const existing = ctx.get<TraceCollector>(TRACE_KEY);
|
|
57
|
-
if (existing) return existing;
|
|
58
|
-
const collector: TraceCollector = { records: [] };
|
|
59
|
-
ctx.set(TRACE_KEY, collector);
|
|
60
|
-
return collector;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function getTrace(ctx: ManduContext): TraceCollector | undefined {
|
|
64
|
-
return ctx.get<TraceCollector>(TRACE_KEY);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Build a normalized trace report with durations
|
|
69
|
-
*/
|
|
70
|
-
export function buildTraceReport(collector: TraceCollector): TraceReport {
|
|
71
|
-
const entries: TraceReportEntry[] = [];
|
|
72
|
-
const errors: TraceEntry[] = [];
|
|
73
|
-
const stacks = new Map<string, TraceEntry[]>();
|
|
74
|
-
|
|
75
|
-
for (const record of collector.records) {
|
|
76
|
-
if (record.phase === "error") {
|
|
77
|
-
errors.push(record);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const key = `${record.event}:${record.name ?? ""}`;
|
|
82
|
-
if (record.phase === "begin") {
|
|
83
|
-
const stack = stacks.get(key) ?? [];
|
|
84
|
-
stack.push(record);
|
|
85
|
-
stacks.set(key, stack);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (record.phase === "end") {
|
|
90
|
-
const stack = stacks.get(key);
|
|
91
|
-
const begin = stack?.pop();
|
|
92
|
-
if (!begin) continue;
|
|
93
|
-
entries.push({
|
|
94
|
-
event: record.event,
|
|
95
|
-
name: record.name,
|
|
96
|
-
start: begin.time,
|
|
97
|
-
end: record.time,
|
|
98
|
-
duration: record.time - begin.time,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return { entries, errors };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Convert trace report to JSON string
|
|
108
|
-
*/
|
|
109
|
-
export function formatTraceReport(report: TraceReport): string {
|
|
110
|
-
return JSON.stringify(report, null, 2);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface Tracer {
|
|
114
|
-
enabled: boolean;
|
|
115
|
-
begin: (event: TraceEvent, name?: string) => () => void;
|
|
116
|
-
error: (event: TraceEvent, err: unknown, name?: string) => void;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const NOOP_TRACER: Tracer = {
|
|
120
|
-
enabled: false,
|
|
121
|
-
begin: () => () => {},
|
|
122
|
-
error: () => {},
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
export function createTracer(ctx: ManduContext, enabled?: boolean): Tracer {
|
|
126
|
-
const shouldEnable = Boolean(enabled) || ctx.has(TRACE_KEY);
|
|
127
|
-
if (!shouldEnable) return NOOP_TRACER;
|
|
128
|
-
|
|
129
|
-
const collector = enableTrace(ctx);
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
enabled: true,
|
|
133
|
-
begin: (event, name) => {
|
|
134
|
-
collector.records.push({ event, phase: "begin", time: now(), name });
|
|
135
|
-
return () => {
|
|
136
|
-
collector.records.push({ event, phase: "end", time: now(), name });
|
|
137
|
-
};
|
|
138
|
-
},
|
|
139
|
-
error: (event, err, name) => {
|
|
140
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
-
collector.records.push({ event, phase: "error", time: now(), name, error: message });
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Trace π§
|
|
3
|
+
* Lifecycle λ¨κ³λ³ μΆμ (μ΅μ
)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ManduContext } from "../filling/context";
|
|
7
|
+
|
|
8
|
+
export type TraceEvent =
|
|
9
|
+
| "request"
|
|
10
|
+
| "parse"
|
|
11
|
+
| "transform"
|
|
12
|
+
| "beforeHandle"
|
|
13
|
+
| "handle"
|
|
14
|
+
| "afterHandle"
|
|
15
|
+
| "mapResponse"
|
|
16
|
+
| "afterResponse"
|
|
17
|
+
| "error";
|
|
18
|
+
|
|
19
|
+
export type TracePhase = "begin" | "end" | "error";
|
|
20
|
+
|
|
21
|
+
export interface TraceEntry {
|
|
22
|
+
event: TraceEvent;
|
|
23
|
+
phase: TracePhase;
|
|
24
|
+
time: number;
|
|
25
|
+
name?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TraceCollector {
|
|
30
|
+
records: TraceEntry[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TraceReportEntry {
|
|
34
|
+
event: TraceEvent;
|
|
35
|
+
name?: string;
|
|
36
|
+
start: number;
|
|
37
|
+
end: number;
|
|
38
|
+
duration: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TraceReport {
|
|
42
|
+
entries: TraceReportEntry[];
|
|
43
|
+
errors: TraceEntry[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const TRACE_KEY = "__mandu_trace";
|
|
47
|
+
|
|
48
|
+
const now = (): number => {
|
|
49
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
50
|
+
return performance.now();
|
|
51
|
+
}
|
|
52
|
+
return Date.now();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function enableTrace(ctx: ManduContext): TraceCollector {
|
|
56
|
+
const existing = ctx.get<TraceCollector>(TRACE_KEY);
|
|
57
|
+
if (existing) return existing;
|
|
58
|
+
const collector: TraceCollector = { records: [] };
|
|
59
|
+
ctx.set(TRACE_KEY, collector);
|
|
60
|
+
return collector;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getTrace(ctx: ManduContext): TraceCollector | undefined {
|
|
64
|
+
return ctx.get<TraceCollector>(TRACE_KEY);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a normalized trace report with durations
|
|
69
|
+
*/
|
|
70
|
+
export function buildTraceReport(collector: TraceCollector): TraceReport {
|
|
71
|
+
const entries: TraceReportEntry[] = [];
|
|
72
|
+
const errors: TraceEntry[] = [];
|
|
73
|
+
const stacks = new Map<string, TraceEntry[]>();
|
|
74
|
+
|
|
75
|
+
for (const record of collector.records) {
|
|
76
|
+
if (record.phase === "error") {
|
|
77
|
+
errors.push(record);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const key = `${record.event}:${record.name ?? ""}`;
|
|
82
|
+
if (record.phase === "begin") {
|
|
83
|
+
const stack = stacks.get(key) ?? [];
|
|
84
|
+
stack.push(record);
|
|
85
|
+
stacks.set(key, stack);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (record.phase === "end") {
|
|
90
|
+
const stack = stacks.get(key);
|
|
91
|
+
const begin = stack?.pop();
|
|
92
|
+
if (!begin) continue;
|
|
93
|
+
entries.push({
|
|
94
|
+
event: record.event,
|
|
95
|
+
name: record.name,
|
|
96
|
+
start: begin.time,
|
|
97
|
+
end: record.time,
|
|
98
|
+
duration: record.time - begin.time,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { entries, errors };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert trace report to JSON string
|
|
108
|
+
*/
|
|
109
|
+
export function formatTraceReport(report: TraceReport): string {
|
|
110
|
+
return JSON.stringify(report, null, 2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface Tracer {
|
|
114
|
+
enabled: boolean;
|
|
115
|
+
begin: (event: TraceEvent, name?: string) => () => void;
|
|
116
|
+
error: (event: TraceEvent, err: unknown, name?: string) => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const NOOP_TRACER: Tracer = {
|
|
120
|
+
enabled: false,
|
|
121
|
+
begin: () => () => {},
|
|
122
|
+
error: () => {},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export function createTracer(ctx: ManduContext, enabled?: boolean): Tracer {
|
|
126
|
+
const shouldEnable = Boolean(enabled) || ctx.has(TRACE_KEY);
|
|
127
|
+
if (!shouldEnable) return NOOP_TRACER;
|
|
128
|
+
|
|
129
|
+
const collector = enableTrace(ctx);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
enabled: true,
|
|
133
|
+
begin: (event, name) => {
|
|
134
|
+
collector.records.push({ event, phase: "begin", time: now(), name });
|
|
135
|
+
return () => {
|
|
136
|
+
collector.records.push({ event, phase: "end", time: now(), name });
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
error: (event, err, name) => {
|
|
140
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
+
collector.records.push({ event, phase: "error", time: now(), name, error: message });
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|