@dom-expressions/runtime 0.50.0-next.18 → 0.50.0-next.20
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/CHANGELOG.md +29 -0
- package/package.json +2 -2
- package/src/client.js +22 -12
- package/src/server.d.ts +48 -12
- package/src/server.js +37 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# dom-expressions
|
|
2
2
|
|
|
3
|
+
## 0.50.0-next.20
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9d5a90b: Queue streamed fragment activations whose `pl-*` marker is not yet in the live DOM instead of silently dropping them.
|
|
8
|
+
|
|
9
|
+
A fragment's marker can sit inside a flushed-but-unactivated ancestor `<template>` (a slot held by a reveal group). Template content is inert, so `document.getElementById` cannot see the marker and `$df`/`$dfl` previously returned `0` and lost the swap permanently — the fallback stayed stuck even though the content template had streamed. Today this window is masked because the server enrolls nested boundaries into the ancestor reveal group, but fixing that enrollment (solidjs/solid#2871, solidjs/solid#2872) makes nested boundaries activate independently, exposing the drop.
|
|
10
|
+
|
|
11
|
+
`$df` and `$dfl` now queue marker misses (`_$HY.dq` / `_$HY.dlq`) and a new `$dfd` drains both queues after every successful swap or fallback materialization — the only events that can bring queued markers into the live document. Content swaps drain before fallbacks so a settled fragment wins over its own pending fallback, and drains cascade through arbitrarily nested held levels. An activation whose content template is already consumed remains a plain no-op and is never queued.
|
|
12
|
+
|
|
13
|
+
## 0.50.0-next.19
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 75de952: The `manifest` option of `renderToString`/`renderToStream` now also accepts a
|
|
18
|
+
resolver — `{ resolve(key), resolveSync?(key) }` — as an alternative to a
|
|
19
|
+
static manifest object, letting dev servers answer asset lookups from their
|
|
20
|
+
live module graph while production keeps passing the built manifest object.
|
|
21
|
+
`resolve` may return a promise and may resolve CSS entries to inline-style
|
|
22
|
+
descriptors (`{ id, content, attrs }`) for HMR adoption; `resolveSync`
|
|
23
|
+
answers with what is knowable without async work (typically js URLs) for
|
|
24
|
+
sync consumers like a lazy component's `moduleUrl` getter used by islands,
|
|
25
|
+
and is exposed on the render context as `resolveAssetsSync` (object
|
|
26
|
+
manifests, being sync by nature, expose it too). A bare function is accepted
|
|
27
|
+
as shorthand for `{ resolve }`. The consumer contract stays
|
|
28
|
+
`renderToStream(fn, { manifest })` in both modes. Entry-asset
|
|
29
|
+
auto-registration only applies to object manifests, since a resolver cannot
|
|
30
|
+
be enumerated for entries.
|
|
31
|
+
|
|
3
32
|
## 0.50.0-next.18
|
|
4
33
|
|
|
5
34
|
## 0.50.0-next.17
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dom-expressions/runtime",
|
|
3
3
|
"description": "A Fine-Grained Runtime for Performant DOM Rendering",
|
|
4
|
-
"version": "0.50.0-next.
|
|
4
|
+
"version": "0.50.0-next.20",
|
|
5
5
|
"author": "Ryan Carniato",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"csstype": "^3.1",
|
|
27
27
|
"seroval": "~1.5.0",
|
|
28
28
|
"seroval-plugins": "~1.5.0",
|
|
29
|
-
"@dom-expressions/babel-plugin-jsx": "0.50.0-next.
|
|
29
|
+
"@dom-expressions/babel-plugin-jsx": "0.50.0-next.20"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"test": "jest --no-cache",
|
package/src/client.js
CHANGED
|
@@ -1020,8 +1020,7 @@ function insertExpression(parent, value, current, marker) {
|
|
|
1020
1020
|
if (tc === "string" || tc === "number") {
|
|
1021
1021
|
parent.firstChild.data = value;
|
|
1022
1022
|
} else {
|
|
1023
|
-
|
|
1024
|
-
if (owned === -1 || owned === parent.childNodes.length) parent.textContent = value;
|
|
1023
|
+
if (ownsAllChildren(parent, current)) parent.textContent = value;
|
|
1025
1024
|
else {
|
|
1026
1025
|
// Foreign nodes present (e.g. stream-injected stylesheet links) —
|
|
1027
1026
|
// replace only our own nodes, keeping text content leading.
|
|
@@ -1106,14 +1105,26 @@ function appendNodes(parent, array, marker = null) {
|
|
|
1106
1105
|
}
|
|
1107
1106
|
}
|
|
1108
1107
|
|
|
1109
|
-
//
|
|
1110
|
-
//
|
|
1111
|
-
//
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1108
|
+
// Whether the tracked `current` value accounts for all of the parent's
|
|
1109
|
+
// children, using only O(1) boundary pointer reads — `childNodes.length`
|
|
1110
|
+
// re-counts the child list after every mutation, which is O(n) on exactly
|
|
1111
|
+
// the hot markerless clear path this guards. Foreign nodes appended after
|
|
1112
|
+
// our content (late-flushed stylesheet <link>s at the end of <body>) break
|
|
1113
|
+
// the `lastChild` identity. `current == null` (initial render / untracked
|
|
1114
|
+
// content) reports true, preserving the designed clear-on-first-render
|
|
1115
|
+
// behavior.
|
|
1116
|
+
function ownsAllChildren(parent, current) {
|
|
1117
|
+
if (current == null) return true;
|
|
1118
|
+
if (Array.isArray(current)) {
|
|
1119
|
+
return current.length
|
|
1120
|
+
? parent.firstChild === current[0] && parent.lastChild === current[current.length - 1]
|
|
1121
|
+
: parent.firstChild === null;
|
|
1122
|
+
}
|
|
1123
|
+
if (current === "") return parent.firstChild === null; // `textContent = ""` left no node behind
|
|
1124
|
+
if (current.nodeType) return parent.firstChild === current && parent.lastChild === current;
|
|
1125
|
+
// string/number content lives in a single leading text node
|
|
1126
|
+
const first = parent.firstChild;
|
|
1127
|
+
return first !== null && first.nodeType === 3 && parent.lastChild === first;
|
|
1117
1128
|
}
|
|
1118
1129
|
|
|
1119
1130
|
// Remove only the nodes tracked by `current` from a root-level (markerless)
|
|
@@ -1141,8 +1152,7 @@ function cleanChildren(parent, current, marker, replacement) {
|
|
|
1141
1152
|
// children. Streaming can append foreign nodes to the root (late-flushed
|
|
1142
1153
|
// stylesheet <link>s land at the end of <body>) and those must survive a
|
|
1143
1154
|
// re-render — wiping them drops loaded CSS.
|
|
1144
|
-
|
|
1145
|
-
if (owned === -1 || owned === parent.childNodes.length) return (parent.textContent = "");
|
|
1155
|
+
if (ownsAllChildren(parent, current)) return (parent.textContent = "");
|
|
1146
1156
|
return removeOwnedChildren(parent, current);
|
|
1147
1157
|
}
|
|
1148
1158
|
if (current.length) {
|
package/src/server.d.ts
CHANGED
|
@@ -11,6 +11,51 @@ export const Namespaces: Record<string, string>;
|
|
|
11
11
|
|
|
12
12
|
type MountableElement = Element | Document | ShadowRoot | DocumentFragment | Node;
|
|
13
13
|
|
|
14
|
+
/** Static asset manifest produced by a build (e.g. parsed Vite manifest.json). */
|
|
15
|
+
export type AssetManifest = Record<
|
|
16
|
+
string,
|
|
17
|
+
{ file: string; css?: string[]; isEntry?: boolean; imports?: string[] }
|
|
18
|
+
> & { _base?: string };
|
|
19
|
+
|
|
20
|
+
/** Inline style content, e.g. dev CSS collected from a bundler's module graph. */
|
|
21
|
+
export type InlineStyleAsset = {
|
|
22
|
+
id: string;
|
|
23
|
+
content: string;
|
|
24
|
+
attrs?: Record<string, string>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ResolvedAssets = {
|
|
28
|
+
js: string[];
|
|
29
|
+
css: (string | InlineStyleAsset)[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolver form of the manifest option — the primitive a dev server
|
|
34
|
+
* implements against its live module graph (a static manifest object is
|
|
35
|
+
* normalized into a sync resolver internally). `resolve` may return a
|
|
36
|
+
* promise (async resolvers require streaming rendering); CSS entries may be
|
|
37
|
+
* URL strings (emitted as load-gated `<link>` tags) or inline-style
|
|
38
|
+
* descriptors (emitted as `<style>` tags). A bare `resolve`-shaped function
|
|
39
|
+
* is accepted as shorthand for `{ resolve }`.
|
|
40
|
+
*/
|
|
41
|
+
export type AssetResolver = {
|
|
42
|
+
resolve(
|
|
43
|
+
key: string
|
|
44
|
+
): ResolvedAssets | null | undefined | Promise<ResolvedAssets | null | undefined>;
|
|
45
|
+
/**
|
|
46
|
+
* Synchronous fast path answering with whatever is knowable without async
|
|
47
|
+
* work (typically js URLs, omitting css). Sync consumers — e.g. a lazy
|
|
48
|
+
* component's `moduleUrl` getter used by islands — use this when `resolve`
|
|
49
|
+
* would return a promise, so adapters should provide it whenever possible.
|
|
50
|
+
*/
|
|
51
|
+
resolveSync?(key: string): ResolvedAssets | null | undefined;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Bare-function shorthand for `AssetResolver` (no sync fast path). */
|
|
55
|
+
export type AssetResolverFn = (
|
|
56
|
+
key: string
|
|
57
|
+
) => ResolvedAssets | null | undefined | Promise<ResolvedAssets | null | undefined>;
|
|
58
|
+
|
|
14
59
|
export function renderToString<T>(
|
|
15
60
|
fn: () => T,
|
|
16
61
|
options?: {
|
|
@@ -18,10 +63,7 @@ export function renderToString<T>(
|
|
|
18
63
|
renderId?: string;
|
|
19
64
|
noScripts?: boolean;
|
|
20
65
|
plugins?: any[];
|
|
21
|
-
manifest?:
|
|
22
|
-
string,
|
|
23
|
-
{ file: string; css?: string[]; isEntry?: boolean; imports?: string[] }
|
|
24
|
-
> & { _base?: string };
|
|
66
|
+
manifest?: AssetManifest | AssetResolver | AssetResolverFn;
|
|
25
67
|
onError?: (err: any) => void;
|
|
26
68
|
}
|
|
27
69
|
): string;
|
|
@@ -34,10 +76,7 @@ export function renderToStringAsync<T>(
|
|
|
34
76
|
renderId?: string;
|
|
35
77
|
noScripts?: boolean;
|
|
36
78
|
plugins?: any[];
|
|
37
|
-
manifest?:
|
|
38
|
-
string,
|
|
39
|
-
{ file: string; css?: string[]; isEntry?: boolean; imports?: string[] }
|
|
40
|
-
> & { _base?: string };
|
|
79
|
+
manifest?: AssetManifest | AssetResolver | AssetResolverFn;
|
|
41
80
|
onError?: (err: any) => void;
|
|
42
81
|
}
|
|
43
82
|
): Promise<string>;
|
|
@@ -48,10 +87,7 @@ export function renderToStream<T>(
|
|
|
48
87
|
renderId?: string;
|
|
49
88
|
noScripts?: boolean;
|
|
50
89
|
plugins?: any[];
|
|
51
|
-
manifest?:
|
|
52
|
-
string,
|
|
53
|
-
{ file: string; css?: string[]; isEntry?: boolean; imports?: string[] }
|
|
54
|
-
> & { _base?: string };
|
|
90
|
+
manifest?: AssetManifest | AssetResolver | AssetResolverFn;
|
|
55
91
|
onCompleteShell?: (info: { write: (v: string) => void }) => void;
|
|
56
92
|
onCompleteAll?: (info: { write: (v: string) => void }) => void;
|
|
57
93
|
onError?: (err: any) => void;
|
package/src/server.js
CHANGED
|
@@ -62,7 +62,9 @@ function resolveAssets(moduleUrl, manifest) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function registerEntryAssets(manifest) {
|
|
65
|
-
|
|
65
|
+
// Resolver manifests can't be enumerated for entries; consumers that want
|
|
66
|
+
// SSR'd entry assets resolve their entry key themselves.
|
|
67
|
+
if (!manifest || typeof manifest === "function" || typeof manifest.resolve === "function") return;
|
|
66
68
|
const ctx = sharedConfig.context;
|
|
67
69
|
if (!ctx?.registerAsset) return;
|
|
68
70
|
for (const key in manifest) {
|
|
@@ -148,21 +150,52 @@ function applyAssetTracking(context, tracking, manifest) {
|
|
|
148
150
|
});
|
|
149
151
|
context.registerModule = tracking.registerModule;
|
|
150
152
|
context.getBoundaryModules = tracking.getBoundaryModules;
|
|
151
|
-
|
|
153
|
+
// A manifest can be the static object produced by a build (sync lookups,
|
|
154
|
+
// entry enumeration) or a resolver — the primitive a dev server implements
|
|
155
|
+
// against its live module graph: `{ resolve, resolveSync? }`, where
|
|
156
|
+
// `resolve` may return a promise and may resolve css entries to
|
|
157
|
+
// inline-style descriptors instead of URLs, and `resolveSync` answers with
|
|
158
|
+
// what is knowable without async work (for sync consumers like a lazy
|
|
159
|
+
// component's moduleUrl getter). A bare function is accepted as shorthand
|
|
160
|
+
// for `{ resolve }`. Callers (the reactive library's lazy()) handle both
|
|
161
|
+
// result shapes. Static manifests are sync by nature, so both context
|
|
162
|
+
// paths point at the same lookup.
|
|
163
|
+
if (typeof manifest === "function") {
|
|
164
|
+
context.resolveAssets = manifest;
|
|
165
|
+
} else if (manifest && typeof manifest.resolve === "function") {
|
|
166
|
+
context.resolveAssets = key => manifest.resolve(key);
|
|
167
|
+
if (typeof manifest.resolveSync === "function") {
|
|
168
|
+
context.resolveAssetsSync = key => manifest.resolveSync(key);
|
|
169
|
+
}
|
|
170
|
+
} else if (manifest) {
|
|
171
|
+
const resolve = moduleUrl => resolveAssets(moduleUrl, manifest);
|
|
172
|
+
context.resolveAssets = resolve;
|
|
173
|
+
context.resolveAssetsSync = resolve;
|
|
174
|
+
}
|
|
152
175
|
}
|
|
153
176
|
|
|
154
177
|
// Based on https://github.com/WebReflection/domtagger/blob/master/esm/sanitizer.js
|
|
155
178
|
const VOID_ELEMENTS =
|
|
156
179
|
/^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i;
|
|
157
180
|
// Fragment replacement helpers emitted into stream task scripts:
|
|
158
|
-
// - $df(id): swap template payload into the `pl-*` marker range.
|
|
181
|
+
// - $df(id): swap template payload into the `pl-*` marker range. A marker that
|
|
182
|
+
// isn't in the live DOM yet (it sits inside a flushed-but-unactivated ancestor
|
|
183
|
+
// template held by a reveal group) queues the id in `_$HY.dq` for retry instead
|
|
184
|
+
// of dropping the swap. A missing content template means the swap already ran —
|
|
185
|
+
// that stays a plain no-op and is never queued.
|
|
159
186
|
// - $dfl(id): materialize fallback from `pl-*` template content without resolving.
|
|
187
|
+
// Marker misses queue in `_$HY.dlq`, same reasoning as $df.
|
|
160
188
|
// - $dflj(ids): materialize fallback content for every id in the list.
|
|
189
|
+
// - $dfd(): drain both retry queues. Runs after every successful swap or fallback
|
|
190
|
+
// materialization — the only events that can bring queued markers into the live
|
|
191
|
+
// document. Content swaps ($df) drain before fallbacks ($dfl) so a settled
|
|
192
|
+
// fragment wins over its own pending fallback. Each pass snapshots the queue,
|
|
193
|
+
// so still-inert entries simply re-queue and wait for the next swap.
|
|
161
194
|
// - $dfs(id, count, defer): register pending stylesheet count for fragment `id`.
|
|
162
195
|
// - $dfc(id): style completion callback; reveals when the fragment/group is unblocked.
|
|
163
196
|
// - $dfg(id): group-style gate check; reveals a waiting group once all style counts hit zero.
|
|
164
197
|
// - $dfj(ids): reveal a group in registration order, waiting if any member still has pending styles.
|
|
165
|
-
const REPLACE_SCRIPT = `function $df(e,n,o,t){if(!(n=document.getElementById(e))
|
|
198
|
+
const REPLACE_SCRIPT = `function $df(e,n,o,t){if(!(n=document.getElementById(e)))return 0;if(!(o=document.getElementById("pl-"+e)))return(_$HY.dq=_$HY.dq||{})[e]=1,0;for(;o&&8!==o.nodeType&&o.nodeValue!=="pl-"+e;)t=o.nextSibling,o.remove(),o=t;_$HY.done?o.remove():o.replaceWith(n.content),n.remove(),_$HY.fe(e),$dfd();return 1}function $dfl(e,o,n){if(!(o=document.getElementById("pl-"+e)))return(_$HY.dlq=_$HY.dlq||{})[e]=1,0;if(o._$fl)return 1;for(n=o.nextSibling;n;){if(8===n.nodeType&&n.nodeValue==="pl-"+e){o.parentNode&&o.parentNode.insertBefore(o.content.cloneNode(!0),n),o._$fl=1,$dfd();return 1}n=n.nextSibling}return 0}function $dflj(e,i){for(i=0;i<e.length;i++)$dfl(e[i])}function $dfd(e,i){if(e=_$HY.dq){_$HY.dq=0;for(i in e)$df(i)}if(e=_$HY.dlq){_$HY.dlq=0;for(i in e)$dfl(i)}}function $dfs(e,c,d){(_$HY.sc=_$HY.sc||{})[e]=c,d&&((_$HY.sd=_$HY.sd||{})[e]=1)}function $dfg(e,g,i,k){if(!(g=_$HY.sg&&_$HY.sg[e]))return;for(i=0;i<g.length;i++)if(_$HY.sc&&_$HY.sc[g[i]]>0)return;for(i=0;i<g.length;i++)k=g[i],delete _$HY.sg[k],$df(k)}function $dfc(e){if(--_$HY.sc[e]<=0){delete _$HY.sc[e],_$HY.sg&&_$HY.sg[e]?$dfg(e):!(_$HY.sd&&_$HY.sd[e])&&$df(e);_$HY.sd&&delete _$HY.sd[e]}}function $dfj(e,i,n){for(i=0;i<e.length;i++)if(_$HY.sc&&_$HY.sc[e[i]]>0){for(n=0;n<e.length;n++)(_$HY.sg=_$HY.sg||{})[e[n]]=e;return}for(i=0;i<e.length;i++)$df(e[i])}`;
|
|
166
199
|
|
|
167
200
|
export function renderToString(code, options = {}) {
|
|
168
201
|
const { renderId = "", nonce, noScripts, manifest } = options;
|