@chr33s/solarflare 0.0.9 → 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 +1 -1
- package/readme.md +1 -0
- package/src/client.styles.ts +9 -6
- package/src/client.ts +9 -5
- package/src/diff-dom-streaming.ts +16 -0
- package/src/router-stream.ts +3 -0
- package/src/router.ts +30 -1
- package/src/stylesheets.ts +25 -2
package/package.json
CHANGED
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" />
|
package/src/client.styles.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
safeAdoptStylesheets,
|
|
3
|
+
stylesheets,
|
|
4
|
+
supportsConstructableStylesheets,
|
|
5
|
+
} from "./stylesheets.ts";
|
|
2
6
|
|
|
3
7
|
/** Style loading state for a component. */
|
|
4
8
|
interface StyleState {
|
|
@@ -51,12 +55,11 @@ export function applyStyles(element: HTMLElement, sheets: CSSStyleSheet[]) {
|
|
|
51
55
|
const shadowRoot = element.shadowRoot;
|
|
52
56
|
|
|
53
57
|
if (shadowRoot) {
|
|
54
|
-
shadowRoot
|
|
58
|
+
safeAdoptStylesheets(shadowRoot, [...shadowRoot.adoptedStyleSheets, ...sheets]);
|
|
55
59
|
} else {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
+
const toAdd = sheets.filter((s) => !document.adoptedStyleSheets.includes(s));
|
|
61
|
+
if (toAdd.length) {
|
|
62
|
+
safeAdoptStylesheets(document, [...document.adoptedStyleSheets, ...toAdd]);
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
}
|
package/src/client.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { parsePath } from "./paths.ts";
|
|
|
3
3
|
import { hydrateStore, initHydrationCoordinator } from "./hydration.ts";
|
|
4
4
|
import { installHeadHoisting, createHeadContext, setHeadContext } from "./head.ts";
|
|
5
5
|
import { getRuntime } from "./runtime.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
safeAdoptStylesheets,
|
|
8
|
+
stylesheets,
|
|
9
|
+
supportsConstructableStylesheets,
|
|
10
|
+
} from "./stylesheets.ts";
|
|
7
11
|
import { getPreloadedStylesheet } from "./server.styles.ts";
|
|
8
12
|
|
|
9
13
|
export { initHmrEntry, reloadAllStylesheets } from "./hmr.ts";
|
|
@@ -51,15 +55,15 @@ export function registerInlineStyles(tag: string, styles: InlineStyleEntry[]) {
|
|
|
51
55
|
const sheets = stylesheets.getForConsumer(tag);
|
|
52
56
|
const shadowRoot = el?.shadowRoot;
|
|
53
57
|
if (shadowRoot) {
|
|
54
|
-
shadowRoot
|
|
58
|
+
safeAdoptStylesheets(shadowRoot, [
|
|
55
59
|
...shadowRoot.adoptedStyleSheets.filter((s) => !sheets.includes(s)),
|
|
56
60
|
...sheets,
|
|
57
|
-
];
|
|
61
|
+
]);
|
|
58
62
|
} else {
|
|
59
|
-
document
|
|
63
|
+
safeAdoptStylesheets(document, [
|
|
60
64
|
...document.adoptedStyleSheets.filter((s) => !sheets.includes(s)),
|
|
61
65
|
...sheets,
|
|
62
|
-
];
|
|
66
|
+
]);
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|
|
@@ -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
|
}
|
package/src/router-stream.ts
CHANGED
|
@@ -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;
|
package/src/stylesheets.ts
CHANGED
|
@@ -9,6 +9,29 @@ export const supportsConstructableStylesheets = () => {
|
|
|
9
9
|
}
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
/** Recreates a stylesheet in the current document context. */
|
|
13
|
+
function cloneSheet(sheet: CSSStyleSheet): CSSStyleSheet {
|
|
14
|
+
const fresh = new CSSStyleSheet();
|
|
15
|
+
fresh.replaceSync([...sheet.cssRules].map((r) => r.cssText).join(""));
|
|
16
|
+
return fresh;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Safely adopts stylesheets, recreating them if cross-document sharing fails. */
|
|
20
|
+
export function safeAdoptStylesheets(
|
|
21
|
+
target: Document | ShadowRoot,
|
|
22
|
+
sheets: CSSStyleSheet[],
|
|
23
|
+
): CSSStyleSheet[] {
|
|
24
|
+
try {
|
|
25
|
+
target.adoptedStyleSheets = sheets;
|
|
26
|
+
return sheets;
|
|
27
|
+
} catch {
|
|
28
|
+
// Stylesheet was created in a different document context - recreate
|
|
29
|
+
const cloned = sheets.map(cloneSheet);
|
|
30
|
+
target.adoptedStyleSheets = cloned;
|
|
31
|
+
return cloned;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
/** Stylesheet entry with metadata. */
|
|
13
36
|
interface StylesheetEntry {
|
|
14
37
|
sheet: CSSStyleSheet;
|
|
@@ -143,7 +166,7 @@ class StylesheetManager {
|
|
|
143
166
|
// Include global sheets
|
|
144
167
|
const globalSheets = [...this.#sheets.values()].filter((e) => e.isGlobal).map((e) => e.sheet);
|
|
145
168
|
|
|
146
|
-
shadowRoot
|
|
169
|
+
safeAdoptStylesheets(shadowRoot, [...globalSheets, ...sheets]);
|
|
147
170
|
}
|
|
148
171
|
|
|
149
172
|
/** Removes a consumer from all its stylesheets. */
|
|
@@ -171,7 +194,7 @@ class StylesheetManager {
|
|
|
171
194
|
#adoptToDocument(sheet: CSSStyleSheet) {
|
|
172
195
|
if (!this.#documentSheets.includes(sheet)) {
|
|
173
196
|
this.#documentSheets.push(sheet);
|
|
174
|
-
document
|
|
197
|
+
safeAdoptStylesheets(document, [...this.#documentSheets]);
|
|
175
198
|
}
|
|
176
199
|
}
|
|
177
200
|
|