@chr33s/solarflare 0.0.10 → 0.0.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chr33s/solarflare",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/readme.md CHANGED
@@ -128,6 +128,7 @@ import { Deferred } from "@chr33s/solarflare/client";
128
128
  <meta name="sf:base" content="/" />
129
129
  <meta name="sf:scroll-behavior" content="auto" />
130
130
  <meta name="sf:view-transitions" content="false" />
131
+ <meta name="sf:skip-node-replacement" content="s-*" />
131
132
 
132
133
  <!-- performance -->
133
134
  <meta name="sf:preconnect" content="https://cdn.example.com" />
@@ -1,11 +1,14 @@
1
1
  // src: https://github.com/brisa-build/diff-dom-streaming/blob/main/src/index.ts
2
2
 
3
+ type ShouldReplaceNode = (node: Node) => boolean;
4
+
3
5
  type Walker = {
4
6
  root: Node | null;
5
7
  [FIRST_CHILD]: (node: Node) => Promise<Node | null>;
6
8
  [NEXT_SIBLING]: (node: Node) => Promise<Node | null>;
7
9
  [APPLY_TRANSITION]: (v: () => void) => void;
8
10
  [FLUSH_SYNC]: () => void;
11
+ shouldReplaceNode?: ShouldReplaceNode;
9
12
  };
10
13
 
11
14
  type NextNodeCallback = (node: Node) => void;
@@ -18,6 +21,8 @@ type Options = {
18
21
  onChunkProcessed?: () => void;
19
22
  /** Apply mutations synchronously instead of batching via requestAnimationFrame. */
20
23
  syncMutations?: boolean;
24
+ /** Elements matching this predicate are atomically replaced instead of diffed. */
25
+ shouldReplaceNode?: ShouldReplaceNode;
21
26
  };
22
27
 
23
28
  const ELEMENT_TYPE = 1;
@@ -75,6 +80,19 @@ async function updateNode(oldNode: Node, newNode: Node, walker: Walker) {
75
80
  });
76
81
  }
77
82
 
83
+ // Elements matching shouldReplaceNode are atomically replaced.
84
+ // Use importNode to create the element in the target document context,
85
+ // avoiding adoptedCallback from firing on third-party web components.
86
+ if (walker.shouldReplaceNode?.(oldNode) || walker.shouldReplaceNode?.(newNode)) {
87
+ return walker[APPLY_TRANSITION](() => {
88
+ if (oldNode.parentNode) {
89
+ const doc = oldNode.ownerDocument ?? document;
90
+ const imported = doc.importNode(newNode, true);
91
+ oldNode.parentNode.replaceChild(imported, oldNode);
92
+ }
93
+ });
94
+ }
95
+
78
96
  await setChildNodes(oldNode, newNode, walker);
79
97
 
80
98
  walker[APPLY_TRANSITION](() => {
@@ -147,6 +165,17 @@ function setAttributes(oldAttributes: NamedNodeMap, newAttributes: NamedNodeMap)
147
165
  }
148
166
  }
149
167
 
168
+ /**
169
+ * Clone a node, using importNode for elements matching shouldReplaceNode
170
+ * to avoid triggering adoptedCallback on third-party web components.
171
+ */
172
+ function cloneForDocument(node: Node, targetDoc: Document, walker: Walker): Node {
173
+ if (walker.shouldReplaceNode?.(node)) {
174
+ return targetDoc.importNode(node, true);
175
+ }
176
+ return node.cloneNode(true);
177
+ }
178
+
150
179
  /**
151
180
  * Utility that will nodes childern to match another nodes children.
152
181
  */
