@cfdez11/vex 0.8.2 → 0.9.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.
Files changed (71) hide show
  1. package/dist/bin/vex.js +3 -0
  2. package/dist/client/services/cache.js +1 -0
  3. package/dist/client/services/hmr-client.js +1 -0
  4. package/dist/client/services/html.js +1 -0
  5. package/dist/client/services/hydrate-client-components.js +1 -0
  6. package/dist/client/services/hydrate.js +1 -0
  7. package/dist/client/services/index.js +1 -0
  8. package/dist/client/services/navigation/create-layouts.js +1 -0
  9. package/dist/client/services/navigation/create-navigation.js +1 -0
  10. package/dist/client/services/navigation/index.js +1 -0
  11. package/dist/client/services/navigation/link-interceptor.js +1 -0
  12. package/dist/client/services/navigation/metadata.js +1 -0
  13. package/dist/client/services/navigation/navigate.js +1 -0
  14. package/dist/client/services/navigation/prefetch.js +1 -0
  15. package/dist/client/services/navigation/render-page.js +1 -0
  16. package/dist/client/services/navigation/render-ssr.js +1 -0
  17. package/dist/client/services/navigation/router.js +1 -0
  18. package/dist/client/services/navigation/use-query-params.js +1 -0
  19. package/dist/client/services/navigation/use-route-params.js +1 -0
  20. package/dist/client/services/navigation.js +1 -0
  21. package/dist/client/services/reactive.js +1 -0
  22. package/dist/server/build-static.js +6 -0
  23. package/dist/server/index.js +4 -0
  24. package/dist/server/prebuild.js +1 -0
  25. package/dist/server/utils/cache.js +1 -0
  26. package/dist/server/utils/component-processor.js +68 -0
  27. package/dist/server/utils/data-cache.js +1 -0
  28. package/dist/server/utils/esbuild-plugin.js +1 -0
  29. package/dist/server/utils/files.js +28 -0
  30. package/dist/server/utils/hmr.js +1 -0
  31. package/dist/server/utils/router.js +11 -0
  32. package/dist/server/utils/streaming.js +1 -0
  33. package/dist/server/utils/template.js +1 -0
  34. package/package.json +8 -7
  35. package/bin/vex.js +0 -69
  36. package/client/favicon.ico +0 -0
  37. package/client/services/cache.js +0 -55
  38. package/client/services/hmr-client.js +0 -22
  39. package/client/services/html.js +0 -377
  40. package/client/services/hydrate-client-components.js +0 -97
  41. package/client/services/hydrate.js +0 -25
  42. package/client/services/index.js +0 -9
  43. package/client/services/navigation/create-layouts.js +0 -172
  44. package/client/services/navigation/create-navigation.js +0 -103
  45. package/client/services/navigation/index.js +0 -8
  46. package/client/services/navigation/link-interceptor.js +0 -39
  47. package/client/services/navigation/metadata.js +0 -23
  48. package/client/services/navigation/navigate.js +0 -64
  49. package/client/services/navigation/prefetch.js +0 -43
  50. package/client/services/navigation/render-page.js +0 -45
  51. package/client/services/navigation/render-ssr.js +0 -157
  52. package/client/services/navigation/router.js +0 -48
  53. package/client/services/navigation/use-query-params.js +0 -225
  54. package/client/services/navigation/use-route-params.js +0 -76
  55. package/client/services/navigation.js +0 -6
  56. package/client/services/reactive.js +0 -247
  57. package/server/build-static.js +0 -138
  58. package/server/index.js +0 -135
  59. package/server/prebuild.js +0 -13
  60. package/server/utils/cache.js +0 -89
  61. package/server/utils/component-processor.js +0 -1631
  62. package/server/utils/data-cache.js +0 -62
  63. package/server/utils/delay.js +0 -1
  64. package/server/utils/esbuild-plugin.js +0 -110
  65. package/server/utils/files.js +0 -845
  66. package/server/utils/hmr.js +0 -21
  67. package/server/utils/router.js +0 -375
  68. package/server/utils/streaming.js +0 -324
  69. package/server/utils/template.js +0 -274
  70. /package/{client → dist/client}/app.webmanifest +0 -0
  71. /package/{server → dist/server}/root.html +0 -0
