@cfdez11/vex 0.1.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 (35) hide show
  1. package/README.md +1383 -0
  2. package/client/app.webmanifest +14 -0
  3. package/client/favicon.ico +0 -0
  4. package/client/services/cache.js +55 -0
  5. package/client/services/hmr-client.js +22 -0
  6. package/client/services/html.js +377 -0
  7. package/client/services/hydrate-client-components.js +97 -0
  8. package/client/services/hydrate.js +25 -0
  9. package/client/services/index.js +9 -0
  10. package/client/services/navigation/create-layouts.js +172 -0
  11. package/client/services/navigation/create-navigation.js +103 -0
  12. package/client/services/navigation/index.js +8 -0
  13. package/client/services/navigation/link-interceptor.js +39 -0
  14. package/client/services/navigation/metadata.js +23 -0
  15. package/client/services/navigation/navigate.js +64 -0
  16. package/client/services/navigation/prefetch.js +43 -0
  17. package/client/services/navigation/render-page.js +45 -0
  18. package/client/services/navigation/render-ssr.js +157 -0
  19. package/client/services/navigation/router.js +48 -0
  20. package/client/services/navigation/use-query-params.js +225 -0
  21. package/client/services/navigation/use-route-params.js +76 -0
  22. package/client/services/reactive.js +231 -0
  23. package/package.json +24 -0
  24. package/server/index.js +115 -0
  25. package/server/prebuild.js +12 -0
  26. package/server/root.html +15 -0
  27. package/server/utils/cache.js +89 -0
  28. package/server/utils/component-processor.js +1526 -0
  29. package/server/utils/data-cache.js +62 -0
  30. package/server/utils/delay.js +1 -0
  31. package/server/utils/files.js +723 -0
  32. package/server/utils/hmr.js +21 -0
  33. package/server/utils/router.js +373 -0
  34. package/server/utils/streaming.js +315 -0
  35. package/server/utils/template.js +263 -0
