@withl5e/l5e 0.2.3 → 0.2.5

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.
@@ -18,6 +18,7 @@ import {
18
18
  getHeadContent,
19
19
  getIslandEntries,
20
20
  getSchemas,
21
+ getSsrIslands,
21
22
  jsxFactory as h,
22
23
  Head,
23
24
  pushMetadata,
@@ -34,6 +35,69 @@ import routeHandler from 'virtual:l5e-route';
34
35
  import { globalLoader } from 'virtual:l5e-global-loader';
35
36
  // @ts-ignore - Virtual modules provided by Vite plugin
36
37
  export { loadMiddleware } from 'virtual:l5e-middleware';
38
+ // @ts-ignore - Virtual modules provided by Vite plugin
39
+ import { islandModules } from 'virtual:l5e-islands';
40
+
41
+ /**
42
+ * Fill `ssr` island placeholders with server-rendered HTML.
43
+ *
44
+ * During the synchronous render pass, each `<ClientIsland ssr>` emits a unique
45
+ * token (as `<!--token-->` body + `data-island-ssr="token"`). Here — after the
46
+ * pass — we can `await import()` the actual React component and renderToString it,
47
+ * then string-replace the token. Only islands present on this page are imported.
48
+ *
49
+ * On failure we strip both the token and the `data-island-ssr` attribute so the
50
+ * client cleanly falls back to a client-only mount (no hydration mismatch).
51
+ */
52
+ async function fillSsrIslands(htmlBody: string): Promise<string> {
53
+ const pending = getSsrIslands();
54
+ if (pending.length === 0) return htmlBody;
55
+
56
+ let renderToString: (el: any) => string;
57
+ let createElement: (type: any, props: any) => any;
58
+ try {
59
+ [{ renderToString }, { createElement }] = await Promise.all([
60
+ import('react-dom/server'),
61
+ import('react'),
62
+ ]);
63
+ } catch (err) {
64
+ console.error('[l5e-island] SSR requires react-dom/server + react:', err);
65
+ // Strip every pending token/attr → client-only fallback for all.
66
+ for (const island of pending) {
67
+ htmlBody = htmlBody
68
+ .replace(`<!--${island.token}-->`, '')
69
+ .replace(` data-island-ssr="${island.token}"`, '');
70
+ }
71
+ return htmlBody;
72
+ }
73
+
74
+ for (const island of pending) {
75
+ try {
76
+ const loader = islandModules['/' + island.src];
77
+ if (!loader) {
78
+ throw new Error(`island module not found in glob: /${island.src}`);
79
+ }
80
+ const mod: any = await loader();
81
+ const Component = mod[island.name] ?? mod.default;
82
+ if (!Component) {
83
+ throw new Error(`no export "${island.name}" or default in /${island.src}`);
84
+ }
85
+ const out = renderToString(createElement(Component, island.props));
86
+ // Replacer fn (not a string) so `$`-sequences in `out` (e.g. "$&", "$$")
87
+ // aren't interpreted as special replacement patterns.
88
+ htmlBody = htmlBody
89
+ .replace(`<!--${island.token}-->`, () => out)
90
+ .replace(`data-island-ssr="${island.token}"`, 'data-island-ssr="1"');
91
+ } catch (err) {
92
+ console.error(`[l5e-island] Failed to SSR island "${island.name}":`, err);
93
+ htmlBody = htmlBody
94
+ .replace(`<!--${island.token}-->`, '')
95
+ .replace(` data-island-ssr="${island.token}"`, '');
96
+ }
97
+ }
98
+
99
+ return htmlBody;
100
+ }
37
101
 
38
102
  export interface RawResponse {
39
103
  body: string | Buffer;
@@ -339,7 +403,9 @@ export async function render(url: string, requestInfo: RequestInfo = {}): Promis
339
403
  });
340
404
 
341
405
  // Render component (có thể có Head components khác)
342
- const htmlBody = renderJsxToHtmlString(h(Component, props));
406
+ let htmlBody = renderJsxToHtmlString(h(Component, props));
407
+ // Fill any opt-in `ssr` island placeholders with server-rendered React HTML.
408
+ htmlBody = await fillSsrIslands(htmlBody);
343
409
  const clientEntries = getClientJsEntries();
