@ape-egg/vibe 4.0.1 → 4.1.3
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 +3 -1
- package/boot.js +10 -9
- package/component.js +0 -29
- package/hot-module-refresh.js +0 -0
- package/index.js +0 -28
- package/package.json +3 -2
- package/runtime/_vibe-compiled-iteration-batch.js +0 -56
- package/runtime/affected.js +6 -189
- package/runtime/cleanup.js +22 -71
- package/runtime/component-cache.js +0 -53
- package/runtime/component.js +6 -407
- package/runtime/conditionals.js +1 -143
- package/runtime/constants.js +19 -82
- package/runtime/debug.js +22 -47
- package/runtime/dispatch.js +0 -46
- package/runtime/hydrate.js +11 -130
- package/runtime/index.js +9 -382
- package/runtime/inert.js +18 -0
- package/runtime/iterate.js +7 -595
- package/runtime/iteration-utils.js +18 -71
- package/runtime/loop-scope.js +0 -58
- package/runtime/manifest.js +0 -27
- package/runtime/parse.js +2 -116
- package/runtime/pre-compiled-iterations.js +3 -51
- package/runtime/pre-compiled-manifest.js +6 -169
- package/runtime/raw-html.js +0 -5
- package/runtime/reconcile.js +4 -159
- package/runtime/staging.js +0 -57
- package/runtime/state.js +0 -61
- package/runtime/this-scope.js +0 -17
- package/runtime/tracking.js +0 -65
- package/runtime/utils.js +1 -144
- package/runtime/vibe-css.js +37 -0
- package/spa.js +0 -76
- package/vibe.css +5 -44
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pre-compiled Manifest - Manifest support for pre-compiled Vibe pages
|
|
3
|
-
*
|
|
4
|
-
* Handles detection, loading, and restoration of pre-compiled page manifests.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// Build manifest with restoration data (captures markers before hydration)
|
|
8
1
|
export const buildHyperspeedManifest = (parsedTree) => {
|
|
9
|
-
// Helper to split text content by @[...] markers into array
|
|
10
2
|
const splitByMarkers = (text) => {
|
|
11
3
|
const regex = /@\[[^\]]+\]/g;
|
|
12
4
|
const parts = [];
|
|
13
5
|
let lastIndex = 0;
|
|
14
6
|
|
|
15
7
|
text.replace(regex, (match, index) => {
|
|
16
|
-
// Add text before marker
|
|
17
8
|
if (index > lastIndex) {
|
|
18
9
|
parts.push(text.slice(lastIndex, index));
|
|
19
10
|
}
|
|
20
|
-
// Add marker
|
|
21
11
|
parts.push(match);
|
|
22
12
|
lastIndex = index + match.length;
|
|
23
13
|
});
|
|
24
14
|
|
|
25
|
-
// Add remaining text
|
|
26
15
|
if (lastIndex < text.length) {
|
|
27
16
|
parts.push(text.slice(lastIndex));
|
|
28
17
|
}
|
|
@@ -37,12 +26,10 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
37
26
|
children: {},
|
|
38
27
|
};
|
|
39
28
|
|
|
40
|
-
// Copy type for iterations/conditionals
|
|
41
29
|
if (node.type) {
|
|
42
30
|
result.type = node.type;
|
|
43
31
|
}
|
|
44
32
|
|
|
45
|
-
// Check if parsed string contains bindings - split into array
|
|
46
33
|
if (typeof node.parsed === "string" && node.parsed.includes("@[")) {
|
|
47
34
|
const parsedArray = splitByMarkers(node.parsed);
|
|
48
35
|
if (parsedArray) {
|
|
@@ -52,7 +39,6 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
52
39
|
}
|
|
53
40
|
}
|
|
54
41
|
|
|
55
|
-
// Check for attribute bindings
|
|
56
42
|
if (node.attributes && typeof node.attributes === "object") {
|
|
57
43
|
const attrBindings = {};
|
|
58
44
|
for (const [key, value] of Object.entries(node.attributes)) {
|
|
@@ -68,7 +54,6 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
68
54
|
}
|
|
69
55
|
}
|
|
70
56
|
|
|
71
|
-
// For iterations, extract template as HTML string
|
|
72
57
|
if (node.type === "iteration" && node.meta?.template?.element) {
|
|
73
58
|
const templateElement = node.meta.template.element;
|
|
74
59
|
if (templateElement && templateElement.innerHTML) {
|
|
@@ -79,7 +64,6 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
79
64
|
}
|
|
80
65
|
}
|
|
81
66
|
|
|
82
|
-
// For conditionals, extract branch templates as HTML strings
|
|
83
67
|
if (node.type === "conditional" && node.meta?.branches) {
|
|
84
68
|
const trueBranch = node.meta.branches.true?.element;
|
|
85
69
|
const falseBranch = node.meta.branches.false?.element;
|
|
@@ -97,7 +81,6 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
97
81
|
}
|
|
98
82
|
}
|
|
99
83
|
|
|
100
|
-
// Recursively process children (skip runtime-only nodes)
|
|
101
84
|
if (node.children && typeof node.children === "object") {
|
|
102
85
|
for (const [key, childNode] of Object.entries(node.children)) {
|
|
103
86
|
result.children[key] = walkNode(childNode);
|
|
@@ -107,7 +90,6 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
107
90
|
return result;
|
|
108
91
|
};
|
|
109
92
|
|
|
110
|
-
// Wrap in body structure (parsedTree is already the body's children)
|
|
111
93
|
return {
|
|
112
94
|
element: null,
|
|
113
95
|
parsed: [],
|
|
@@ -115,37 +97,18 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
115
97
|
body: {
|
|
116
98
|
element: null,
|
|
117
99
|
parsed: [],
|
|
118
|
-
children: walkNode(parsedTree).children,
|
|
100
|
+
children: walkNode(parsedTree).children,
|
|
119
101
|
},
|
|
120
102
|
},
|
|
121
103
|
};
|
|
122
104
|
};
|
|
123
105
|
|
|
124
|
-
// Manifest detection - per-page manifest
|
|
125
|
-
// Each compiled page has its own manifest: /vibe-hyperspeed/{page-path}.manifest.js
|
|
126
106
|
let hyperspeedData = null;
|
|
127
107
|
let hyperspeedDetectionAttempted = false;
|
|
128
108
|
|
|
129
|
-
/**
|
|
130
|
-
* Build the ordered list of manifest URLs to try for a page, most-likely first.
|
|
131
|
-
*
|
|
132
|
-
* `pathname` is window.location.pathname; `route` is the optional route template
|
|
133
|
-
* the page declares (window.__ROUTE__, e.g. "/brawlers/:index"). When the route
|
|
134
|
-
* marks a segment dynamic with `:param`, the compiler has collapsed that segment
|
|
135
|
-
* to `$` in the manifest path — so we point straight at the tokenized manifest
|
|
136
|
-
* instead of probing literal paths (`/brawlers/0.html.manifest.js`) that are
|
|
137
|
-
* guaranteed to 404. Without the route hint the original literal-first strategies
|
|
138
|
-
* apply unchanged. The result is de-duplicated (subdirectory pages otherwise
|
|
139
|
-
* produce the same candidate twice).
|
|
140
|
-
*
|
|
141
|
-
* @param {string} pathname
|
|
142
|
-
* @param {string|null|undefined} route
|
|
143
|
-
* @returns {string[]}
|
|
144
|
-
*/
|
|
145
109
|
export const buildManifestCandidatePaths = (pathname, route) => {
|
|
146
110
|
let pagePath = pathname;
|
|
147
111
|
|
|
148
|
-
// Normalize path: handle directory URLs and missing extensions
|
|
149
112
|
if (pagePath.endsWith("/")) {
|
|
150
113
|
pagePath = pagePath + "index.html";
|
|
151
114
|
} else if (!pagePath.includes(".")) {
|
|
@@ -164,23 +127,12 @@ export const buildManifestCandidatePaths = (pathname, route) => {
|
|
|
164
127
|
|
|
165
128
|
const possiblePaths = [];
|
|
166
129
|
|
|
167
|
-
// Route-aware fast path: a `:segment` in the declared route is a dynamic param
|
|
168
|
-
// the compiler tokenized to `$`. Tokenize exactly those positions (params can
|
|
169
|
-
// sit mid-path, e.g. /a/:id/b) and try that manifest first — a direct hit, no
|
|
170
|
-
// 404 probing. Skipped entirely when no route is declared.
|
|
171
130
|
const routeSegments = route ? route.split("/").filter((s) => s) : null;
|
|
172
131
|
const isCatchAll =
|
|
173
132
|
routeSegments &&
|
|
174
133
|
routeSegments[routeSegments.length - 1]?.startsWith(":") &&
|
|
175
134
|
routeSegments[routeSegments.length - 1]?.endsWith("*");
|
|
176
135
|
if (isCatchAll) {
|
|
177
|
-
// A trailing `:name*` catch-all (pages/x/$$name.html) swallows every
|
|
178
|
-
// remaining URL segment — zero or more — so tokenizing by position is
|
|
179
|
-
// meaningless past the static prefix. The compiler collapses the whole
|
|
180
|
-
// `$$name.html` file to the same `$` token as a single `$param`, giving
|
|
181
|
-
// ONE manifest for every depth: <static-prefix>/$.html.manifest.js.
|
|
182
|
-
// Built from the raw pathname: with zero extra segments the .html
|
|
183
|
-
// normalization above has already mutated the prefix's last segment.
|
|
184
136
|
const rawSegments = pathname.split("/").filter((s) => s);
|
|
185
137
|
const prefix = rawSegments
|
|
186
138
|
.slice(0, routeSegments.length - 1)
|
|
@@ -195,22 +147,16 @@ export const buildManifestCandidatePaths = (pathname, route) => {
|
|
|
195
147
|
possiblePaths.push(`/vibe-hyperspeed/${tokenized.join("/")}.manifest.js`);
|
|
196
148
|
}
|
|
197
149
|
|
|
198
|
-
// Strategy 1: vibe-hyperspeed at the same level as parent directory
|
|
199
|
-
// /compiled/playground/test.html -> /compiled/vibe-hyperspeed/playground/test.html.manifest.js
|
|
200
150
|
if (dirSegments.length >= 1) {
|
|
201
|
-
const subPath = dirSegments.slice(1).join("/");
|
|
202
|
-
const baseDir = "/" + dirSegments[0];
|
|
151
|
+
const subPath = dirSegments.slice(1).join("/");
|
|
152
|
+
const baseDir = "/" + dirSegments[0];
|
|
203
153
|
possiblePaths.push(
|
|
204
154
|
`${baseDir}/vibe-hyperspeed/${subPath ? subPath + "/" : ""}${fileName}.manifest.js`,
|
|
205
155
|
);
|
|
206
156
|
}
|
|
207
157
|
|
|
208
|
-
// Strategy 2: vibe-hyperspeed at web root (original behavior)
|
|
209
|
-
// /compiled/playground/test.html -> /vibe-hyperspeed/compiled/playground/test.html.manifest.js
|
|
210
158
|
possiblePaths.push(`/vibe-hyperspeed${pagePath}.manifest.js`);
|
|
211
159
|
|
|
212
|
-
// Strategy 3: vibe-hyperspeed relative to immediate parent
|
|
213
|
-
// /playground/test.html -> /vibe-hyperspeed/playground/test.html.manifest.js
|
|
214
160
|
if (dirSegments.length > 0) {
|
|
215
161
|
const relativePath = dirSegments.join("/");
|
|
216
162
|
possiblePaths.push(
|
|
@@ -218,11 +164,6 @@ export const buildManifestCandidatePaths = (pathname, route) => {
|
|
|
218
164
|
);
|
|
219
165
|
}
|
|
220
166
|
|
|
221
|
-
// Strategy 4: dynamic routes without a declared route. The compiler collapses a
|
|
222
|
-
// `$param` segment to a single `$` token (the-arena/$id.html ->
|
|
223
|
-
// the-arena/$.html.manifest.js), so a concrete URL only matches once its
|
|
224
|
-
// trailing segment is tokenized. Tried after the literal strategies, so static
|
|
225
|
-
// pages still win on an exact hit.
|
|
226
167
|
const dot = fileName.indexOf(".");
|
|
227
168
|
const tokenized = dot >= 0 ? "$" + fileName.slice(dot) : "$";
|
|
228
169
|
if (tokenized !== fileName) {
|
|
@@ -230,37 +171,16 @@ export const buildManifestCandidatePaths = (pathname, route) => {
|
|
|
230
171
|
possiblePaths.push(`/vibe-hyperspeed${dirPrefix}/${tokenized}.manifest.js`);
|
|
231
172
|
}
|
|
232
173
|
|
|
233
|
-
// Subdirectory pages make strategies 2 and 3 collapse to the same URL — probe
|
|
234
|
-
// each candidate once.
|
|
235
174
|
return [...new Set(possiblePaths)];
|
|
236
175
|
};
|
|
237
176
|
|
|
238
|
-
/**
|
|
239
|
-
* Detect page-specific manifest (async, cached after first call)
|
|
240
|
-
* Returns { manifest, path } or null
|
|
241
|
-
*/
|
|
242
177
|
const detectHyperspeed = async () => {
|
|
243
178
|
if (hyperspeedDetectionAttempted) return hyperspeedData;
|
|
244
179
|
hyperspeedDetectionAttempted = true;
|
|
245
180
|
|
|
246
|
-
// Runtime-mode pages still have [vibe-fouc] / .vibe-fouc on their vibe root
|
|
247
|
-
// at this point — the compiler strips it at build time, and the runtime only
|
|
248
|
-
// clears it after hydration (PHASE_READY). Vibe can latch to any element, so
|
|
249
|
-
// search the whole document. If any fouc marker is still here, we're in
|
|
250
|
-
// runtime mode and no hyperspeed manifest will exist — skip the network
|
|
251
|
-
// fetches and avoid the 404 devtools noise.
|
|
252
181
|
const skipNetwork = !!document.querySelector("[vibe-fouc], .vibe-fouc");
|
|
253
182
|
|
|
254
183
|
try {
|
|
255
|
-
// window.__MANIFEST__ is an explicit manifest URL stamped into the page by
|
|
256
|
-
// the build/dev server. The compiled SPA shell needs it: the shell is
|
|
257
|
-
// served for EVERY route, so pathname-derived candidates point at manifests
|
|
258
|
-
// that don't exist (deep links probed 404s and never found the shell's own
|
|
259
|
-
// manifest). When present it is the single, authoritative candidate.
|
|
260
|
-
//
|
|
261
|
-
// window.__ROUTE__ is the page's route template (e.g. "/brawlers/:index"),
|
|
262
|
-
// injected by the compiler/dev server for dynamic pages. It lets us resolve
|
|
263
|
-
// the tokenized `$` manifest directly instead of probing literal 404s.
|
|
264
184
|
const hinted = typeof window !== "undefined" ? window.__MANIFEST__ : null;
|
|
265
185
|
const possiblePaths = hinted
|
|
266
186
|
? [hinted]
|
|
@@ -272,12 +192,7 @@ const detectHyperspeed = async () => {
|
|
|
272
192
|
if (possiblePaths.length === 0) return null;
|
|
273
193
|
|
|
274
194
|
if (!skipNetwork) {
|
|
275
|
-
// Fully-runtime dynamic import. Hidden behind `new Function` so any
|
|
276
|
-
// bundler's static-analysis can't read into it — there's nothing we
|
|
277
|
-
// could or should tell it about these manifest paths, which are decided
|
|
278
|
-
// at runtime by searching a list.
|
|
279
195
|
const dynamicImport = new Function('p', 'return import(p)');
|
|
280
|
-
// Try each possible path
|
|
281
196
|
for (const manifestPath of possiblePaths) {
|
|
282
197
|
try {
|
|
283
198
|
const module = await dynamicImport(manifestPath);
|
|
@@ -287,43 +202,21 @@ const detectHyperspeed = async () => {
|
|
|
287
202
|
};
|
|
288
203
|
return hyperspeedData;
|
|
289
204
|
} catch (e) {
|
|
290
|
-
// Try next path
|
|
291
205
|
continue;
|
|
292
206
|
}
|
|
293
207
|
}
|
|
294
208
|
}
|
|
295
209
|
|
|
296
|
-
// When skipping network, yield a macrotask so module-graph timing matches
|
|
297
|
-
// the old behavior where `await import()` on a missing manifest resolved
|
|
298
|
-
// via a network 404 (macrotask), not a microtask. Without this yield, the
|
|
299
|
-
// vibe module-graph resolves too fast and the post-boot microtask fires
|
|
300
|
-
// before sibling `<script type="module">` tags (e.g. component scripts)
|
|
301
|
-
// have had a chance to register their state.
|
|
302
210
|
if (skipNetwork) {
|
|
303
211
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
304
212
|
}
|
|
305
213
|
|
|
306
|
-
// No manifest found
|
|
307
214
|
return null;
|
|
308
215
|
} catch {
|
|
309
|
-
// No manifest for this page - runtime-only mode
|
|
310
216
|
return null;
|
|
311
217
|
}
|
|
312
218
|
};
|
|
313
219
|
|
|
314
|
-
/**
|
|
315
|
-
* Restore DOM from pre-rendered values to @[...] markers using pre-compiled manifest
|
|
316
|
-
* This enables FOUC-free loading while maintaining runtime reactivity
|
|
317
|
-
*
|
|
318
|
-
* Flow:
|
|
319
|
-
* 1. Page loads with pre-rendered values: <strong>John Doe</strong> (visible, no FOUC)
|
|
320
|
-
* 2. Restoration: Replace with markers: <strong>@[firstName] @[lastName]</strong>
|
|
321
|
-
* 3. Runtime processes normally: Finds markers, makes reactive
|
|
322
|
-
*
|
|
323
|
-
* @param {Element} rootElement - The DOM element to restore (e.g., <body>)
|
|
324
|
-
* @param {Object} subtree - The matching subtree from manifest (e.g., manifest.children.body)
|
|
325
|
-
* @param {Object} fullManifest - The full manifest (unused now, kept for compatibility)
|
|
326
|
-
*/
|
|
327
220
|
export const restoreMarkersFromManifest = (
|
|
328
221
|
rootElement,
|
|
329
222
|
subtree,
|
|
@@ -332,68 +225,46 @@ export const restoreMarkersFromManifest = (
|
|
|
332
225
|
const walkTree = (tree, element) => {
|
|
333
226
|
if (!tree || !element) return;
|
|
334
227
|
|
|
335
|
-
// Check for compiled restoration data
|
|
336
228
|
const restoration = tree.compiled?.restoration;
|
|
337
229
|
|
|
338
230
|
if (restoration) {
|
|
339
|
-
// Restore text content with markers if parsed contains bindings
|
|
340
231
|
if (restoration.parsed && Array.isArray(restoration.parsed)) {
|
|
341
232
|
const hasBindings = restoration.parsed.some(
|
|
342
233
|
(item) => typeof item === "string" && item.includes("@["),
|
|
343
234
|
);
|
|
344
235
|
|
|
345
236
|
if (hasBindings) {
|
|
346
|
-
// Reconstruct original content with markers
|
|
347
237
|
let originalContent = restoration.parsed.join("");
|
|
348
238
|
|
|
349
|
-
// Transform component-scoped bindings back to this. format
|
|
350
|
-
// Compiler transforms @[this.count] → @[_c0.count] for stamping
|
|
351
|
-
// Runtime expects @[this.count], so transform back
|
|
352
239
|
originalContent = originalContent.replace(/@\[_c\d+\./g, "@[this.");
|
|
353
240
|
|
|
354
|
-
// For text nodes, update parent's innerHTML
|
|
355
|
-
// For elements with children, update only text nodes
|
|
356
241
|
if (
|
|
357
242
|
element.childNodes.length === 1 &&
|
|
358
243
|
element.childNodes[0].nodeType === 3
|
|
359
244
|
) {
|
|
360
|
-
// Single text node - replace it
|
|
361
245
|
element.childNodes[0].textContent = originalContent;
|
|
362
246
|
} else if (element.childNodes.length === 0) {
|
|
363
|
-
// No children - set textContent
|
|
364
247
|
element.textContent = originalContent;
|
|
365
248
|
}
|
|
366
|
-
// If element has multiple children, they'll be handled recursively
|
|
367
249
|
}
|
|
368
250
|
}
|
|
369
251
|
|
|
370
|
-
// Restore attribute bindings for reactivity
|
|
371
|
-
// Two cases:
|
|
372
|
-
// 1. Boolean-like attributes with falsy values were removed - restore them
|
|
373
|
-
// 2. Value attributes were stamped - replace stamped values with markers
|
|
374
252
|
if (restoration.attributes) {
|
|
375
253
|
for (let [attrName, attrValue] of Object.entries(
|
|
376
254
|
restoration.attributes,
|
|
377
255
|
)) {
|
|
378
|
-
// Transform component-scoped bindings back to this. format
|
|
379
|
-
// Compiler transforms @[this.count] → @[_c0.count], runtime expects @[this.count]
|
|
380
256
|
attrValue = attrValue.replace(/@\[_c\d+\./g, "@[this.");
|
|
381
257
|
|
|
382
|
-
// Always set the attribute to restore the marker
|
|
383
|
-
// - If missing (boolean-like, falsy): adds it back
|
|
384
|
-
// - If present (value attr, stamped): replaces stamped value with marker
|
|
385
258
|
element.setAttribute(attrName, attrValue);
|
|
386
259
|
}
|
|
387
260
|
}
|
|
388
261
|
|
|
389
|
-
// Restore name bindings (sparse array indexed by attribute position)
|
|
390
262
|
if (restoration.nameBindings && Array.isArray(restoration.nameBindings)) {
|
|
391
263
|
const attrs = Array.from(element.attributes);
|
|
392
264
|
|
|
393
265
|
for (let i = 0; i < restoration.nameBindings.length; i++) {
|
|
394
266
|
const marker = restoration.nameBindings[i];
|
|
395
267
|
if (marker && attrs[i]) {
|
|
396
|
-
// Replace the stamped attribute name with the marker
|
|
397
268
|
const stampedName = attrs[i].name;
|
|
398
269
|
const attrValue = attrs[i].value;
|
|
399
270
|
|
|
@@ -403,17 +274,11 @@ export const restoreMarkersFromManifest = (
|
|
|
403
274
|
}
|
|
404
275
|
}
|
|
405
276
|
|
|
406
|
-
// After restoration, delete compiled.restoration (runtime will parse DOM fresh)
|
|
407
277
|
if (tree.compiled) {
|
|
408
278
|
delete tree.compiled.restoration;
|
|
409
279
|
}
|
|
410
280
|
}
|
|
411
281
|
|
|
412
|
-
// IMPORTANT: Restore text nodes BEFORE processing conditionals/iterations
|
|
413
|
-
// Text nodes use childNodes indices which become invalid after DOM modifications
|
|
414
|
-
// Ascending index order, same invariant the directive restoration below
|
|
415
|
-
// relies on: each text node is returned to its pre-stamp state before a
|
|
416
|
-
// later index is consulted, so every index stays valid as we go.
|
|
417
282
|
if (tree.children) {
|
|
418
283
|
const texts = Object.keys(tree.children)
|
|
419
284
|
.filter((key) => key.startsWith("text_"))
|
|
@@ -431,23 +296,16 @@ export const restoreMarkersFromManifest = (
|
|
|
431
296
|
);
|
|
432
297
|
|
|
433
298
|
if (hasBindings) {
|
|
434
|
-
// Transform component-scoped bindings back to this. format
|
|
435
299
|
const originalContent = restoration.parsed
|
|
436
300
|
.join("")
|
|
437
301
|
.replace(/@\[_c\d+\./g, "@[this.");
|
|
438
302
|
|
|
439
303
|
const node = element.childNodes[index];
|
|
440
|
-
if (
|
|
304
|
+
if (index === 0 && Object.keys(tree.children).length === 1) {
|
|
305
|
+
element.textContent = originalContent;
|
|
306
|
+
} else if (node && node.nodeType === Node.TEXT_NODE) {
|
|
441
307
|
node.textContent = originalContent;
|
|
442
308
|
} else {
|
|
443
|
-
// The stamp ELIDED this text node: every binding in it rendered
|
|
444
|
-
// to an empty string, and an empty text node has no HTML
|
|
445
|
-
// serialization, so the compiled page carries no node here at all
|
|
446
|
-
// (`<out>@[removed.join(',')]</out>` with an empty array stamps to
|
|
447
|
-
// `<out></out>`). Recreate it at its recorded index — otherwise
|
|
448
|
-
// the marker never returns and the binding is dead for the page's
|
|
449
|
-
// lifetime, and every later sibling index at this level is off by
|
|
450
|
-
// one, so their restorations silently miss too.
|
|
451
309
|
element.insertBefore(
|
|
452
310
|
document.createTextNode(originalContent),
|
|
453
311
|
node ?? null,
|
|
@@ -462,13 +320,6 @@ export const restoreMarkersFromManifest = (
|
|
|
462
320
|
}
|
|
463
321
|
}
|
|
464
322
|
|
|
465
|
-
// Restore iterations/conditionals at THIS level by their childNodes index.
|
|
466
|
-
// The manifest key encodes the start comment's pre-stamp position
|
|
467
|
-
// (`conditional_13` = element.childNodes[13]); processing in ascending
|
|
468
|
-
// order keeps every index valid, because each restoration returns its
|
|
469
|
-
// region to the exact pre-stamp node count before the next index is
|
|
470
|
-
// consulted. Locating by index — not by expression text — is what
|
|
471
|
-
// disambiguates sibling directives that share the same expression.
|
|
472
323
|
if (tree.children) {
|
|
473
324
|
const directives = Object.keys(tree.children)
|
|
474
325
|
.filter((key) => {
|
|
@@ -483,8 +334,6 @@ export const restoreMarkersFromManifest = (
|
|
|
483
334
|
const childTree = tree.children[key];
|
|
484
335
|
const restoration = childTree.compiled?.restoration;
|
|
485
336
|
|
|
486
|
-
// Always delete the directive node from the tree — the runtime
|
|
487
|
-
// re-creates it when it parses the restored DOM.
|
|
488
337
|
delete tree.children[key];
|
|
489
338
|
|
|
490
339
|
if (!restoration?.template) continue;
|
|
@@ -494,8 +343,6 @@ export const restoreMarkersFromManifest = (
|
|
|
494
343
|
continue;
|
|
495
344
|
}
|
|
496
345
|
|
|
497
|
-
// Find the matching end marker at this sibling level, depth-counted
|
|
498
|
-
// so nested same-kind directives inside the region don't end it early.
|
|
499
346
|
const isIteration = childTree.type === "iteration";
|
|
500
347
|
const openPrefix = isIteration ? "each " : "if ";
|
|
501
348
|
const closeMarker = isIteration ? "/each" : "/if";
|
|
@@ -516,9 +363,6 @@ export const restoreMarkersFromManifest = (
|
|
|
516
363
|
}
|
|
517
364
|
if (!endComment) continue;
|
|
518
365
|
|
|
519
|
-
// Drop the stamped content (including the <!-- else --> marker and
|
|
520
|
-
// any stamped-row comments) and re-insert the pre-stamp template —
|
|
521
|
-
// node-for-node identical to what the manifest was built against.
|
|
522
366
|
let current = startComment.nextSibling;
|
|
523
367
|
while (current && current !== endComment) {
|
|
524
368
|
const next = current.nextSibling;
|
|
@@ -532,12 +376,10 @@ export const restoreMarkersFromManifest = (
|
|
|
532
376
|
}
|
|
533
377
|
}
|
|
534
378
|
|
|
535
|
-
// Recursively restore children (skip conditional/iteration children as they were already handled)
|
|
536
379
|
if (tree.children) {
|
|
537
380
|
for (const key in tree.children) {
|
|
538
381
|
const childTree = tree.children[key];
|
|
539
382
|
|
|
540
|
-
// Skip conditionals and iterations (already processed/deleted above)
|
|
541
383
|
if (
|
|
542
384
|
childTree.type === "conditional" ||
|
|
543
385
|
childTree.type === "iteration"
|
|
@@ -545,14 +387,10 @@ export const restoreMarkersFromManifest = (
|
|
|
545
387
|
continue;
|
|
546
388
|
}
|
|
547
389
|
|
|
548
|
-
// Skip text nodes - already handled before conditional/iteration processing
|
|
549
390
|
if (key.startsWith("text_")) {
|
|
550
391
|
continue;
|
|
551
392
|
}
|
|
552
393
|
|
|
553
|
-
// Find corresponding child element by tag name and index
|
|
554
|
-
// Keys like "layout_1" mean the node at childNodes index 1 (includes text nodes)
|
|
555
|
-
// Tag names can contain digits (h1, h2, etc.) so use [a-z0-9-]+
|
|
556
394
|
const match = key.match(/^([a-z0-9-]+)_(\d+)$/);
|
|
557
395
|
if (match) {
|
|
558
396
|
const tagName = match[1].toUpperCase();
|
|
@@ -575,7 +413,6 @@ export const restoreMarkersFromManifest = (
|
|
|
575
413
|
walkTree(subtree, rootElement);
|
|
576
414
|
};
|
|
577
415
|
|
|
578
|
-
// Top-level await to detect before module exports
|
|
579
416
|
hyperspeedData = await detectHyperspeed();
|
|
580
417
|
|
|
581
418
|
export const hyperspeedManifest = hyperspeedData?.manifest || null;
|
package/runtime/raw-html.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
// Marker for trusted raw-HTML output. `$.unsafe(str)` wraps a string so the
|
|
2
|
-
// hydrate path knows to set innerHTML instead of escaping via textContent.
|
|
3
|
-
// toString() returns the raw string so any non-pure use (the marker spliced
|
|
4
|
-
// into surrounding text) degrades to escaped literal text automatically — the
|
|
5
|
-
// browser escapes it when it lands in a text node.
|
|
6
1
|
export class RawHtml {
|
|
7
2
|
constructor(html) {
|
|
8
3
|
this.html = html == null ? '' : String(html);
|