@@ -0,0 +1,263 @@
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
+ node.data = node.data.replace(/\{\{(.+?)\}\}/g, (_, expr) =>
81
+ getDataValue(expr.trim(), scope)
82
+ );
83
+ return node;
84
+ }
85
+
86
+ if (node.type === "tag") {
87
+ const attrs = node.attribs || {};
88
+
89
+ for (const [attrName, attrValue] of Object.entries(attrs)) {
90
+ if (typeof attrValue === "string") {
91
+ attrs[attrName] = attrValue.replace(/\{\{(.+?)\}\}/g, (_, expr) =>
92
+ getDataValue(expr.trim(), scope)
93
+ );
94
+ }
95
+ }
96
+
97
+ if ("x-if" in attrs) {
98
+ const show = getDataValue(attrs["x-if"], scope);
99
+ delete attrs["x-if"];
100
+ if (!show) return null;
101
+ }
102
+
103
+ if ("x-else-if" in attrs) {
104
+ const show = getDataValue(attrs["x-else-if"], scope);
105
+ delete attrs["x-else-if"];
106
+ if (previousRendered || !show) return null;
107
+ }
108
+
109
+ if ("x-else" in attrs) {
110
+ delete attrs["x-else"];
111
+ if (previousRendered) {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ if ("x-show" in attrs) {
117
+ const show = getDataValue(attrs["x-show"], scope);
118
+ delete attrs["x-show"];
119
+ if (!show) {
120
+ attrs.style = (attrs.style || "") + "display:none;";
121
+ }
122
+ }
123
+
124
+ if ("x-for" in attrs) {
125
+ const exp = attrs["x-for"];
126
+ delete attrs["x-for"];
127
+
128
+ // format: item in items
129
+ const match = exp.match(/(.+?)\s+in\s+(.+)/);
130
+ if (!match) throw new Error("Invalid x-for format: " + exp);
131
+
132
+ const itemName = match[1].trim();
133
+ const listExpr = match[2].trim();
134
+ const list = getDataValue(listExpr, scope);
135
+
136
+ if (!Array.isArray(list)) return null;
137
+
138
+ const clones = [];
139
+
140
+ for (const item of list) {
141
+ const cloned = structuredClone(node);
142
+ const newScope = { ...scope, [itemName]: item };
143
+ clones.push(processNode(cloned, newScope));
144
+ }
145
+
146
+ return clones;
147
+ }
148
+
149
+ for (const [name, value] of Object.entries({ ...attrs })) {
150
+ if (name.startsWith(":")) {
151
+ const isSuspenseFallback =
152
+ name === ":fallback" && node.name === "Suspense";
153
+ const realName = name.slice(1);
154
+ attrs[realName] = !isSuspenseFallback
155
+ ? String(getDataValue(value, scope))
156
+ : value;
157
+ delete attrs[name];
158
+ }
159
+
160
+ if (name.startsWith("x-bind:")) {
161
+ const realName = name.slice(7);
162
+ attrs[realName] = String(getDataValue(value, scope));
163
+ delete attrs[name];
164
+ }
165
+ }
166
+
167
+ for (const [name] of Object.entries({ ...attrs })) {
168
+ if (name.startsWith("@") || name.startsWith("x-on:")) {
169
+ delete attrs[name];
170
+ }
171
+ }
172
+
173
+ if (node.children) {
174
+ const result = [];
175
+ let isPreviousRendered = false;
176
+ for (const child of node.children) {
177
+ if (isEmptyTextNode(child)) {
178
+ continue;
179
+ }
180
+ const processed = processNode(child, scope, isPreviousRendered);
181
+ if (Array.isArray(processed)) {
182
+ result.push(...processed);
183
+ isPreviousRendered = processed.length > 0;
184
+ } else if (processed) {
185
+ result.push(processed);
186
+ isPreviousRendered = true;
187
+ } else {
188
+ isPreviousRendered = false;
189
+ }
190
+ }
191
+ node.children = result;
192
+ }
193
+
194
+ return node;
195
+ }
196
+
197
+ return node;
198
+ }
199
+
200
+ /**
201
+ * Renders HTML template content with provided data
202
+ * @param {string} templateContent
203
+ * @param {{
204
+ * [name: string]: string,
205
+ * clientScripts?: string[],
206
+ * metadata?: {
207
+ * title?: string,
208
+ * description?: string,
209
+ * }
210
+ * }} data
211
+ * @returns {string}
212
+ *
213
+ */
214
+ /**
215
+ * Parsed-template cache (PERF-05).
216
+ *
217
+ * `parseHTMLToNodes` runs `parseDocument` (htmlparser2) on every call, which
218
+ * tokenises and builds a full DOM tree from the template string. The template
219
+ * string is constant between requests — only `data` changes — so the resulting
220
+ * tree can be cached and deep-cloned before each processing pass.
221
+ *
222
+ * `structuredClone` is used to deep-clone the cached nodes. htmlparser2 nodes
223
+ * are plain objects (no functions / Symbols), so structuredClone handles them
224
+ * correctly including the circular parent ↔ children references.
225
+ *
226
+ * Key: raw template string
227
+ * Value: array of parsed DOM nodes (never mutated — always clone before use)
228
+ */
229
+ const parsedTemplateCache = new Map();
230
+
231
+ /**
232
+ * Compiles a Vue-like HTML template string into a rendered HTML string.
233
+ *
234
+ * Parsing is performed only on the first call for a given template string.
235
+ * Subsequent calls clone the cached node tree and process the clone directly,
236
+ * avoiding repeated `parseDocument` invocations.
237
+ *
238
+ * @param {string} template
239
+ * @param {{
240
+ * [name: string]: string,
241
+ * clientScripts?: string[],
242
+ * metadata?: { title?: string, description?: string }
243
+ * }} data
244
+ * @returns {string}
245
+ */
246
+ export function compileTemplateToHTML(template, data = {}) {
247
+ try {
248
+ if (!parsedTemplateCache.has(template)) {
249
+ parsedTemplateCache.set(template, parseHTMLToNodes(template));
250
+ }
251
+ // Clone before processing — processNode mutates the nodes in place
252
+ const nodes = structuredClone(parsedTemplateCache.get(template));
253
+ const processed = nodes
254
+ .map((n) => processNode(n, data))
255
+ .flat()
256
+ .filter(Boolean);
257
+
258
+ return render(processed, { encodeEntities: false });
259
+ } catch (error) {
260
+ console.error("Error compiling template:", error);
261
+ throw error;
262
+ }
263
+ }