@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.
- package/README.md +1383 -0
- package/client/app.webmanifest +14 -0
- package/client/favicon.ico +0 -0
- package/client/services/cache.js +55 -0
- package/client/services/hmr-client.js +22 -0
- package/client/services/html.js +377 -0
- package/client/services/hydrate-client-components.js +97 -0
- package/client/services/hydrate.js +25 -0
- package/client/services/index.js +9 -0
- package/client/services/navigation/create-layouts.js +172 -0
- package/client/services/navigation/create-navigation.js +103 -0
- package/client/services/navigation/index.js +8 -0
- package/client/services/navigation/link-interceptor.js +39 -0
- package/client/services/navigation/metadata.js +23 -0
- package/client/services/navigation/navigate.js +64 -0
- package/client/services/navigation/prefetch.js +43 -0
- package/client/services/navigation/render-page.js +45 -0
- package/client/services/navigation/render-ssr.js +157 -0
- package/client/services/navigation/router.js +48 -0
- package/client/services/navigation/use-query-params.js +225 -0
- package/client/services/navigation/use-route-params.js +76 -0
- package/client/services/reactive.js +231 -0
- package/package.json +24 -0
- package/server/index.js +115 -0
- package/server/prebuild.js +12 -0
- package/server/root.html +15 -0
- package/server/utils/cache.js +89 -0
- package/server/utils/component-processor.js +1526 -0
- package/server/utils/data-cache.js +62 -0
- package/server/utils/delay.js +1 -0
- package/server/utils/files.js +723 -0
- package/server/utils/hmr.js +21 -0
- package/server/utils/router.js +373 -0
- package/server/utils/streaming.js +315 -0
- 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
|
+
}
|