@@ -237,7 +266,8 @@ async function setChildNodes(oldParent: Node, newParent: Node, walker: Walker) {
237
266
  checkOld = oldNode;
238
267
  oldNode = oldNode.nextSibling;
239
268
  if (getKey(checkOld)) {
240
- insertedNode = newNode.cloneNode(true);
269
+ const targetDoc = oldParent.ownerDocument ?? document;
270
+ insertedNode = cloneForDocument(newNode, targetDoc, walker);
241
271
  if (shouldInsertImmediately(insertedNode!)) {
242
272
  flushPendingInsertions();
243
273
  walker[APPLY_TRANSITION](() => {
@@ -258,7 +288,8 @@ async function setChildNodes(oldParent: Node, newParent: Node, walker: Walker) {
258
288
  await updateNode(checkOld, newNode, walker);
259
289
  }
260
290
  } else {
261
- insertedNode = newNode.cloneNode(true);
291
+ const targetDoc = oldParent.ownerDocument ?? document;
292
+ insertedNode = cloneForDocument(newNode, targetDoc, walker);
262
293
  if (shouldInsertImmediately(insertedNode!)) {
263
294
  flushPendingInsertions();
264
295
  walker[APPLY_TRANSITION](() => oldParent.appendChild(insertedNode!));
@@ -426,5 +457,6 @@ async function htmlStreamWalker(stream: ReadableStream, options: Options = {}) {
426
457
  }
427
458
  },
428
459
  [FLUSH_SYNC]: flushMutationsSync,
460
+ shouldReplaceNode: options.shouldReplaceNode,
429
461
  };
430
462
  }
@@ -19,6 +19,8 @@ export interface ApplyPatchStreamOptions {
19
19
  useTransition: boolean;
20
20
  applyMeta: (meta: PatchMeta) => void;
21
21
  onChunkProcessed?: () => void;
22
+ /** Elements matching this predicate are atomically replaced instead of diffed. */
23
+ shouldReplaceNode?: (node: Node) => boolean;
22
24
  }
23
25
 
24
26
  export async function applyPatchStream(response: Response, options: ApplyPatchStreamOptions) {
@@ -60,6 +62,7 @@ export async function applyPatchStream(response: Response, options: ApplyPatchSt
60
62
  transition: options.useTransition,
61
63
  syncMutations: !options.useTransition,
62
64
  onChunkProcessed: options.onChunkProcessed,
65
+ shouldReplaceNode: options.shouldReplaceNode,
63
66
  });
64
67
  await consumeHtml;
65
68
  }
package/src/router.ts CHANGED
@@ -35,11 +35,31 @@ export interface RouterConfig {
35
35
  onNotFound?: (url: URL) => void;
36
36
  onNavigate?: (match: RouteMatch) => void;
37
37
  onError?: (error: Error, url: URL) => void;
38
+ /** Elements matching this predicate are atomically replaced instead of diffed. */
39
+ shouldReplaceNode?: (node: Node) => boolean;
38
40
  }
39
41
 
40
42
  /** Subscription callback for route changes. */
41
43
  export type RouteSubscriber = (match: RouteMatch | null) => void;
42
44
 
45
+ /**
46
+ * Creates a node matcher from a glob-like pattern.
47
+ * Supports: "s-*" (prefix), "*-element" (suffix), "my-component" (exact)
48
+ * Multiple patterns can be comma-separated: "s-*,ui-*"
49
+ */
50
+ export function createNodeMatcher(pattern: string): (node: Node) => boolean {
51
+ const patterns = pattern.split(",").map((p) => p.trim().toUpperCase());
52
+ const matchers = patterns.map((p) => {
53
+ if (p.endsWith("*")) return (name: string) => name.startsWith(p.slice(0, -1));
54
+ if (p.startsWith("*")) return (name: string) => name.endsWith(p.slice(1));
55
+ return (name: string) => name === p;
56
+ });
57
+ return (node: Node) => {
58
+ const name = node.nodeName;
59
+ return matchers.some((m) => m(name));
60
+ };
61
+ }
62
+
43
63
  /** Checks if View Transitions API is supported. */
44
64
  export function supportsViewTransitions() {
45
65
  return typeof document !== "undefined" && "startViewTransition" in document;
@@ -48,7 +68,8 @@ export function supportsViewTransitions() {
48
68
  /** Client-side SPA router using Navigation API and View Transitions. */
49
69
  export class Router {
50
70
  #routes: Route[] = [];
51
- #config: Required<RouterConfig>;
71
+ #config: Required<Omit<RouterConfig, "shouldReplaceNode">> &
72
+ Pick<RouterConfig, "shouldReplaceNode">;
52
73
  #started = false;
53
74
  #cleanupFns: (() => void)[] = [];
54
75
  #inflightAbort: AbortController | null = null;
@@ -72,6 +93,12 @@ export class Router {
72
93
  const metaBase = getMeta("base");
73
94
  const metaViewTransitions = getMeta<"true" | "false">("view-transitions");
74
95
  const metaScrollBehavior = getMeta<"auto" | "smooth" | "instant">("scroll-behavior");
96
+ const metaSkipNodeReplace = getMeta("skip-node-replacement");
97
+
98
+ // Convert glob pattern (e.g., "s-*") to shouldReplaceNode predicate
99
+ const shouldReplaceNode =
100
+ config.shouldReplaceNode ??
101
+ (metaSkipNodeReplace ? createNodeMatcher(metaSkipNodeReplace) : undefined);
75
102
 
76
103
  this.#config = {
77
104
  base: config.base ?? metaBase ?? manifest.base ?? "",
@@ -84,6 +111,7 @@ export class Router {
84
111
  ((error, url) => {
85
112
  console.error(`[solarflare] Navigation error for ${url.href}:`, error);
86
113
  }),
114
+ shouldReplaceNode,
87
115
  };
88
116
 
89
117
  this.params = computed(() => this.current.value?.params ?? {});
@@ -293,6 +321,7 @@ export class Router {
293
321
  await applyPatchStream(response, {
294
322
  useTransition,
295
323
  applyMeta,
324
+ shouldReplaceNode: this.#config.shouldReplaceNode,
296
325
  onChunkProcessed: () => {
297
326
  if (didScroll) return;
298
327
  didScroll = true;