@lovrozagar/flare 0.1.2 → 0.2.0
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/README.md +42 -19
- package/package.json +8 -6
- package/src/broadcast/index.ts +1 -1
- package/src/broadcast/provider.tsx +2 -2
- package/src/caches/index.ts +26 -0
- package/src/components/await.tsx +36 -39
- package/src/components/dev-error-overlay.tsx +8 -9
- package/src/components/direction-script.tsx +2 -2
- package/src/components/locale-script.tsx +2 -2
- package/src/components/reset-css.tsx +1 -1
- package/src/components/ssr-context.tsx +7 -14
- package/src/components/theme-script.tsx +2 -2
- package/src/components/view-transition-css.tsx +1 -1
- package/src/defer/index.ts +22 -0
- package/src/direction/index.tsx +61 -36
- package/src/errors/index.ts +45 -11
- package/src/fonts/component.tsx +2 -1
- package/src/form/index.tsx +18 -12
- package/src/generators/index.ts +73 -30
- package/src/head-client/index.ts +7 -0
- package/src/history/index.ts +6 -0
- package/src/hydrate/index.tsx +90 -60
- package/src/hydration/index.ts +26 -6
- package/src/i18n/index.ts +11 -2
- package/src/image/index.tsx +7 -4
- package/src/intercept-outlet/index.tsx +6 -2
- package/src/lazy/index.tsx +50 -50
- package/src/link/index.tsx +97 -58
- package/src/loader-pipeline/index.ts +93 -35
- package/src/locale/cookie.ts +15 -0
- package/src/locale/index.tsx +24 -35
- package/src/locale.ts +1 -0
- package/src/middleware/builtins/api-proxy.ts +58 -4
- package/src/middleware/builtins/cdn-proxy.ts +24 -3
- package/src/middleware/builtins/i18n.ts +23 -25
- package/src/middleware/builtins/keepalive.ts +2 -3
- package/src/mount/index.ts +8 -1
- package/src/navigation/index.ts +575 -129
- package/src/navigation/types.ts +11 -1
- package/src/navigation-progress/index.tsx +17 -16
- package/src/ndjson-client/index.ts +42 -17
- package/src/ndjson-server/index.ts +78 -29
- package/src/outlet/index.tsx +192 -97
- package/src/outlet/types.ts +9 -1
- package/src/plugins/dev-cdn-cache.ts +18 -5
- package/src/plugins/dev-dashboard/client.tsx +130 -118
- package/src/plugins/dev-prerender.ts +6 -1
- package/src/plugins/dev-server.ts +12 -3
- package/src/plugins/image-plugin.ts +18 -4
- package/src/plugins/index.ts +10 -7
- package/src/plugins/prerender-plugin.ts +3 -3
- package/src/plugins/server-fn.ts +15 -20
- package/src/plugins/solid-jsx.d.ts +3 -3
- package/src/plugins/sx-ast/atomic.ts +5 -5
- package/src/plugins/sx-ast/index.ts +9 -0
- package/src/plugins/sx.d.ts +4 -5
- package/src/plugins/virtual.ts +11 -6
- package/src/prerender/index.ts +68 -32
- package/src/protocol.ts +65 -0
- package/src/query-client/index.tsx +73 -14
- package/src/rewrite/index.ts +10 -4
- package/src/route-builder/create-layout.ts +1 -1
- package/src/route-builder/create-root-layout.ts +1 -1
- package/src/route-builder/types.ts +5 -1
- package/src/router-primitives/types.ts +2 -0
- package/src/security/index.ts +41 -5
- package/src/server/index.ts +1 -2
- package/src/server-fn/get-input.ts +90 -0
- package/src/server-fn/http-error.ts +13 -0
- package/src/server-fn/index.ts +94 -70
- package/src/server-fn-query.ts +9 -17
- package/src/server-handler/etag.ts +4 -2
- package/src/server-handler/headers.ts +1 -2
- package/src/server-handler/index.ts +164 -56
- package/src/server-handler/types.ts +2 -0
- package/src/service-worker/template.ts +10 -42
- package/src/ssr/head-prefix.ts +2 -1
- package/src/ssr/hoist-head-markers.ts +25 -0
- package/src/ssr/index.tsx +109 -92
- package/src/state-parser/index.ts +38 -26
- package/src/styles/index.ts +89 -60
- package/src/testing/index.ts +8 -3
- package/src/theme/index.tsx +35 -32
- package/src/tracing/timing.ts +6 -1
- package/src/url/index.ts +9 -0
package/src/styles/index.ts
CHANGED
|
@@ -47,6 +47,10 @@ export function __setMaxRegistrySize(size: number): void {
|
|
|
47
47
|
const registry = new Map<string, string>();
|
|
48
48
|
let domInjectionEnabled = false;
|
|
49
49
|
let ssrSheetPresent = false;
|
|
50
|
+
/* Names whose scoped rules are in the live `#flare-runtime` sheet. Distinct
|
|
51
|
+
* from `registry`: a name can be registered while the SSR sheet gate is up
|
|
52
|
+
* (lazy `styles()` during hydrate) and never inserted. */
|
|
53
|
+
const injected = new Set<string>();
|
|
50
54
|
|
|
51
55
|
export function enableDomInjection(): void {
|
|
52
56
|
domInjectionEnabled = true;
|
|
@@ -58,6 +62,14 @@ export function enableDomInjection(): void {
|
|
|
58
62
|
|
|
59
63
|
export function finishHydration(): void {
|
|
60
64
|
ssrSheetPresent = false;
|
|
65
|
+
/* Flush CSS registered while the SSR sheet blocked insertRule — otherwise
|
|
66
|
+
* a later styles() call sees registry.has and never injects. */
|
|
67
|
+
for (const [name, scoped] of registry) {
|
|
68
|
+
ensureInjected(name, scoped, "attr");
|
|
69
|
+
}
|
|
70
|
+
for (const [name, scoped] of classRegistry) {
|
|
71
|
+
ensureInjected(name, scoped, "class");
|
|
72
|
+
}
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
function hashString(str: string): string {
|
|
@@ -892,11 +904,16 @@ export function registerCSS(css: string): string {
|
|
|
892
904
|
}
|
|
893
905
|
|
|
894
906
|
export function registerCSSByName(name: string, css: string): string {
|
|
895
|
-
|
|
907
|
+
const existing = registry.get(name);
|
|
908
|
+
if (existing !== undefined) {
|
|
909
|
+
ensureInjected(name, existing, "attr");
|
|
910
|
+
return name;
|
|
911
|
+
}
|
|
896
912
|
if (isDev) validateCSS(name, css);
|
|
897
913
|
const scoped = minifyCSS(scopeCSS(name, css));
|
|
898
914
|
registry.set(name, scoped);
|
|
899
915
|
injectStyleToDOM(scoped);
|
|
916
|
+
markInjected(name);
|
|
900
917
|
/* FIFO eviction: remove oldest entries when over limit */
|
|
901
918
|
if (registry.size > maxRegistrySize) {
|
|
902
919
|
const iter = registry.keys();
|
|
@@ -904,6 +921,7 @@ export function registerCSSByName(name: string, css: string): string {
|
|
|
904
921
|
const oldest = iter.next();
|
|
905
922
|
if (oldest.done) break;
|
|
906
923
|
registry.delete(oldest.value);
|
|
924
|
+
injected.delete(oldest.value);
|
|
907
925
|
}
|
|
908
926
|
rebuildSheet();
|
|
909
927
|
}
|
|
@@ -917,81 +935,85 @@ export function registerCSSByName(name: string, css: string): string {
|
|
|
917
935
|
*/
|
|
918
936
|
export function forceRegisterCSSByName(name: string, css: string): void {
|
|
919
937
|
registry.delete(name);
|
|
938
|
+
injected.delete(name);
|
|
920
939
|
registerCSSByName(name, css);
|
|
921
940
|
}
|
|
922
941
|
|
|
942
|
+
function readDocumentNonce(): string {
|
|
943
|
+
if (typeof document === "undefined") return "";
|
|
944
|
+
const meta = document.querySelector('meta[name="csp-nonce"]');
|
|
945
|
+
const fromContent = meta?.getAttribute("content");
|
|
946
|
+
if (fromContent) return fromContent;
|
|
947
|
+
const tagged = document.querySelector("script[nonce], style[nonce]") as HTMLElement | null;
|
|
948
|
+
return tagged?.nonce || tagged?.getAttribute("nonce") || "";
|
|
949
|
+
}
|
|
950
|
+
|
|
923
951
|
/**
|
|
924
|
-
*
|
|
925
|
-
*
|
|
952
|
+
* Insert a complete nonce'd `<style id="flare-runtime">`. CSP checks nonce at
|
|
953
|
+
* insertion; mutating an in-document sheet is not covered by that nonce.
|
|
926
954
|
*/
|
|
927
|
-
function
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
depth--;
|
|
935
|
-
if (depth === 0) {
|
|
936
|
-
const rule = css.slice(start, i + 1).trim();
|
|
937
|
-
if (rule.length > 0) rules.push(rule);
|
|
938
|
-
start = i + 1;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
955
|
+
function writeRuntimeSheet(css: string): void {
|
|
956
|
+
const el = document.createElement("style");
|
|
957
|
+
el.id = RUNTIME_SHEET_ID;
|
|
958
|
+
const nonce = readDocumentNonce();
|
|
959
|
+
if (nonce) {
|
|
960
|
+
el.setAttribute("nonce", nonce);
|
|
961
|
+
el.nonce = nonce;
|
|
941
962
|
}
|
|
942
|
-
|
|
963
|
+
el.textContent = css;
|
|
964
|
+
const old = document.getElementById(RUNTIME_SHEET_ID);
|
|
965
|
+
if (old) old.replaceWith(el);
|
|
966
|
+
else document.head.appendChild(el);
|
|
943
967
|
}
|
|
944
968
|
|
|
945
|
-
function
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
969
|
+
function liveSheetText(): string {
|
|
970
|
+
if (typeof document === "undefined") return "";
|
|
971
|
+
const el = document.getElementById(RUNTIME_SHEET_ID) as HTMLStyleElement | null;
|
|
972
|
+
if (!el) return "";
|
|
973
|
+
if (el.sheet) {
|
|
974
|
+
return Array.from(el.sheet.cssRules)
|
|
975
|
+
.map((r) => r.cssText)
|
|
976
|
+
.join("");
|
|
951
977
|
}
|
|
952
|
-
return
|
|
978
|
+
return el.textContent ?? "";
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function sheetHasName(name: string, kind: "attr" | "class"): boolean {
|
|
982
|
+
const text = liveSheetText();
|
|
983
|
+
if (!text) return false;
|
|
984
|
+
if (kind === "class") return text.includes(`.${name}`);
|
|
985
|
+
const esc = escapeCSSAttrValue(name);
|
|
986
|
+
return text.includes(`[data-c="${esc}"]`) || text.includes(`[data-c=${esc}]`);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function markInjected(name: string): void {
|
|
990
|
+
if (domInjectionEnabled && !ssrSheetPresent) injected.add(name);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function ensureInjected(name: string, scoped: string, kind: "attr" | "class"): void {
|
|
994
|
+
if (!domInjectionEnabled || ssrSheetPresent || typeof document === "undefined") {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (sheetHasName(name, kind)) {
|
|
998
|
+
injected.add(name);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
injectStyleToDOM(scoped);
|
|
1002
|
+
injected.add(name);
|
|
953
1003
|
}
|
|
954
1004
|
|
|
955
|
-
/**
|
|
956
|
-
* Inject scoped CSS into the DOM via CSSOM insertRule (CSP-safe).
|
|
957
|
-
* Falls back to textContent append for environments without CSSOM (jsdom).
|
|
958
|
-
*/
|
|
959
1005
|
function injectStyleToDOM(scoped: string): void {
|
|
960
1006
|
if (!domInjectionEnabled || ssrSheetPresent || typeof document === "undefined") return;
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
if (sheet) {
|
|
964
|
-
for (const rule of splitCSSRules(scoped)) {
|
|
965
|
-
try {
|
|
966
|
-
sheet.insertRule(rule, sheet.cssRules.length);
|
|
967
|
-
} catch {
|
|
968
|
-
/* Invalid rule — dev validator catches these separately */
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
} else {
|
|
972
|
-
styleEl.appendChild(document.createTextNode(scoped));
|
|
973
|
-
}
|
|
1007
|
+
const current = document.getElementById(RUNTIME_SHEET_ID)?.textContent ?? "";
|
|
1008
|
+
writeRuntimeSheet(current + scoped);
|
|
974
1009
|
}
|
|
975
1010
|
|
|
976
|
-
/**
|
|
977
|
-
* Rebuild the sheet from current registry after eviction.
|
|
978
|
-
*/
|
|
979
1011
|
function rebuildSheet(): void {
|
|
980
1012
|
if (!domInjectionEnabled || typeof document === "undefined") return;
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
for (const scoped of registry.values()) {
|
|
986
|
-
for (const rule of splitCSSRules(scoped)) {
|
|
987
|
-
try {
|
|
988
|
-
sheet.insertRule(rule, sheet.cssRules.length);
|
|
989
|
-
} catch {}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
} else {
|
|
993
|
-
styleEl.textContent = Array.from(registry.values()).join("");
|
|
994
|
-
}
|
|
1013
|
+
injected.clear();
|
|
1014
|
+
writeRuntimeSheet(Array.from(registry.values()).join("") + Array.from(classRegistry.values()).join(""));
|
|
1015
|
+
for (const name of registry.keys()) injected.add(name);
|
|
1016
|
+
for (const name of classRegistry.keys()) injected.add(name);
|
|
995
1017
|
}
|
|
996
1018
|
|
|
997
1019
|
export function getScopedStyles(): string {
|
|
@@ -1020,9 +1042,14 @@ function scopeCssToClass(className: string, css: string): string {
|
|
|
1020
1042
|
* Mirrors registerCSSByName but keys by class name and uses class selectors.
|
|
1021
1043
|
*/
|
|
1022
1044
|
export function registerCSSAsClass(className: string, scopedCssInLayer: string): string {
|
|
1023
|
-
|
|
1045
|
+
const existing = classRegistry.get(className);
|
|
1046
|
+
if (existing !== undefined) {
|
|
1047
|
+
ensureInjected(className, existing, "class");
|
|
1048
|
+
return className;
|
|
1049
|
+
}
|
|
1024
1050
|
classRegistry.set(className, scopedCssInLayer);
|
|
1025
1051
|
injectStyleToDOM(scopedCssInLayer);
|
|
1052
|
+
markInjected(className);
|
|
1026
1053
|
/* FIFO eviction mirrors main registry */
|
|
1027
1054
|
if (classRegistry.size > maxRegistrySize) {
|
|
1028
1055
|
const iter = classRegistry.keys();
|
|
@@ -1030,6 +1057,7 @@ export function registerCSSAsClass(className: string, scopedCssInLayer: string):
|
|
|
1030
1057
|
const oldest = iter.next();
|
|
1031
1058
|
if (oldest.done) break;
|
|
1032
1059
|
classRegistry.delete(oldest.value);
|
|
1060
|
+
injected.delete(oldest.value);
|
|
1033
1061
|
}
|
|
1034
1062
|
}
|
|
1035
1063
|
return className;
|
|
@@ -1185,6 +1213,7 @@ export function cn(...inputs: CnValue[]): string {
|
|
|
1185
1213
|
export function clearScopedStyles(): void {
|
|
1186
1214
|
registry.clear();
|
|
1187
1215
|
classRegistry.clear();
|
|
1216
|
+
injected.clear();
|
|
1188
1217
|
domInjectionEnabled = false;
|
|
1189
1218
|
ssrSheetPresent = false;
|
|
1190
1219
|
if (typeof document !== "undefined") {
|
package/src/testing/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EMPTY_ARR } from "../internal/index.ts";
|
|
2
|
+
import { ATTR_HYDRATED } from "../protocol.ts";
|
|
2
3
|
|
|
3
4
|
const HYDRATION_PATTERNS = [/hydration/i, /mismatch/i, /unable to find dom nodes/i];
|
|
4
5
|
|
|
@@ -148,9 +149,13 @@ export class FlarePage {
|
|
|
148
149
|
/* ── Hydration ──────────────────────────────────────────────────── */
|
|
149
150
|
|
|
150
151
|
async waitForHydration(timeout = 15_000): Promise<void> {
|
|
151
|
-
await this.page.waitForFunction(
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
await this.page.waitForFunction(
|
|
153
|
+
`document.documentElement.hasAttribute(${JSON.stringify(ATTR_HYDRATED)})`,
|
|
154
|
+
undefined,
|
|
155
|
+
{
|
|
156
|
+
timeout,
|
|
157
|
+
},
|
|
158
|
+
);
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
/* ── State ──────────────────────────────────────────────────────── */
|
package/src/theme/index.tsx
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createContext,
|
|
3
3
|
createEffect,
|
|
4
|
+
createMemo,
|
|
4
5
|
createSignal,
|
|
5
|
-
|
|
6
|
-
on,
|
|
7
|
-
onCleanup,
|
|
8
|
-
onMount,
|
|
6
|
+
onSettled,
|
|
9
7
|
sharedConfig,
|
|
8
|
+
untrack,
|
|
10
9
|
useContext,
|
|
11
10
|
} from "solid-js";
|
|
11
|
+
import { isServer, type JSX } from "@solidjs/web";
|
|
12
12
|
|
|
13
13
|
export type Theme = "light" | "dark" | "system";
|
|
14
14
|
export type ResolvedTheme = "light" | "dark";
|
|
@@ -57,7 +57,7 @@ export interface ThemeContextValue {
|
|
|
57
57
|
toggleTheme: () => void;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
const ThemeCtx = createContext<ThemeContextValue>();
|
|
60
|
+
const ThemeCtx = createContext<ThemeContextValue | null>(null);
|
|
61
61
|
|
|
62
62
|
export function ThemeProvider(props: { children: JSX.Element; config?: ThemeConfig }): JSX.Element {
|
|
63
63
|
const cfg: Required<ThemeConfig> = {
|
|
@@ -68,7 +68,7 @@ export function ThemeProvider(props: { children: JSX.Element; config?: ThemeConf
|
|
|
68
68
|
themes: props.config?.themes ?? DEFAULT_CONFIG.themes,
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
const hydrating = !!sharedConfig.
|
|
71
|
+
const hydrating = isServer || !!sharedConfig.hydrating;
|
|
72
72
|
|
|
73
73
|
/* During SSR + hydration, always start from defaultTheme so the tree matches.
|
|
74
74
|
ThemeScript already applied localStorage to <html> before first paint.
|
|
@@ -95,10 +95,10 @@ export function ThemeProvider(props: { children: JSX.Element; config?: ThemeConf
|
|
|
95
95
|
}
|
|
96
96
|
const [systemTheme, setSystemTheme] = createSignal<ResolvedTheme>(initialSystem);
|
|
97
97
|
|
|
98
|
-
const resolvedTheme = (): ResolvedTheme => {
|
|
98
|
+
const resolvedTheme = createMemo((): ResolvedTheme => {
|
|
99
99
|
const t = theme();
|
|
100
100
|
return t === "system" ? systemTheme() : t;
|
|
101
|
-
};
|
|
101
|
+
});
|
|
102
102
|
|
|
103
103
|
const applyToDocument = (resolved: ResolvedTheme): void => {
|
|
104
104
|
if (typeof document === "undefined") return;
|
|
@@ -114,36 +114,36 @@ export function ThemeProvider(props: { children: JSX.Element; config?: ThemeConf
|
|
|
114
114
|
/* Client remount / CSR: apply immediately. During hydration, ThemeScript owns
|
|
115
115
|
first paint — applying here would overwrite a stored theme with "system". */
|
|
116
116
|
if (!hydrating) {
|
|
117
|
-
applyToDocument(resolvedTheme
|
|
117
|
+
applyToDocument(untrack(resolvedTheme));
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/* After hydrate, sync from localStorage so useTheme() matches ThemeScript. */
|
|
121
|
-
|
|
121
|
+
onSettled(() => {
|
|
122
122
|
if (typeof localStorage === "undefined") return;
|
|
123
123
|
try {
|
|
124
124
|
const stored = localStorage.getItem(cfg.storageKey);
|
|
125
|
-
if (stored && cfg.themes.includes(stored as Theme) && stored !== theme
|
|
125
|
+
if (stored && cfg.themes.includes(stored as Theme) && stored !== untrack(theme)) {
|
|
126
126
|
setThemeSignal(stored as Theme);
|
|
127
127
|
}
|
|
128
128
|
} catch {
|
|
129
129
|
/* noop */
|
|
130
130
|
}
|
|
131
|
-
applyToDocument(resolvedTheme
|
|
131
|
+
applyToDocument(untrack(resolvedTheme));
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
/* Listen for OS preference changes */
|
|
135
|
-
|
|
135
|
+
onSettled(() => {
|
|
136
136
|
if (typeof matchMedia === "undefined") return;
|
|
137
137
|
const mq = matchMedia("(prefers-color-scheme: dark)");
|
|
138
138
|
const handler = (e: MediaQueryListEvent) => {
|
|
139
139
|
setSystemTheme(e.matches ? "dark" : "light");
|
|
140
140
|
};
|
|
141
141
|
mq.addEventListener("change", handler);
|
|
142
|
-
|
|
142
|
+
return () => mq.removeEventListener("change", handler);
|
|
143
143
|
});
|
|
144
144
|
|
|
145
145
|
/* Cross-tab sync: StorageEvent fires in OTHER tabs only */
|
|
146
|
-
|
|
146
|
+
onSettled(() => {
|
|
147
147
|
const handler = (e: StorageEvent) => {
|
|
148
148
|
if (e.key !== cfg.storageKey || e.storageArea !== localStorage) return;
|
|
149
149
|
const v = e.newValue;
|
|
@@ -152,19 +152,18 @@ export function ThemeProvider(props: { children: JSX.Element; config?: ThemeConf
|
|
|
152
152
|
}
|
|
153
153
|
};
|
|
154
154
|
window.addEventListener("storage", handler);
|
|
155
|
-
|
|
155
|
+
return () => window.removeEventListener("storage", handler);
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
/* Sync resolved theme to DOM (skip transition-disable on first run) */
|
|
159
159
|
let skipFirst = true;
|
|
160
|
-
createEffect(() => {
|
|
161
|
-
const resolved = resolvedTheme();
|
|
160
|
+
createEffect(resolvedTheme, (resolved) => {
|
|
162
161
|
if (typeof document === "undefined") return;
|
|
163
162
|
|
|
164
163
|
if (skipFirst) {
|
|
165
164
|
skipFirst = false;
|
|
166
165
|
/* After Solid hydrates <html> it drops ThemeScript's data-theme.
|
|
167
|
-
Re-apply the visual value (storage or system) without waiting for
|
|
166
|
+
Re-apply the visual value (storage or system) without waiting for onSettled. */
|
|
168
167
|
let visual: ResolvedTheme = resolved;
|
|
169
168
|
if (typeof localStorage !== "undefined") {
|
|
170
169
|
try {
|
|
@@ -180,6 +179,12 @@ export function ThemeProvider(props: { children: JSX.Element; config?: ThemeConf
|
|
|
180
179
|
|
|
181
180
|
if (cfg.disableTransitionOnChange) {
|
|
182
181
|
const style = document.createElement("style");
|
|
182
|
+
const script = document.querySelector("script[nonce]") as HTMLElement | null;
|
|
183
|
+
const nonce = script?.nonce || script?.getAttribute("nonce");
|
|
184
|
+
if (nonce) {
|
|
185
|
+
style.setAttribute("nonce", nonce);
|
|
186
|
+
style.nonce = nonce;
|
|
187
|
+
}
|
|
183
188
|
style.textContent = "* { transition: none !important }";
|
|
184
189
|
document.head.appendChild(style);
|
|
185
190
|
applyToDocument(resolved);
|
|
@@ -192,17 +197,15 @@ export function ThemeProvider(props: { children: JSX.Element; config?: ThemeConf
|
|
|
192
197
|
|
|
193
198
|
/* Persist to localStorage on changes (not initial — it was read from there) */
|
|
194
199
|
createEffect(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
{ defer: true },
|
|
205
|
-
),
|
|
200
|
+
theme,
|
|
201
|
+
(t) => {
|
|
202
|
+
try {
|
|
203
|
+
localStorage.setItem(cfg.storageKey, t);
|
|
204
|
+
} catch {
|
|
205
|
+
/* noop */
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{ defer: true },
|
|
206
209
|
);
|
|
207
210
|
|
|
208
211
|
const setTheme = (t: Theme): void => {
|
|
@@ -211,7 +214,7 @@ export function ThemeProvider(props: { children: JSX.Element; config?: ThemeConf
|
|
|
211
214
|
};
|
|
212
215
|
|
|
213
216
|
const toggleTheme = (): void => {
|
|
214
|
-
const current = resolvedTheme
|
|
217
|
+
const current = untrack(resolvedTheme);
|
|
215
218
|
setTheme(current === "light" ? "dark" : "light");
|
|
216
219
|
};
|
|
217
220
|
|
|
@@ -222,7 +225,7 @@ export function ThemeProvider(props: { children: JSX.Element; config?: ThemeConf
|
|
|
222
225
|
toggleTheme,
|
|
223
226
|
};
|
|
224
227
|
|
|
225
|
-
return <ThemeCtx
|
|
228
|
+
return <ThemeCtx value={value}>{props.children}</ThemeCtx>;
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
export function useTheme(): ThemeContextValue {
|
package/src/tracing/timing.ts
CHANGED
|
@@ -29,6 +29,11 @@ export function createTimingTracer(): TimingTracer {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/* Server-Timing `name` is an RFC 7230 token — no `:`, `/`, or spaces. */
|
|
33
|
+
function timingToken(name: string): string {
|
|
34
|
+
return name.replace(/[^0-9A-Za-z!#$%&'*+\-.^_`|~]/g, ".");
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
export function buildServerTimingHeader(entries: TimingEntry[]): string {
|
|
33
|
-
return entries.map((e) => `${e.name};dur=${e.dur.toFixed(1)}`).join(", ");
|
|
38
|
+
return entries.map((e) => `${timingToken(e.name)};dur=${e.dur.toFixed(1)}`).join(", ");
|
|
34
39
|
}
|
package/src/url/index.ts
CHANGED
|
@@ -138,3 +138,12 @@ export function buildUrl(options: BuildUrlOptions): string {
|
|
|
138
138
|
|
|
139
139
|
return result;
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
/** Path+search for a same-origin 3xx. Protocol-relative `//host` is not a path. */
|
|
143
|
+
export function sameOriginRedirectPath(url: URL): string {
|
|
144
|
+
const path = url.pathname;
|
|
145
|
+
if (!path.startsWith("/") || path.startsWith("//") || /[\0\r\n]/.test(path) || /[\0\r\n]/.test(url.search)) {
|
|
146
|
+
return "/";
|
|
147
|
+
}
|
|
148
|
+
return `${path}${url.search}`;
|
|
149
|
+
}
|