@@ -1,324 +0,0 @@
1
- import {
2
- processClientComponent,
3
- renderHtmlFile,
4
- } from "./component-processor.js";
5
-
6
-
7
- /**
8
- * Parses a string of raw HTML-like attributes into a structured object.
9
- * Return object will be used to pass props to components through template
10
- *
11
- * Supports:
12
- * - Dynamic props with `:` prefix (e.g., `:prop="value"`).
13
- * - Event handlers with `@` prefix (e.g., `@click="handler"`).
14
- * - Static attributes (e.g., `id="my-id"` or `class="my-class"`).
15
- *
16
- * @param {string} rawAttrs - The raw attribute string extracted from an element tag.
17
- *
18
- * @returns {Record<string, string>} An object mapping attribute names to their values.
19
- * Dynamic props and event handlers retain their raw
20
- * string representations (e.g., template expressions).
21
- *
22
- * @example
23
- * parseAttributes(':links="${links}" @click="handleClick" id="my-component"');
24
- * // Returns:
25
- * // {
26
- * // links: '${links}',
27
- * // click: 'handleClick',
28
- * // id: 'my-component'
29
- * // }
30
- */
31
- function decodeAttrValue(raw) {
32
- const decoded = raw.replace(/&quot;/g, '"');
33
- if (decoded.startsWith("[") || decoded.startsWith("{")) {
34
- try { return JSON.parse(decoded); } catch {}
35
- }
36
- return decoded;
37
- }
38
-
39
- function parseAttributes(rawAttrs) {
40
- const attrs = {};
41
- const regex =
42
- /:([\w-]+)=(?:"([^"]*)"|'([^']*)')|@([\w-]+)=(?:"([^"]*)"|'([^']*)')|([\w:-]+)=(?:"([^"]*)"|'([^']*)')/g;
43
- let match;
44
-
45
- while ((match = regex.exec(rawAttrs)) !== null) {
46
- if (match[1]) {
47
- // Dynamic prop :prop
48
- attrs[match[1]] = match[2] ?? match[3] ?? "";
49
- } else if (match[4]) {
50
- // Event handler @event
51
- attrs[match[4]] = match[5] ?? match[6] ?? "";
52
- } else if (match[7]) {
53
- // Static prop — decode &quot; and recover arrays/objects serialized by template.js
54
- attrs[match[7]] = decodeAttrValue(match[8] ?? match[9] ?? "");
55
- }
56
- }
57
-
58
- return attrs;
59
- }
60
-
61
-
62
- /**
63
- * Renders components in HTML
64
- * @param {string} html
65
- * @param {Map<string, { path: string }>} serverComponents
66
- * @returns {Promise<string>}
67
- */
68
- /**
69
- * Renders server component instances in parallel.
70
- *
71
- * Each component type found in `serverComponents` may appear multiple times in
72
- * the HTML (e.g. three `<UserCard>` tags). Previously they were rendered one by
73
- * one in a serial `for` loop, even though each instance is fully independent.
74
- *
75
- * Now, all instances of a given component are kicked off at the same time with
76
- * `Promise.all` and their results are applied in reverse-index order so that
77
- * string offsets stay valid (replacing from the end of the string backwards).
78
- *
79
- * @param {string} html
80
- * @param {Map<string, { path: string }>} serverComponents
81
- * @returns {Promise<string>}
82
- */
83
- async function processServerComponents(html, serverComponents) {
84
- let processedHtml = html;
85
-
86
- for (const [componentName, componentData] of serverComponents.entries()) {
87
- const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
88
- const componentRegex = new RegExp(
89
- `<${escapedName}(?![a-zA-Z0-9_-])\\s*([^>]*?)\\s*(?:\\/>|>\\s*<\\/${escapedName}(?![a-zA-Z0-9_-])>)`,
90
- "gi"
91
- );
92
-
93
- const replacements = [];
94
- let match;
95
-
96
- while ((match = componentRegex.exec(processedHtml)) !== null) {
97
- replacements.push({
98
- name: componentName,
99
- attrs: parseAttributes(match[1]),
100
- fullMatch: match[0],
101
- start: match.index,
102
- end: match.index + match[0].length,
103
- });
104
- }
105
-
106
- if (replacements.length === 0) continue;
107
-
108
- // Render all instances of this component concurrently, then apply results
109
- // from the end of the string backwards so earlier offsets stay valid.
110
- const rendered = await Promise.all(
111
- replacements.map(({ attrs }) => renderHtmlFile(componentData.path, attrs))
112
- );
113
-
114
- for (let i = replacements.length - 1; i >= 0; i--) {
115
- const { start, end } = replacements[i];
116
- processedHtml =
117
- processedHtml.slice(0, start) +
118
- rendered[i].html +
119
- processedHtml.slice(end);
120
- }
121
- }
122
-
123
- return processedHtml;
124
- }
125
-
126
- /**
127
- * Renders components in HTML and client scripts to load them
128
- * @param {string} html
129
- * @param {Map<string, { path: string }>} clientComponents
130
- * @returns {Promise<{
131
- * html: string,
132
- * allScripts: Array<string>,
133
- * }>}
134
- */
135
- async function renderClientComponents(html, clientComponents) {
136
- let processedHtml = html;
137
- const allScripts = [];
138
-
139
- for (const [componentName, { path: componentAbsPath }] of clientComponents.entries()) {
140
- const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
141
-
142
- const componentRegex = new RegExp(
143
- `<${escapedName}\\b((?:\\s+(?:[^\\s>"'=]+(?:=(?:"[^"]*"|'[^']*'|[^\\s"'=<>]+))?))*?)\\s*\\/?>`,
144
- "gi"
145
- );
146
-
147
- const replacements = [];
148
- let match;
149
- const htmlToProcess = processedHtml;
150
-
151
- while ((match = componentRegex.exec(htmlToProcess)) !== null) {
152
- const matchData = {
153
- name: componentName,
154
- attrs: parseAttributes(match[1]),
155
- fullMatch: match[0],
156
- start: match.index,
157
- end: match.index + match[0].length,
158
- };
159
-
160
- replacements.push(matchData);
161
- }
162
-
163
- // Render in reverse order to maintain indices
164
- for (let i = replacements.length - 1; i >= 0; i--) {
165
- const { start, end, attrs } = replacements[i];
166
-
167
- const htmlComponent = await processClientComponent(componentName, componentAbsPath, attrs);
168
-
169
- processedHtml =
170
- processedHtml.slice(0, start) +
171
- htmlComponent +
172
- processedHtml.slice(end);
173
- }
174
- }
175
-
176
- return { html: processedHtml, allScripts };
177
- }
178
-
179
- /**
180
- * Renders server components, handling both regular and suspense boundaries
181
- * Server components without suspense are rendered immediately.
182
- * Server components inside <Suspense> boundaries are saved in suspenseComponents.
183
- * @param {string} pageHtml
184
- * @param {Map<string, { path: string }>} serverComponents
185
- * @param {boolean} awaitSuspenseComponents - If true, renders suspense components immediately
186
- * @returns {Promise<{
187
- * html: string,
188
- * suspenseComponents: Array<{
189
- * id: string,
190
- * content: string,
191
- * }>
192
- * }>}
193
- */
194
- async function renderServerComponents(pageHtml, serverComponents = new Map(), awaitSuspenseComponents = false) {
195
- const suspenseComponents = [];
196
- let suspenseId = 0;
197
- let html = pageHtml;
198
-
199
- // Fresh regex per call — avoids the shared-lastIndex race condition.
200
- // Each request gets its own regex instance with lastIndex starting at 0.
201
- const suspenseRegex = /<Suspense\s+fallback="([^"]*)">([\s\S]*?)<\/Suspense>/g;
202
-
203
- let match;
204
- while ((match = suspenseRegex.exec(html)) !== null) {
205
- const id = `suspense-${suspenseId++}`;
206
- const [fullMatch, fallback, content] = match;
207
-
208
- const suspenseContent = awaitSuspenseComponents ? content : fallback;
209
-
210
- // Render components in fallback if not awaiting suspense or in content if awaiting suspense
211
- const fallbackHtml = await processServerComponents(
212
- suspenseContent,
213
- serverComponents
214
- );
215
-
216
- suspenseComponents.push({
217
- id,
218
- content: content,
219
- });
220
-
221
- // Replace suspense block with container and restart the search from the
222
- // beginning of the modified string (indices have shifted after the replace).
223
- const replacement = `<div id="${id}">${fallbackHtml}</div>`;
224
- html = html.replace(fullMatch, replacement);
225
- suspenseRegex.lastIndex = 0;
226
- }
227
-
228
- // Render all non-suspended components
229
- html = await processServerComponents(html, serverComponents);
230
-
231
- return { html, suspenseComponents };
232
- }
233
-
234
- /**
235
- * Renders server components, client components in HTML, suspense components and client scripts to load client components
236
- * @param {{
237
- * pageHtml: string,
238
- * serverComponents: Map<string, { path: string, originalPath: string, importStatement: string }>,
239
- * clientComponents: Map<string, { path: string, originalPath: string, importStatement: string }>,
240
- * awaitSuspenseComponents: boolean,
241
- * }}
242
- * @returns {Promise<{
243
- * html: string,
244
- * clientComponentsScripts: Array<string>,
245
- * suspenseComponents: Array<{
246
- * id: string,
247
- * content: string,
248
- * }>
249
- * }>}
250
- */
251
- export async function renderComponents({
252
- html,
253
- serverComponents = new Map(),
254
- clientComponents = new Map(),
255
- awaitSuspenseComponents = false,
256
- }) {
257
- const hasServerComponents = serverComponents.size > 0;
258
- const hasClientComponents = clientComponents.size > 0;
259
-
260
- const { html: htmlServerComponents, suspenseComponents } = hasServerComponents ?
261
- await renderServerComponents(html, serverComponents, awaitSuspenseComponents) :
262
- {
263
- html,
264
- suspenseComponents: [],
265
- };
266
-
267
- const { html: htmlClientComponents, allScripts: clientComponentsScripts } =
268
- hasClientComponents ?
269
- await renderClientComponents(htmlServerComponents, clientComponents) :
270
- {
271
- html: htmlServerComponents,
272
- allScripts: [],
273
- };
274
-
275
- return {
276
- html: htmlClientComponents,
277
- suspenseComponents,
278
- clientComponentsScripts,
279
- };
280
- }
281
-
282
- /**
283
- * Generates the streaming HTML payload that replaces a Suspense fallback with
284
- * the real rendered content once it is ready.
285
- *
286
- * The payload consists of two parts streamed back-to-back:
287
- * 1. A `<template id="…">` holding the rendered HTML (invisible to the user).
288
- * 2. A tiny inline `<script>` that calls `window.hydrateTarget(targetId, sourceId)`.
289
- *
290
- * `window.hydrateTarget` is defined once in root.html via a single
291
- * `<script src="hydrate.js">`. Using an inline call instead of a per-boundary
292
- * `<script src="hydrate.js">` avoids the browser parsing and initialising the
293
- * same script N times.
294
- *
295
- * @param {string} suspenseId - The id of the fallback <div> to replace.
296
- * @param {string} renderedContent - The real HTML to swap in.
297
- * @returns {string}
298
- */
299
- export function generateReplacementContent(suspenseId, renderedContent) {
300
- const contentId = `${suspenseId}-content`;
301
- return `<template id="${contentId}">${renderedContent}</template><script>window.hydrateTarget("${suspenseId}","${contentId}")</script>`;
302
- }
303
-
304
- /**
305
- * Renders all components inside a suspense boundary
306
- * @param {{
307
- * id: string,
308
- * content: string,
309
- * components: Array<{name: string, attrs: object, fullMatch: string}>
310
- * }} suspenseComponent
311
- * @param {Map<string, { path: string }>} serverComponents
312
- * @returns {Promise<string>}
313
- */
314
- export async function renderSuspenseComponent(
315
- suspenseComponent,
316
- serverComponents
317
- ) {
318
- const html = await processServerComponents(
319
- suspenseComponent.content,
320
- serverComponents
321
- );
322
-
323
- return html;
324
- }
@@ -1,274 +0,0 @@
1
- import { parseDocument, DomUtils } from "htmlparser2";
2
- import { render } from "dom-serializer";
3
-
4
- /**
5
- * Compiled-function cache.
6
- *
7
- * `Function(...keys, body)` compiles a new function object on every call.
8
- * With ~20 template expressions and 100 req/s that is ~2000 allocations/s
9
- * that the GC has to reclaim. The compiled function only depends on the
10
- * expression string and the names of the scope keys — not their values —
11
- * so it can be reused across requests by calling it with different arguments.
12
- *
13
- * Key format: `"<expression>::<key1>,<key2>,..."` — must include both the
14
- * expression and the key names because the same expression with a different
15
- * scope shape produces a different function signature.
16
- */
17
- const fnCache = new Map();
18
-
19
- /**
20
- * Evaluates a template expression against the provided data scope.
21
- *
22
- * The compiled `Function` is cached by expression + scope key names so it is
23
- * only created once per unique (expression, scope shape) pair.
24
- *
25
- * @param {string} expression
26
- * @param {object} scope
27
- * @returns {any}
28
- */
29
- function getDataValue(expression, scope) {
30
- const keys = Object.keys(scope);
31
- const cacheKey = `${expression}::${keys.join(",")}`;
32
- if (!fnCache.has(cacheKey)) {
33
- fnCache.set(cacheKey, Function(...keys, `return (${expression})`));
34
- }
35
- try {
36
- return fnCache.get(cacheKey)(...Object.values(scope));
37
- } catch (e) {
38
- return "";
39
- }
40
- }
41
-
42
- /**
43
- * Checks if a DOM node is an empty text node
44
- * @param {ChildNode} node
45
- * @returns {boolean}
46
- */
47
- function isEmptyTextNode(node) {
48
- return node.type === "text" && /^\s*$/.test(node.data);
49
- }
50
-
51
- /**
52
- * Parses HTML string and returns DOM nodes
53
- * @param {string} html
54
- * @returns {ChildNode[]}
55
- */
56
- function parseHTMLToNodes(html) {
57
- try {
58
- const cleanHtml = html
59
- .replace(/[\r\n\t]+/g, " ")
60
- .replace(/ +/g, " ")
61
- .trim();
62
- const dom = parseDocument(cleanHtml, { xmlMode: true });
63
- return DomUtils.getChildren(dom);
64
- } catch (error) {
65
- console.error('Error parsing HTML:', error);
66
- return [];
67
- }
68
- }
69
-
70
-
71
- /**
72
- * Processes an HTML file to extract script, template, metadata, client code, and component registry
73
- * @param {ChildNode} node
74
- * @param {Object} scope
75
- * @param {boolean} previousRendered
76
- * @returns {ChildNode | ChildNode[] | null}
77
- */
78
- function processNode(node, scope, previousRendered = false) {
79
- if (node.type === "text") {
80
- // Replace {{expr}} with its value from scope (SSR interpolation).
81
- // The lookbehind (?<!\\) skips escaped \{{expr}}, which are then
82
- // unescaped to literal {{expr}} by the second replace.
83
- node.data = node.data
84
- .replace(/(?<!\\)\{\{(.+?)\}\}/g, (_, expr) => getDataValue(expr.trim(), scope))
85
- .replace(/\\\{\{/g, "{{");
86
- return node;
87
- }
88
-
89
- if (node.type === "tag") {
90
- const attrs = node.attribs || {};
91
-
92
- for (const [attrName, attrValue] of Object.entries(attrs)) {
93
- if (typeof attrValue === "string") {
94
- attrs[attrName] = attrValue
95
- .replace(/(?<!\\)\{\{(.+?)\}\}/g, (_, expr) => getDataValue(expr.trim(), scope))
96
- .replace(/\\\{\{/g, "{{");
97
- }
98
- }
99
-
100
- if ("x-if" in attrs) {
101
- const show = getDataValue(attrs["x-if"], scope);
102
- delete attrs["x-if"];
103
- if (!show) return null;
104
- }
105
-
106
- if ("x-else-if" in attrs) {
107
- const show = getDataValue(attrs["x-else-if"], scope);
108
- delete attrs["x-else-if"];
109
- if (previousRendered || !show) return null;
110
- }
111
-
112
- if ("x-else" in attrs) {
113
- delete attrs["x-else"];
114
- if (previousRendered) {
115
- return null;
116
- }
117
- }
118
-
119
- if ("x-show" in attrs) {
120
- const show = getDataValue(attrs["x-show"], scope);
121
- delete attrs["x-show"];
122
- if (!show) {
123
- attrs.style = (attrs.style || "") + "display:none;";
124
- }
125
- }
126
-
127
- if ("x-for" in attrs) {
128
- const exp = attrs["x-for"];
129
- delete attrs["x-for"];
130
-
131
- // format: item in items
132
- const match = exp.match(/(.+?)\s+in\s+(.+)/);
133
- if (!match) throw new Error("Invalid x-for format: " + exp);
134
-
135
- const itemName = match[1].trim();
136
- const listExpr = match[2].trim();
137
- const list = getDataValue(listExpr, scope);
138
-
139
- if (!Array.isArray(list)) return null;
140
-
141
- const clones = [];
142
-
143
- for (const item of list) {
144
- const cloned = structuredClone(node);
145
- const newScope = { ...scope, [itemName]: item };
146
- clones.push(processNode(cloned, newScope));
147
- }
148
-
149
- return clones;
150
- }
151
-
152
- for (const [name, value] of Object.entries({ ...attrs })) {
153
- if (name.startsWith(":")) {
154
- const isSuspenseFallback =
155
- name === ":fallback" && node.name === "Suspense";
156
- const realName = name.slice(1);
157
- if (!isSuspenseFallback) {
158
- const val = getDataValue(value, scope);
159
- attrs[realName] = (val !== null && val !== undefined && typeof val === "object")
160
- ? JSON.stringify(val)
161
- : String(val ?? "");
162
- } else {
163
- attrs[realName] = value;
164
- }
165
- delete attrs[name];
166
- }
167
-
168
- if (name.startsWith("x-bind:")) {
169
- const realName = name.slice(7);
170
- const val = getDataValue(value, scope);
171
- attrs[realName] = (val !== null && val !== undefined && typeof val === "object")
172
- ? JSON.stringify(val)
173
- : String(val ?? "");
174
- delete attrs[name];
175
- }
176
- }
177
-
178
- for (const [name] of Object.entries({ ...attrs })) {
179
- if (name.startsWith("@") || name.startsWith("x-on:")) {
180
- delete attrs[name];
181
- }
182
- }
183
-
184
- if (node.children) {
185
- const result = [];
186
- let isPreviousRendered = false;
187
- for (const child of node.children) {
188
- if (isEmptyTextNode(child)) {
189
- continue;
190
- }
191
- const processed = processNode(child, scope, isPreviousRendered);
192
- if (Array.isArray(processed)) {
193
- result.push(...processed);
194
- isPreviousRendered = processed.length > 0;
195
- } else if (processed) {
196
- result.push(processed);
197
- isPreviousRendered = true;
198
- } else {
199
- isPreviousRendered = false;
200
- }
201
- }
202
- node.children = result;
203
- }
204
-
205
- return node;
206
- }
207
-
208
- return node;
209
- }
210
-
211
- /**
212
- * Renders HTML template content with provided data
213
- * @param {string} templateContent
214
- * @param {{
215
- * [name: string]: string,
216
- * clientScripts?: string[],
217
- * metadata?: {
218
- * title?: string,
219
- * description?: string,
220
- * }
221
- * }} data
222
- * @returns {string}
223
- *
224
- */
225
- /**
226
- * Parsed-template cache (PERF-05).
227
- *
228
- * `parseHTMLToNodes` runs `parseDocument` (htmlparser2) on every call, which
229
- * tokenises and builds a full DOM tree from the template string. The template
230
- * string is constant between requests — only `data` changes — so the resulting
231
- * tree can be cached and deep-cloned before each processing pass.
232
- *
233
- * `structuredClone` is used to deep-clone the cached nodes. htmlparser2 nodes
234
- * are plain objects (no functions / Symbols), so structuredClone handles them
235
- * correctly including the circular parent ↔ children references.
236
- *
237
- * Key: raw template string
238
- * Value: array of parsed DOM nodes (never mutated — always clone before use)
239
- */
240
- const parsedTemplateCache = new Map();
241
-
242
- /**
243
- * Compiles a Vue-like HTML template string into a rendered HTML string.
244
- *
245
- * Parsing is performed only on the first call for a given template string.
246
- * Subsequent calls clone the cached node tree and process the clone directly,
247
- * avoiding repeated `parseDocument` invocations.
248
- *
249
- * @param {string} template
250
- * @param {{
251
- * [name: string]: string,
252
- * clientScripts?: string[],
253
- * metadata?: { title?: string, description?: string }
254
- * }} data
255
- * @returns {string}
256
- */
257
- export function compileTemplateToHTML(template, data = {}) {
258
- try {
259
- if (!parsedTemplateCache.has(template)) {
260
- parsedTemplateCache.set(template, parseHTMLToNodes(template));
261
- }
262
- // Clone before processing — processNode mutates the nodes in place
263
- const nodes = structuredClone(parsedTemplateCache.get(template));
264
- const processed = nodes
265
- .map((n) => processNode(n, data))
266
- .flat()
267
- .filter(Boolean);
268
-
269
- return render(processed, { encodeEntities: false });
270
- } catch (error) {
271
- console.error("Error compiling template:", error);
272
- throw error;
273
- }
274
- }
File without changes
File without changes