@mandujs/core 0.13.0 β†’ 0.13.2

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.
Files changed (157) hide show
  1. package/README.ko.md +4 -4
  2. package/README.md +653 -653
  3. package/package.json +1 -1
  4. package/src/bundler/build.ts +91 -91
  5. package/src/bundler/css.ts +302 -302
  6. package/src/client/Link.tsx +227 -227
  7. package/src/client/globals.ts +44 -44
  8. package/src/client/hooks.ts +267 -267
  9. package/src/client/index.ts +5 -5
  10. package/src/client/island.ts +8 -8
  11. package/src/client/router.ts +435 -435
  12. package/src/client/runtime.ts +23 -23
  13. package/src/client/serialize.ts +404 -404
  14. package/src/client/window-state.ts +101 -101
  15. package/src/config/mandu.ts +9 -0
  16. package/src/config/validate.ts +12 -0
  17. package/src/config/watcher.ts +311 -311
  18. package/src/constants.ts +40 -40
  19. package/src/content/content-layer.ts +314 -314
  20. package/src/content/content.test.ts +433 -433
  21. package/src/content/data-store.ts +245 -245
  22. package/src/content/digest.ts +133 -133
  23. package/src/content/index.ts +164 -164
  24. package/src/content/loader-context.ts +172 -172
  25. package/src/content/loaders/api.ts +216 -216
  26. package/src/content/loaders/file.ts +169 -169
  27. package/src/content/loaders/glob.ts +252 -252
  28. package/src/content/loaders/index.ts +34 -34
  29. package/src/content/loaders/types.ts +137 -137
  30. package/src/content/meta-store.ts +209 -209
  31. package/src/content/types.ts +282 -282
  32. package/src/content/watcher.ts +135 -135
  33. package/src/contract/client-safe.test.ts +42 -42
  34. package/src/contract/client-safe.ts +114 -114
  35. package/src/contract/client.ts +16 -16
  36. package/src/contract/define.ts +459 -459
  37. package/src/contract/handler.ts +10 -10
  38. package/src/contract/normalize.test.ts +276 -276
  39. package/src/contract/normalize.ts +404 -404
  40. package/src/contract/registry.test.ts +206 -206
  41. package/src/contract/registry.ts +568 -568
  42. package/src/contract/schema.ts +48 -48
  43. package/src/contract/types.ts +58 -58
  44. package/src/contract/validator.ts +32 -32
  45. package/src/devtools/ai/context-builder.ts +375 -375
  46. package/src/devtools/ai/index.ts +25 -25
  47. package/src/devtools/ai/mcp-connector.ts +465 -465
  48. package/src/devtools/client/catchers/error-catcher.ts +327 -327
  49. package/src/devtools/client/catchers/index.ts +18 -18
  50. package/src/devtools/client/catchers/network-proxy.ts +363 -363
  51. package/src/devtools/client/components/index.ts +39 -39
  52. package/src/devtools/client/components/kitchen-root.tsx +362 -362
  53. package/src/devtools/client/components/mandu-character.tsx +241 -241
  54. package/src/devtools/client/components/overlay.tsx +368 -368
  55. package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
  56. package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
  57. package/src/devtools/client/components/panel/index.ts +32 -32
  58. package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
  59. package/src/devtools/client/components/panel/network-panel.tsx +292 -292
  60. package/src/devtools/client/components/panel/panel-container.tsx +259 -259
  61. package/src/devtools/client/filters/context-filters.ts +282 -282
  62. package/src/devtools/client/filters/index.ts +16 -16
  63. package/src/devtools/client/index.ts +63 -63
  64. package/src/devtools/client/persistence.ts +335 -335
  65. package/src/devtools/client/state-manager.ts +478 -478
  66. package/src/devtools/design-tokens.ts +263 -263
  67. package/src/devtools/hook/create-hook.ts +207 -207
  68. package/src/devtools/hook/index.ts +13 -13
  69. package/src/devtools/index.ts +439 -439
  70. package/src/devtools/init.ts +266 -266
  71. package/src/devtools/protocol.ts +237 -237
  72. package/src/devtools/server/index.ts +17 -17
  73. package/src/devtools/server/source-context.ts +444 -444
  74. package/src/devtools/types.ts +319 -319
  75. package/src/devtools/worker/index.ts +25 -25
  76. package/src/devtools/worker/redaction-worker.ts +222 -222
  77. package/src/devtools/worker/worker-manager.ts +409 -409
  78. package/src/error/domains.ts +265 -265
  79. package/src/error/result.ts +46 -46
  80. package/src/error/types.ts +6 -6
  81. package/src/errors/extractor.ts +409 -409
  82. package/src/errors/index.ts +19 -19
  83. package/src/filling/auth.ts +308 -308
  84. package/src/filling/context.ts +24 -1
  85. package/src/filling/deps.ts +238 -238
  86. package/src/filling/index.ts +4 -0
  87. package/src/filling/sse-catchup.test.ts +56 -0
  88. package/src/filling/sse-catchup.ts +67 -0
  89. package/src/filling/sse.test.ts +168 -0
  90. package/src/filling/sse.ts +162 -0
  91. package/src/generator/index.ts +3 -3
  92. package/src/guard/analyzer.ts +360 -360
  93. package/src/guard/ast-analyzer.ts +806 -806
  94. package/src/guard/contract-guard.ts +9 -9
  95. package/src/guard/file-type.test.ts +24 -24
  96. package/src/guard/presets/atomic.ts +70 -70
  97. package/src/guard/presets/clean.ts +77 -77
  98. package/src/guard/presets/fsd.ts +79 -79
  99. package/src/guard/presets/hexagonal.ts +68 -68
  100. package/src/guard/presets/index.ts +291 -291
  101. package/src/guard/reporter.ts +445 -445
  102. package/src/guard/rules.ts +12 -12
  103. package/src/guard/statistics.ts +578 -578
  104. package/src/guard/suggestions.ts +358 -358
  105. package/src/guard/types.ts +348 -348
  106. package/src/guard/validator.ts +834 -834
  107. package/src/guard/watcher.ts +404 -404
  108. package/src/index.ts +6 -1
  109. package/src/intent/index.ts +310 -310
  110. package/src/island/index.ts +304 -304
  111. package/src/logging/index.ts +22 -22
  112. package/src/logging/transports.ts +365 -365
  113. package/src/plugins/index.ts +38 -38
  114. package/src/plugins/registry.ts +377 -377
  115. package/src/plugins/types.ts +363 -363
  116. package/src/report/index.ts +1 -1
  117. package/src/router/fs-patterns.ts +387 -387
  118. package/src/router/fs-scanner.ts +497 -497
  119. package/src/runtime/boundary.tsx +232 -232
  120. package/src/runtime/compose.ts +222 -222
  121. package/src/runtime/escape.ts +44 -0
  122. package/src/runtime/lifecycle.ts +381 -381
  123. package/src/runtime/logger.test.ts +345 -345
  124. package/src/runtime/logger.ts +677 -677
  125. package/src/runtime/router.test.ts +476 -476
  126. package/src/runtime/router.ts +105 -105
  127. package/src/runtime/security.ts +155 -155
  128. package/src/runtime/server.ts +257 -0
  129. package/src/runtime/session-key.ts +328 -328
  130. package/src/runtime/ssr.ts +16 -21
  131. package/src/runtime/streaming-ssr.ts +24 -33
  132. package/src/runtime/trace.ts +144 -144
  133. package/src/seo/index.ts +214 -214
  134. package/src/seo/integration/ssr.ts +307 -307
  135. package/src/seo/render/basic.ts +427 -427
  136. package/src/seo/render/index.ts +143 -143
  137. package/src/seo/render/jsonld.ts +539 -539
  138. package/src/seo/render/opengraph.ts +191 -191
  139. package/src/seo/render/robots.ts +116 -116
  140. package/src/seo/render/sitemap.ts +137 -137
  141. package/src/seo/render/twitter.ts +126 -126
  142. package/src/seo/resolve/index.ts +353 -353
  143. package/src/seo/resolve/opengraph.ts +143 -143
  144. package/src/seo/resolve/robots.ts +73 -73
  145. package/src/seo/resolve/title.ts +94 -94
  146. package/src/seo/resolve/twitter.ts +73 -73
  147. package/src/seo/resolve/url.ts +97 -97
  148. package/src/seo/routes/index.ts +290 -290
  149. package/src/seo/types.ts +575 -575
  150. package/src/slot/validator.ts +39 -39
  151. package/src/spec/index.ts +3 -3
  152. package/src/spec/load.ts +76 -76
  153. package/src/spec/lock.ts +56 -56
  154. package/src/utils/bun.ts +8 -8
  155. package/src/utils/lru-cache.ts +75 -75
  156. package/src/utils/safe-io.ts +188 -188
  157. package/src/utils/string-safe.ts +298 -298
@@ -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
- .replace(/\\/g, "\\\\") // λ°±μŠ¬λž˜μ‹œ λ¨Όμ € (λ‹€λ₯Έ μ΄μŠ€μΌ€μ΄ν”„μ— 영ν–₯)
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: "${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
- .replace(/</g, "\\u003c")
564
- .replace(/>/g, "\\u003e");
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__["${routeId}"] = window.__MANDU_DEFERRED__["${routeId}"] || {};
570
- Object.assign(window.__MANDU_DEFERRED__["${routeId}"], ${json});
571
- window.dispatchEvent(new CustomEvent('mandu:deferred-data', { detail: { routeId: "${routeId}", key: "${key}" } }));
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
  }
@@ -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
+ }