@dom-expressions/runtime 0.50.0-next.17 → 0.50.0-next.19

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # dom-expressions
2
2
 
3
+ ## 0.50.0-next.19
4
+
5
+ ### Patch Changes
6
+
7
+ - 75de952: The `manifest` option of `renderToString`/`renderToStream` now also accepts a
8
+ resolver — `{ resolve(key), resolveSync?(key) }` — as an alternative to a
9
+ static manifest object, letting dev servers answer asset lookups from their
10
+ live module graph while production keeps passing the built manifest object.
11
+ `resolve` may return a promise and may resolve CSS entries to inline-style
12
+ descriptors (`{ id, content, attrs }`) for HMR adoption; `resolveSync`
13
+ answers with what is knowable without async work (typically js URLs) for
14
+ sync consumers like a lazy component's `moduleUrl` getter used by islands,
15
+ and is exposed on the render context as `resolveAssetsSync` (object
16
+ manifests, being sync by nature, expose it too). A bare function is accepted
17
+ as shorthand for `{ resolve }`. The consumer contract stays
18
+ `renderToStream(fn, { manifest })` in both modes. Entry-asset
19
+ auto-registration only applies to object manifests, since a resolver cannot
20
+ be enumerated for entries.
21
+
22
+ ## 0.50.0-next.18
23
+
3
24
  ## 0.50.0-next.17
4
25
 
5
26
  ### Patch Changes
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.17",
4
+ "version": "0.50.0-next.19",
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.17"
29
+ "@dom-expressions/babel-plugin-jsx": "0.50.0-next.19"
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
- const owned = ownedChildCount(current);
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
- // Number of parent children the tracked `current` value accounts for, or -1
1110
- // when unknown (initial render / untracked content the whole parent is
1111
- // considered owned, preserving the designed clear-on-first-render behavior).
1112
- function ownedChildCount(current) {
1113
- if (Array.isArray(current)) return current.length;
1114
- if (current == null) return -1;
1115
- if (current === "") return 0; // `textContent = ""` left no node behind
1116
- return 1; // single node, or a string/number rendered as one text node
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
- const owned = ownedChildCount(current);
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?: Record<
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?: Record<
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?: Record<
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
- if (!manifest) return;
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,7 +150,28 @@ function applyAssetTracking(context, tracking, manifest) {
148
150
  });
149
151
  context.registerModule = tracking.registerModule;
150
152
  context.getBoundaryModules = tracking.getBoundaryModules;
151
- if (manifest) context.resolveAssets = moduleUrl => resolveAssets(moduleUrl, manifest);
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