344
410
  const cssEntries = getCssEntries();
345
411
  const islandEntries = getIslandEntries();
@@ -65,10 +65,23 @@ interface IslandEntry {
65
65
  name: string; // "Counter" — export name
66
66
  }
67
67
 
68
+ /**
69
+ * A pending server-side render request for an `ssr` island.
70
+ * Collected during the synchronous render pass and filled in afterwards by
71
+ * entry-server (which can `await import()` the component + call renderToString).
72
+ */
73
+ interface SsrIslandEntry {
74
+ token: string; // unique placeholder token embedded in the HTML body
75
+ src: string; // "src/views/.../Counter.tsx" — manifest-compatible path (no leading slash)
76
+ name: string; // "Counter" — export name
77
+ props: Record<string, any>;
78
+ }
79
+
68
80
  interface RenderContext {
69
81
  clientJsRegistry: Array<{ path: string; from: string }>;
70
82
  cssRegistry: Array<{ path: string; from: string }>;
71
83
  islandRegistry: IslandEntry[];
84
+ ssrIslands: SsrIslandEntry[];
72
85
  cacheTags: Set<string>;
73
86
  headRegistry: HeadEntry[]; // Thay vì JSXChild[]
74
87
  metadataStack: Metadata[]; // Stack để track metadata hierarchy
@@ -85,6 +98,7 @@ function createRequestContext(requestInfo: RequestInfo): RenderContext {
85
98
  clientJsRegistry: [],
86
99
  cssRegistry: [],
87
100
  islandRegistry: [],
101
+ ssrIslands: [],
88
102
  cacheTags: new Set(),
89
103
  headRegistry: [],
90
104
  metadataStack: [],
@@ -119,6 +133,25 @@ export function getIslandEntries(): IslandEntry[] {
119
133
  return context.islandRegistry.slice();
120
134
  }
121
135
 
136
+ /**
137
+ * Register a pending SSR island render. Returns a unique placeholder token that
138
+ * the caller embeds (as raw HTML) in the island's body. entry-server replaces
139
+ * the token with the server-rendered component HTML after the sync render pass.
140
+ */
141
+ export function registerSsrIsland(src: string, name: string, props: Record<string, any>): string {
142
+ const renderContext = renderStore.getStore();
143
+ if (!renderContext) return '';
144
+ const token = `__L5E_SSR_${renderContext.ssrIslands.length}__`;
145
+ renderContext.ssrIslands.push({ token, src, name, props });
146
+ return token;
147
+ }
148
+
149
+ export function getSsrIslands(): SsrIslandEntry[] {
150
+ const context = renderStore.getStore();
151
+ if (!context) return [];
152
+ return context.ssrIslands.slice();
153
+ }
154
+
122
155
  export function useCss(path: string): string {
123
156
  if (typeof path === 'string' && path.length > 0) {
124
157
  const renderContext = renderStore.getStore();
@@ -356,12 +356,15 @@ async function createPageResponse({
356
356
 
357
357
  const templateWithLang = rendered.lang ? applyHtmlLang(template, rendered.lang) : template;
358
358
 
359
+ // Replacer fns (not strings) so `$`-sequences in rendered HTML — e.g. an SSR
360
+ // island body or user text containing "$$" / "$&" — aren't interpreted as
361
+ // special String.replace replacement patterns.
359
362
  const html = rendered.rawHtml
360
363
  ? rendered.html || ''
361
364
  : templateWithLang
362
- .replace(`<!--app-head-->`, (rendered.head ?? '') + extraHead + cssHtml)
363
- .replace(`<!--app-html-->`, rendered.html ?? '')
364
- .replace(`<!--app-scripts-->`, scriptsHtml);
365
+ .replace(`<!--app-head-->`, () => (rendered.head ?? '') + extraHead + cssHtml)
366
+ .replace(`<!--app-html-->`, () => rendered.html ?? '')
367
+ .replace(`<!--app-scripts-->`, () => scriptsHtml);
365
368
 
366
369
  const headers = new Headers({
367
370
  'Content-Type': 'text/html',
@@ -16,6 +16,7 @@ const VIRTUAL_L5E_ROUTE = 'virtual:l5e-route';
16
16
  const VIRTUAL_L5E_SSR_ENTRY = 'virtual:l5e-ssr-entry';
17
17
  const VIRTUAL_L5E_GLOBAL_LOADER = 'virtual:l5e-global-loader';
18
18
  const VIRTUAL_L5E_ISLAND_STRATEGIES = 'virtual:l5e-island-strategies';
19
+ const VIRTUAL_L5E_ISLANDS = 'virtual:l5e-islands';
19
20
  const VIRTUAL_L5E_ACTIONS = 'virtual:l5e-actions';
20
21
  const VIRTUAL_L5E_MIDDLEWARE = 'virtual:l5e-middleware';
21
22
 
@@ -553,6 +554,9 @@ export function coreVite(): Plugin {
553
554
  if (id === VIRTUAL_L5E_ISLAND_STRATEGIES) {
554
555
  return '\0' + VIRTUAL_L5E_ISLAND_STRATEGIES;
555
556
  }
557
+ if (id === VIRTUAL_L5E_ISLANDS) {
558
+ return '\0' + VIRTUAL_L5E_ISLANDS;
559
+ }
556
560
  if (id === VIRTUAL_L5E_ACTIONS) {
557
561
  return '\0' + VIRTUAL_L5E_ACTIONS;
558
562
  }
@@ -772,6 +776,14 @@ export async function loadMiddleware() {
772
776
  `;
773
777
  }
774
778
 
779
+ // Virtual module: l5e-islands
780
+ // Lazy glob of all React island components so the SSR entry can import a
781
+ // component by its __src path and renderToString() it. Non-eager → only
782
+ // islands actually present on a page (with ssr) get imported at runtime.
783
+ if (id === '\0' + VIRTUAL_L5E_ISLANDS) {
784
+ return `export const islandModules = import.meta.glob('/src/**/react/*.{tsx,jsx}');`;
785
+ }
786
+
775
787
  // Virtual module: l5e-island-strategies
776
788
  if (id === '\0' + VIRTUAL_L5E_ISLAND_STRATEGIES) {
777
789
  // Check if src/island-strategies.ts exists
@@ -1,4 +1,10 @@
1
- import { jsxFactory, registerIsland, type JSXChild, type JSXNode } from '../core/jsx-runtime';
1
+ import {
2
+ jsxFactory,
3
+ registerIsland,
4
+ registerSsrIsland,
5
+ type JSXChild,
6
+ type JSXNode,
7
+ } from '../core/jsx-runtime';
2
8
 
3
9
  /**
4
10
  * Derive component name from `from` path.
@@ -21,6 +27,13 @@ export interface ClientIslandProps {
21
27
  id?: string;
22
28
  children?: JSXChild | JSXChild[];
23
29
 
30
+ /**
31
+ * Opt-in: server-render the component into the placeholder and hydrate on the
32
+ * client (instead of the default client-only mount). Component must be
33
+ * SSR-safe (no top-level browser access) and props must be JSON-serializable.
34
+ */
35
+ ssr?: boolean;
36
+
24
37
  /** INTERNAL — injected by vite-plugin. Format: "[name]_[hash]" */
25
38
  __key?: string;
26
39
  /** INTERNAL — injected by vite-plugin. Manifest-compatible source path with extension */
@@ -36,6 +49,7 @@ export function ClientIsland(attrs: ClientIslandProps): JSXNode {
36
49
  class: className,
37
50
  id,
38
51
  children,
52
+ ssr,
39
53
  __key,
40
54
  __src,
41
55
  } = attrs;
@@ -44,6 +58,8 @@ export function ClientIsland(attrs: ClientIslandProps): JSXNode {
44
58
 
45
59
  // Register island in render context (like useClientJs)
46
60
  // server.ts will use this to generate per-page window.__L5E_ISLANDS__
61
+ // (needed for BOTH client-only mount AND ssr hydration — the client still
62
+ // loads the component chunk via this registry).
47
63
  if (__key && __src) {
48
64
  registerIsland(__key, __src, componentName);
49
65
  }
@@ -59,6 +75,25 @@ export function ClientIsland(attrs: ClientIslandProps): JSXNode {
59
75
  dataAttrs['data-island-opts'] = mountOpts;
60
76
  }
61
77
 
78
+ // SSR opt-in: register a pending server render and embed a unique token as the
79
+ // placeholder body. entry-server replaces the token with renderToString() output
80
+ // after the synchronous render pass, then normalizes data-island-ssr to "1".
81
+ // The attribute value holds the token so the post-pass can target this exact
82
+ // island (and strip the attribute on render failure → clean client-only fallback).
83
+ if (ssr && __src) {
84
+ const token = registerSsrIsland(__src, componentName, props);
85
+ if (token) {
86
+ return jsxFactory('div', {
87
+ ...dataAttrs,
88
+ 'data-island-ssr': token,
89
+ ...(id ? { id } : {}),
90
+ class: className ? `l5e-island ${className}` : 'l5e-island',
91
+ // Raw token comment as body; replaced with SSR HTML in entry-server.
92
+ setHtml: `<!--${token}-->`,
93
+ });
94
+ }
95
+ }
96
+
62
97
  return jsxFactory(
63
98
  'div',
64
99
  {
@@ -77,6 +77,7 @@ function discoverIslands(): IslandMeta[] {
77
77
  props: JSON.parse(el.getAttribute('data-island-props') || '{}'),
78
78
  mount: el.getAttribute('data-island-mount') || 'load',
79
79
  mountOpts: el.getAttribute('data-island-opts') || undefined,
80
+ ssr: el.hasAttribute('data-island-ssr'),
80
81
  }));
81
82
  }
82
83
 
@@ -97,7 +98,7 @@ function createMountFn(island: IslandMeta): () => Promise<void> {
97
98
  }
98
99
 
99
100
  try {
100
- const [{ createRoot }, { createElement }, mod] = await Promise.all([
101
+ const [reactDomClient, { createElement }, mod] = await Promise.all([
101
102
  import('react-dom/client'),
102
103
  import('react'),
103
104
  import(/* @vite-ignore */ url),
@@ -109,8 +110,14 @@ function createMountFn(island: IslandMeta): () => Promise<void> {
109
110
  return;
110
111
  }
111
112
 
112
- const root = createRoot(island.element);
113
- root.render(createElement(Component, island.props));
113
+ if (island.ssr) {
114
+ // Server already rendered this component into the element → hydrate it.
115
+ reactDomClient.hydrateRoot(island.element, createElement(Component, island.props));
116
+ } else {
117
+ // Client-only mount (default): fresh render into an empty placeholder.
118
+ const root = reactDomClient.createRoot(island.element);
119
+ root.render(createElement(Component, island.props));
120
+ }
114
121
  } catch (error) {
115
122
  console.error(`[l5e-island] Failed to mount "${island.registryKey}":`, error);
116
123
  }
@@ -19,6 +19,7 @@ export interface IslandMeta {
19
19
  props: Record<string, any>;
20
20
  mount: string; // Strategy name
21
21
  mountOpts?: string; // Options passed to strategy function
22
+ ssr?: boolean; // Server-rendered → hydrate instead of fresh client mount
22
23
  }
23
24
 
24
25
  export interface IslandEntry {
@@ -1 +0,0 @@
1
- {"version":3,"file":"jsx-runtime-DVQ9eOji.js","sources":["../src/core/head-priority.ts","../src/seo/mergeMetadata.ts","../src/core/const.ts","../src/core/jsx-runtime.ts"],"sourcesContent":["/**\n * Priority constants for Head component rendering order\n * Lower numbers render first (higher priority)\n */\nexport const HEAD_PRIORITY = {\n CRITICAL: 0, // charset, viewport - must be first\n HIGH: 10, // title, description, canonical\n MEDIUM: 50, // meta tags, robots\n SEO: 80, // OpenGraph, Twitter\n LOW: 100, // Custom head elements\n SCRIPTS: 200, // Scripts\n STYLES: 300, // Styles\n} as const;\n\nexport type HeadPriority = (typeof HEAD_PRIORITY)[keyof typeof HEAD_PRIORITY] | number;\n","import type { Metadata } from './types';\n\n/**\n * Merges parent and child metadata objects\n * Follows Next.js shallow merge pattern with deep merge for nested objects\n *\n * @param parent - Parent metadata (from layout/global loader)\n * @param child - Child metadata (from page/view loader)\n * @returns Merged metadata object\n */\nexport function mergeMetadata(\n parent: Metadata | null | undefined,\n child: Metadata | null | undefined,\n): Metadata {\n // If no parent, return child (or empty object)\n if (!parent) {\n return child || {};\n }\n\n // If no child, return parent\n if (!child) {\n return parent;\n }\n\n // Shallow merge base properties (child overrides parent)\n const merged: Metadata = {\n ...parent,\n ...child,\n };\n\n // Deep merge for nested objects\n // OpenGraph: merge nested properties\n if (child.openGraph || parent.openGraph) {\n if (child.openGraph && parent.openGraph) {\n merged.openGraph = {\n ...parent.openGraph,\n ...child.openGraph,\n // Deep merge for nested arrays/objects in openGraph\n images: child.openGraph.images ?? parent.openGraph.images,\n videos: child.openGraph.videos ?? parent.openGraph.videos,\n audio: child.openGraph.audio ?? parent.openGraph.audio,\n alternateLocale: child.openGraph.alternateLocale ?? parent.openGraph.alternateLocale,\n authors: child.openGraph.authors ?? parent.openGraph.authors,\n tags: child.openGraph.tags ?? parent.openGraph.tags,\n };\n } else {\n merged.openGraph = child.openGraph || parent.openGraph;\n }\n }\n\n // Twitter: merge nested properties\n if (child.twitter || parent.twitter) {\n if (child.twitter && parent.twitter) {\n merged.twitter = {\n ...parent.twitter,\n ...child.twitter,\n // Deep merge for nested objects\n images: child.twitter.images ?? parent.twitter.images,\n app: child.twitter.app\n ? {\n ...parent.twitter.app,\n ...child.twitter.app,\n id: {\n ...parent.twitter.app?.id,\n ...child.twitter.app.id,\n },\n url: {\n ...parent.twitter.app?.url,\n ...child.twitter.app.url,\n },\n }\n : parent.twitter.app,\n };\n } else {\n merged.twitter = child.twitter || parent.twitter;\n }\n }\n\n // Icons: merge nested properties\n if (child.icons || parent.icons) {\n if (child.icons && parent.icons) {\n merged.icons = {\n icon: child.icons.icon ?? parent.icons.icon,\n shortcut: child.icons.shortcut ?? parent.icons.shortcut,\n apple: child.icons.apple ?? parent.icons.apple,\n other: child.icons.other ?? parent.icons.other,\n };\n } else {\n merged.icons = child.icons || parent.icons;\n }\n }\n\n // Verification: merge nested properties\n if (child.verification || parent.verification) {\n if (child.verification && parent.verification) {\n merged.verification = {\n ...parent.verification,\n ...child.verification,\n // Deep merge for other verification tags\n other: child.verification.other\n ? {\n ...parent.verification.other,\n ...child.verification.other,\n }\n : parent.verification.other,\n me: child.verification.me ?? parent.verification.me,\n };\n } else {\n merged.verification = child.verification || parent.verification;\n }\n }\n\n // AppLinks: merge nested properties\n if (child.appLinks || parent.appLinks) {\n if (child.appLinks && parent.appLinks) {\n merged.appLinks = {\n ios: child.appLinks.ios\n ? {\n ...parent.appLinks.ios,\n ...child.appLinks.ios,\n }\n : parent.appLinks.ios,\n android: child.appLinks.android\n ? {\n ...parent.appLinks.android,\n ...child.appLinks.android,\n }\n : parent.appLinks.android,\n web: child.appLinks.web\n ? {\n ...parent.appLinks.web,\n ...child.appLinks.web,\n }\n : parent.appLinks.web,\n };\n } else {\n merged.appLinks = child.appLinks || parent.appLinks;\n }\n }\n\n // FormatDetection: merge nested properties\n if (child.formatDetection || parent.formatDetection) {\n if (child.formatDetection && parent.formatDetection) {\n merged.formatDetection = {\n ...parent.formatDetection,\n ...child.formatDetection,\n };\n } else {\n merged.formatDetection = child.formatDetection || parent.formatDetection;\n }\n }\n\n // Viewport: merge if both are objects\n if (child.viewport && parent.viewport) {\n if (typeof child.viewport === 'object' && typeof parent.viewport === 'object') {\n merged.viewport = {\n ...parent.viewport,\n ...child.viewport,\n } as Metadata['viewport'];\n } else {\n // If either is string, child overrides\n merged.viewport = child.viewport;\n }\n }\n\n // Robots: merge if both are objects\n if (child.robots && parent.robots) {\n if (typeof child.robots === 'object' && typeof parent.robots === 'object') {\n merged.robots = {\n ...parent.robots,\n ...child.robots,\n } as Metadata['robots'];\n } else {\n // If either is string, child overrides\n merged.robots = child.robots;\n }\n }\n\n // Array fields: child overrides parent (shallow merge behavior)\n // These are already handled by spread operator above\n // But we explicitly handle them for clarity:\n merged.keywords = child.keywords ?? parent.keywords;\n merged.themeColor = child.themeColor ?? parent.themeColor;\n merged.archives = child.archives ?? parent.archives;\n merged.assets = child.assets ?? parent.assets;\n\n // Other: merge objects\n if (child.other || parent.other) {\n if (child.other && parent.other) {\n merged.other = {\n ...parent.other,\n ...child.other,\n };\n } else {\n merged.other = child.other || parent.other;\n }\n }\n\n return merged;\n}\n","// Marker object để đánh dấu raw HTML không escape\nexport const RAW_HTML_MARKER = Symbol('rawHtml');\n","import { mergeMetadata } from '../seo/mergeMetadata';\nimport type { Metadata } from '../seo/types';\nimport { RAW_HTML_MARKER } from './const';\nimport { RequestInfo } from './entry-server';\nimport { HEAD_PRIORITY, type HeadPriority } from './head-priority';\n\nexport type JSXChild =\n | string\n | number\n | boolean\n | null\n | undefined\n | JSXNode\n | JSXChild[]\n | RawHtmlObject\n | HtmlContentObject;\n\nexport type RawHtmlObject = {\n [RAW_HTML_MARKER]: true;\n content: string;\n};\n\nexport type HtmlContentObject = {\n htmlContent: string;\n};\n\nexport type RenderedNode = {\n string: string;\n};\n\nexport type JSXNode = {\n type: string | ((props: any) => JSXChild);\n props: Record<string, any>;\n children: JSXChild[];\n};\n\nexport function jsxFactory(type: any, props: any, ...children: any): JSXNode {\n return { type, props: props || {}, children: children.flat() };\n}\n\nexport function Fragment({\n children,\n ...props\n}: { children?: JSXChild; setHtml?: unknown } & Record<string, any>): JSXChild {\n // Hỗ trợ setHtml cho Fragment\n if (props.setHtml !== undefined) {\n // Trả về object đặc biệt để không bị escape\n return { [RAW_HTML_MARKER]: true, content: props.setHtml?.toString() || '' };\n }\n return children;\n}\n\n// AsyncLocalStorage for render context (request-level)\nimport { AsyncLocalStorage } from 'async_hooks';\n\ninterface HeadEntry {\n content: JSXChild;\n priority: number; // Số càng nhỏ, render càng sớm\n source?: string; // Để debug (ví dụ: 'layout', 'page', 'seo')\n}\n\ninterface IslandEntry {\n key: string; // \"Counter_a3f2\" — registry key\n src: string; // \"src/views/.../Counter.tsx\" — manifest-compatible path\n name: string; // \"Counter\" — export name\n}\n\ninterface RenderContext {\n clientJsRegistry: Array<{ path: string; from: string }>;\n cssRegistry: Array<{ path: string; from: string }>;\n islandRegistry: IslandEntry[];\n cacheTags: Set<string>;\n headRegistry: HeadEntry[]; // Thay vì JSXChild[]\n metadataStack: Metadata[]; // Stack để track metadata hierarchy\n schemaRegistry: Array<Record<string, any>>; // Schema.org structured data từ loaders\n request: RequestInfo;\n viewName?: string; // View name from route handler\n}\n\nconst renderStore = new AsyncLocalStorage<RenderContext>();\n\n// Create context for each request\nfunction createRequestContext(requestInfo: RequestInfo): RenderContext {\n return {\n clientJsRegistry: [],\n cssRegistry: [],\n islandRegistry: [],\n cacheTags: new Set(),\n headRegistry: [],\n metadataStack: [],\n schemaRegistry: [],\n request: requestInfo,\n };\n}\n\nexport function useClientJs(path: string): string {\n if (typeof path === 'string' && path.length > 0) {\n const renderContext = renderStore.getStore();\n if (renderContext) {\n renderContext.clientJsRegistry.push({ path, from: 'Unknown' });\n }\n }\n return '';\n}\n\nexport function registerIsland(key: string, src: string, name: string): void {\n const renderContext = renderStore.getStore();\n if (renderContext) {\n // Dedupe by key\n if (!renderContext.islandRegistry.some((e) => e.key === key)) {\n renderContext.islandRegistry.push({ key, src, name });\n }\n }\n}\n\nexport function getIslandEntries(): IslandEntry[] {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.islandRegistry.slice();\n}\n\nexport function useCss(path: string): string {\n if (typeof path === 'string' && path.length > 0) {\n const renderContext = renderStore.getStore();\n if (renderContext) {\n renderContext.cssRegistry.push({ path, from: 'Unknown' });\n }\n }\n return '';\n}\n\n// Wrapper to run render in async context\nexport function runInRenderContext<T>(\n renderFn: () => T | Promise<T>,\n requestInfo: RequestInfo,\n viewName?: string,\n): Promise<T> {\n const context = createRequestContext(requestInfo);\n if (viewName) {\n context.viewName = viewName;\n }\n return renderStore.run(context, () => Promise.resolve(renderFn()));\n}\n\n// Set view name in current render context\nexport function setViewName(viewName: string): void {\n const context = renderStore.getStore();\n if (context) {\n context.viewName = viewName;\n }\n}\n\n// Get entries from current context\nexport function getClientJsEntries(): Array<{ path: string; from: string }> {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.clientJsRegistry.slice();\n}\n\n// Get cache tags from current context\nexport function getCacheTags(): string[] {\n const context = renderStore.getStore();\n if (!context) return [];\n return Array.from(context.cacheTags);\n}\n\n// Add cache tags to current context\nexport function addCacheTag(tag: string | string[] | Record<string, boolean>): void {\n const context = renderStore.getStore();\n if (!context) return;\n\n if (Array.isArray(tag)) {\n tag.forEach((t) => {\n if (typeof t === 'string' && t.trim()) {\n context.cacheTags.add(t.trim());\n }\n });\n } else if (typeof tag === 'string' && tag.trim()) {\n context.cacheTags.add(tag.trim());\n } else if (typeof tag === 'object' && tag !== null) {\n Object.entries(tag).forEach(([key, value]) => {\n if (value && typeof key === 'string' && key.trim()) {\n context.cacheTags.add(key.trim());\n }\n });\n }\n}\n\n// Get CSS entries from current context\nexport function getCssEntries(): Array<{ path: string; from: string }> {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.cssRegistry.slice();\n}\n\n// Head component to collect head elements with priority support\nexport function Head({\n children,\n priority = HEAD_PRIORITY.LOW, // Default priority\n}: {\n children?: JSXChild;\n priority?: HeadPriority;\n}): null {\n const renderContext = renderStore.getStore();\n if (renderContext && children) {\n renderContext.headRegistry.push({\n content: children,\n priority: typeof priority === 'number' ? priority : HEAD_PRIORITY.LOW,\n source: 'manual',\n });\n\n // Sort theo priority sau mỗi lần push để đảm bảo thứ tự đúng\n renderContext.headRegistry.sort((a, b) => a.priority - b.priority);\n }\n return null;\n}\n\n// Get head content from current context (already sorted by priority)\nexport function getHeadContent(): JSXChild[] {\n const context = renderStore.getStore();\n if (!context) return [];\n // Đã được sort trong Head component, chỉ cần map để lấy content\n return context.headRegistry.map((entry) => entry.content);\n}\n\n// Push metadata to stack (for hierarchical metadata support)\nexport function pushMetadata(metadata: Metadata): void {\n const context = renderStore.getStore();\n if (context && metadata) {\n context.metadataStack.push(metadata);\n }\n}\n\n// Resolve and merge all metadata from stack (root → leaf)\nexport function resolveMetadata(): Metadata | null {\n const context = renderStore.getStore();\n if (!context || context.metadataStack.length === 0) {\n return null;\n }\n\n // Merge from root → leaf (reduce left to right)\n return context.metadataStack.reduce(\n (acc, current) => mergeMetadata(acc, current),\n null as Metadata | null,\n );\n}\n\n// Push schema to registry (for schema markup from loaders)\n// Accepts schema-dts types (WithContext<T> or array of schemas)\nexport function pushSchema(schema: any | Array<any>): void {\n const context = renderStore.getStore();\n if (!context) return;\n\n if (Array.isArray(schema)) {\n context.schemaRegistry.push(...schema);\n } else {\n context.schemaRegistry.push(schema);\n }\n}\n\n// Get all schemas from registry\nexport function getSchemas(): Array<Record<string, any>> {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.schemaRegistry.slice();\n}\n\n// Hook to get render request context\nexport function useRequest() {\n const context = renderStore.getStore();\n\n if (!context) {\n throw new Error('useRequest called outside of render context');\n }\n\n return {\n request: context.request,\n view: context.viewName,\n locals: (context.request.locals ?? {}) as Record<string, unknown>,\n params: (context.request.params ?? {}) as Record<string, any>,\n\n // Add cache tags\n addCacheTag: (tag: string | string[] | Record<string, boolean>) => {\n addCacheTag(tag);\n },\n\n // Get all cache tags\n getCacheTags: () => {\n return Array.from(context.cacheTags);\n },\n };\n}\n\n/**\n * Checks if a value is a valid JSX element (JSXNode)\n * Similar to React.isValidElement\n */\nexport function isValidElement(value: any): value is JSXNode {\n return (\n value !== null &&\n typeof value === 'object' &&\n 'type' in value &&\n 'props' in value &&\n 'children' in value &&\n (typeof value.type === 'string' || typeof value.type === 'function')\n );\n}\n\n/**\n * Clones a JSX element with new props and/or children\n * Similar to React.cloneElement\n */\nexport function cloneElement(\n element: JSXNode,\n props?: Record<string, any>,\n ...children: JSXChild[]\n): JSXNode {\n const newProps = { ...element.props, ...props };\n const newChildren = children.length > 0 ? children.flat() : element.children;\n\n return {\n type: element.type,\n props: newProps,\n children: newChildren,\n };\n}\n"],"names":["HEAD_PRIORITY","mergeMetadata","parent","child","merged","RAW_HTML_MARKER","jsxFactory","type","props","children","Fragment","renderStore","AsyncLocalStorage","createRequestContext","requestInfo","useClientJs","path","renderContext","registerIsland","key","src","name","e","getIslandEntries","context","useCss","runInRenderContext","renderFn","viewName","setViewName","getClientJsEntries","getCacheTags","addCacheTag","tag","t","value","getCssEntries","Head","priority","a","b","getHeadContent","entry","pushMetadata","metadata","resolveMetadata","acc","current","pushSchema","schema","getSchemas","useRequest","isValidElement","cloneElement","element","newProps","newChildren"],"mappings":"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"}