@chr33s/solarflare 0.0.10 → 0.0.11

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.11",
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,16 @@ async function updateNode(oldNode: Node, newNode: Node, walker: Walker) {
75
80
  });
76
81
  }
77
82
 
83
+ // Elements matching shouldReplaceNode are atomically replaced to avoid
84
+ // triggering adoptedCallback on third-party web components (e.g. Polaris).
85
+ if (walker.shouldReplaceNode?.(oldNode) || walker.shouldReplaceNode?.(newNode)) {
86
+ return walker[APPLY_TRANSITION](() => {
87
+ if (oldNode.parentNode) {
88
+ oldNode.parentNode.replaceChild(newNode.cloneNode(true), oldNode);
89
+ }
90
+ });
91
+ }
92
+
78
93
  await setChildNodes(oldNode, newNode, walker);
79
94
 
80
95
  walker[APPLY_TRANSITION](() => {
@@ -426,5 +441,6 @@ async function htmlStreamWalker(stream: ReadableStream, options: Options = {}) {
426
441
  }
427
442
  },
428
443
  [FLUSH_SYNC]: flushMutationsSync,
444
+ shouldReplaceNode: options.shouldReplaceNode,
429
445
  };
430
446
  }
@@ -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;