@rangojs/router 0.0.0-experimental.002d056c
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/AGENTS.md +9 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1606 -0
- package/dist/vite/index.js +5153 -0
- package/package.json +177 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +253 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +638 -0
- package/src/browser/navigation-client.ts +261 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +582 -0
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +145 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +368 -0
- package/src/browser/react/NavigationProvider.tsx +413 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +464 -0
- package/src/browser/scroll-restoration.ts +397 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +547 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +479 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +982 -0
- package/src/cache/cf/index.ts +29 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +44 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +281 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +397 -0
- package/src/router/lazy-includes.ts +236 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +269 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +193 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +749 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +320 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1242 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1006 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +237 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +920 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +48 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +363 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +445 -0
- package/src/vite/router-discovery.ts +777 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ThemeProvider - Client component that provides theme state and management.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Syncs theme to cookie/localStorage
|
|
8
|
+
* - Detects system preference changes
|
|
9
|
+
* - Cross-tab synchronization via storage events
|
|
10
|
+
* - Updates HTML element attribute when theme changes
|
|
11
|
+
* - Handles SSR hydration by deferring system theme detection
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, {
|
|
15
|
+
useCallback,
|
|
16
|
+
useEffect,
|
|
17
|
+
useMemo,
|
|
18
|
+
useState,
|
|
19
|
+
useRef,
|
|
20
|
+
} from "react";
|
|
21
|
+
import { ThemeContext } from "./theme-context.js";
|
|
22
|
+
import type {
|
|
23
|
+
ResolvedTheme,
|
|
24
|
+
ResolvedThemeConfig,
|
|
25
|
+
Theme,
|
|
26
|
+
ThemeContextValue,
|
|
27
|
+
ThemeProviderProps,
|
|
28
|
+
} from "./types.js";
|
|
29
|
+
import { THEME_COOKIE } from "./constants.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get system preference for color scheme
|
|
33
|
+
*/
|
|
34
|
+
function getSystemTheme(): ResolvedTheme {
|
|
35
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
36
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
37
|
+
? "dark"
|
|
38
|
+
: "light";
|
|
39
|
+
}
|
|
40
|
+
return "light";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read theme from cookie
|
|
45
|
+
*/
|
|
46
|
+
function readThemeFromCookie(storageKey: string): string | null {
|
|
47
|
+
if (typeof document === "undefined") return null;
|
|
48
|
+
|
|
49
|
+
const cookies = document.cookie.split(";");
|
|
50
|
+
for (const cookie of cookies) {
|
|
51
|
+
const [name, ...rest] = cookie.trim().split("=");
|
|
52
|
+
if (name === storageKey) {
|
|
53
|
+
const raw = rest.join("=");
|
|
54
|
+
try {
|
|
55
|
+
return decodeURIComponent(raw);
|
|
56
|
+
} catch {
|
|
57
|
+
return raw;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read theme from localStorage
|
|
66
|
+
*/
|
|
67
|
+
function readThemeFromStorage(storageKey: string): string | null {
|
|
68
|
+
if (typeof localStorage === "undefined") return null;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
return localStorage.getItem(storageKey);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Write theme to cookie
|
|
79
|
+
*/
|
|
80
|
+
function writeThemeToCookie(storageKey: string, theme: Theme): void {
|
|
81
|
+
if (typeof document === "undefined") return;
|
|
82
|
+
|
|
83
|
+
const value = encodeURIComponent(theme);
|
|
84
|
+
const cookie = `${storageKey}=${value}; Path=${THEME_COOKIE.path}; Max-Age=${THEME_COOKIE.maxAge}; SameSite=${THEME_COOKIE.sameSite}`;
|
|
85
|
+
document.cookie = cookie;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Write theme to localStorage
|
|
90
|
+
*/
|
|
91
|
+
function writeThemeToStorage(storageKey: string, theme: Theme): void {
|
|
92
|
+
if (typeof localStorage === "undefined") return;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
localStorage.setItem(storageKey, theme);
|
|
96
|
+
} catch {
|
|
97
|
+
// localStorage might be disabled or full
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Apply theme to HTML element
|
|
103
|
+
*/
|
|
104
|
+
function applyThemeToDocument(theme: Theme, config: ResolvedThemeConfig): void {
|
|
105
|
+
if (typeof document === "undefined") return;
|
|
106
|
+
|
|
107
|
+
const resolved =
|
|
108
|
+
theme === "system" && config.enableSystem
|
|
109
|
+
? getSystemTheme()
|
|
110
|
+
: (theme as ResolvedTheme);
|
|
111
|
+
|
|
112
|
+
const value = config.value[resolved] || resolved;
|
|
113
|
+
const el = document.documentElement;
|
|
114
|
+
|
|
115
|
+
// Apply attribute
|
|
116
|
+
if (config.attribute === "class") {
|
|
117
|
+
// Remove all theme classes
|
|
118
|
+
for (const t of config.themes) {
|
|
119
|
+
const v = config.value[t] || t;
|
|
120
|
+
el.classList.remove(v);
|
|
121
|
+
}
|
|
122
|
+
// Add current theme class
|
|
123
|
+
el.classList.add(value);
|
|
124
|
+
} else {
|
|
125
|
+
el.setAttribute(config.attribute, value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Set color-scheme for native dark mode support
|
|
129
|
+
if (config.enableColorScheme) {
|
|
130
|
+
el.style.colorScheme = resolved;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the resolved stored theme (validated against available themes)
|
|
136
|
+
*/
|
|
137
|
+
function getStoredTheme(config: ResolvedThemeConfig): Theme {
|
|
138
|
+
const { storageKey, themes, defaultTheme, enableSystem } = config;
|
|
139
|
+
|
|
140
|
+
// Try cookie first (for SSR consistency)
|
|
141
|
+
let stored = readThemeFromCookie(storageKey);
|
|
142
|
+
|
|
143
|
+
// Fall back to localStorage
|
|
144
|
+
if (!stored) {
|
|
145
|
+
stored = readThemeFromStorage(storageKey);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Validate stored value
|
|
149
|
+
if (stored) {
|
|
150
|
+
if (stored === "system" && enableSystem) {
|
|
151
|
+
return "system";
|
|
152
|
+
}
|
|
153
|
+
if (themes.includes(stored)) {
|
|
154
|
+
return stored as Theme;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return defaultTheme;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* ThemeProvider component
|
|
163
|
+
*
|
|
164
|
+
* Provides theme state to the component tree via context.
|
|
165
|
+
* Handles theme persistence, system preference detection, and cross-tab sync.
|
|
166
|
+
*/
|
|
167
|
+
export function ThemeProvider({
|
|
168
|
+
config,
|
|
169
|
+
initialTheme,
|
|
170
|
+
children,
|
|
171
|
+
}: ThemeProviderProps): React.ReactNode {
|
|
172
|
+
// Track mount state to avoid hydration mismatches
|
|
173
|
+
// During SSR and initial hydration, mounted is false
|
|
174
|
+
const [mounted, setMounted] = useState(false);
|
|
175
|
+
|
|
176
|
+
// Initialize theme from prop, storage, or default
|
|
177
|
+
const [theme, setThemeState] = useState<Theme>(() => {
|
|
178
|
+
if (initialTheme) return initialTheme;
|
|
179
|
+
if (typeof window === "undefined") return config.defaultTheme;
|
|
180
|
+
return getStoredTheme(config);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Track system preference - use stable default during SSR
|
|
184
|
+
const [systemTheme, setSystemTheme] = useState<ResolvedTheme>("light");
|
|
185
|
+
|
|
186
|
+
// Set mounted after hydration and detect actual system theme
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
setMounted(true);
|
|
189
|
+
setSystemTheme(getSystemTheme());
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
192
|
+
// Set theme and persist to storage
|
|
193
|
+
const setTheme = useCallback(
|
|
194
|
+
(newTheme: Theme) => {
|
|
195
|
+
setThemeState(newTheme);
|
|
196
|
+
writeThemeToCookie(config.storageKey, newTheme);
|
|
197
|
+
writeThemeToStorage(config.storageKey, newTheme);
|
|
198
|
+
applyThemeToDocument(newTheme, config);
|
|
199
|
+
},
|
|
200
|
+
[config],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Listen for system preference changes
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (!config.enableSystem) return;
|
|
206
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
207
|
+
|
|
208
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
209
|
+
|
|
210
|
+
const handleChange = (e: MediaQueryListEvent) => {
|
|
211
|
+
const newSystemTheme = e.matches ? "dark" : "light";
|
|
212
|
+
setSystemTheme(newSystemTheme);
|
|
213
|
+
|
|
214
|
+
// If current theme is "system", re-apply to update document
|
|
215
|
+
if (theme === "system") {
|
|
216
|
+
applyThemeToDocument("system", config);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Modern browsers
|
|
221
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
222
|
+
|
|
223
|
+
return () => {
|
|
224
|
+
mediaQuery.removeEventListener("change", handleChange);
|
|
225
|
+
};
|
|
226
|
+
}, [config, theme]);
|
|
227
|
+
|
|
228
|
+
// Cross-tab synchronization via localStorage storage event
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (typeof window === "undefined") return;
|
|
231
|
+
|
|
232
|
+
const handleStorageChange = (e: StorageEvent) => {
|
|
233
|
+
if (e.key !== config.storageKey) return;
|
|
234
|
+
|
|
235
|
+
const newTheme = e.newValue;
|
|
236
|
+
if (!newTheme) return;
|
|
237
|
+
|
|
238
|
+
// Validate and apply
|
|
239
|
+
if (newTheme === "system" || config.themes.includes(newTheme)) {
|
|
240
|
+
setThemeState(newTheme as Theme);
|
|
241
|
+
applyThemeToDocument(newTheme as Theme, config);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
window.addEventListener("storage", handleStorageChange);
|
|
246
|
+
|
|
247
|
+
return () => {
|
|
248
|
+
window.removeEventListener("storage", handleStorageChange);
|
|
249
|
+
};
|
|
250
|
+
}, [config]);
|
|
251
|
+
|
|
252
|
+
// Compute resolved theme
|
|
253
|
+
// During SSR (not mounted), use the initial theme or default to avoid hydration mismatch
|
|
254
|
+
const resolvedTheme: ResolvedTheme = useMemo(() => {
|
|
255
|
+
if (!mounted) {
|
|
256
|
+
// During SSR, return the initial theme if it's not "system", otherwise "light"
|
|
257
|
+
// The inline script will apply the correct class before hydration
|
|
258
|
+
if (initialTheme && initialTheme !== "system") {
|
|
259
|
+
return initialTheme as ResolvedTheme;
|
|
260
|
+
}
|
|
261
|
+
return "light";
|
|
262
|
+
}
|
|
263
|
+
if (theme === "system" && config.enableSystem) {
|
|
264
|
+
return systemTheme;
|
|
265
|
+
}
|
|
266
|
+
return theme as ResolvedTheme;
|
|
267
|
+
}, [theme, systemTheme, config.enableSystem, mounted, initialTheme]);
|
|
268
|
+
|
|
269
|
+
// Build themes list (include "system" if enabled)
|
|
270
|
+
const themes = useMemo(() => {
|
|
271
|
+
if (config.enableSystem) {
|
|
272
|
+
return ["system", ...config.themes.filter((t) => t !== "system")];
|
|
273
|
+
}
|
|
274
|
+
return config.themes;
|
|
275
|
+
}, [config.themes, config.enableSystem]);
|
|
276
|
+
|
|
277
|
+
// Context value
|
|
278
|
+
// During SSR (not mounted), return stable values to avoid hydration mismatch
|
|
279
|
+
const contextValue: ThemeContextValue = useMemo(
|
|
280
|
+
() => ({
|
|
281
|
+
theme,
|
|
282
|
+
setTheme,
|
|
283
|
+
resolvedTheme,
|
|
284
|
+
// Return stable "light" for systemTheme during SSR - actual value updates after mount
|
|
285
|
+
systemTheme: mounted ? systemTheme : "light",
|
|
286
|
+
themes,
|
|
287
|
+
config,
|
|
288
|
+
}),
|
|
289
|
+
[theme, setTheme, resolvedTheme, systemTheme, themes, config, mounted],
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<ThemeContext.Provider value={contextValue}>
|
|
294
|
+
{children}
|
|
295
|
+
</ThemeContext.Provider>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThemeScript - Server component that renders an inline script for FOUC prevention.
|
|
3
|
+
*
|
|
4
|
+
* This component renders a blocking inline script that:
|
|
5
|
+
* 1. Reads theme from cookie/localStorage before paint
|
|
6
|
+
* 2. Applies the theme to the HTML element immediately
|
|
7
|
+
* 3. Prevents flash of unstyled content (FOUC)
|
|
8
|
+
*
|
|
9
|
+
* Must be placed in the <head> element of your document, before any stylesheets.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* // In your document component
|
|
14
|
+
* import { ThemeScript } from "@rangojs/router/theme";
|
|
15
|
+
*
|
|
16
|
+
* export function Document({ children }) {
|
|
17
|
+
* return (
|
|
18
|
+
* <html lang="en" suppressHydrationWarning>
|
|
19
|
+
* <head>
|
|
20
|
+
* <ThemeScript />
|
|
21
|
+
* <MetaTags />
|
|
22
|
+
* </head>
|
|
23
|
+
* <body>{children}</body>
|
|
24
|
+
* </html>
|
|
25
|
+
* );
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import React from "react";
|
|
31
|
+
import { generateThemeScript } from "./theme-script.js";
|
|
32
|
+
import type { ResolvedThemeConfig } from "./types.js";
|
|
33
|
+
|
|
34
|
+
export interface ThemeScriptProps {
|
|
35
|
+
/**
|
|
36
|
+
* Theme configuration - passed from router.themeConfig
|
|
37
|
+
*/
|
|
38
|
+
config: ResolvedThemeConfig;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Optional nonce for CSP
|
|
42
|
+
*/
|
|
43
|
+
nonce?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Server component that renders the theme initialization script.
|
|
48
|
+
*
|
|
49
|
+
* This renders a synchronous inline script that applies the theme
|
|
50
|
+
* to the HTML element before React hydration, preventing FOUC.
|
|
51
|
+
*/
|
|
52
|
+
export function ThemeScript({
|
|
53
|
+
config,
|
|
54
|
+
nonce,
|
|
55
|
+
}: ThemeScriptProps): React.ReactNode {
|
|
56
|
+
const scriptContent = generateThemeScript(config);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<script nonce={nonce} dangerouslySetInnerHTML={{ __html: scriptContent }} />
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default values for theme configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ResolvedThemeConfig, ThemeConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default theme configuration values
|
|
9
|
+
*/
|
|
10
|
+
export const THEME_DEFAULTS = {
|
|
11
|
+
defaultTheme: "system",
|
|
12
|
+
themes: ["light", "dark"],
|
|
13
|
+
attribute: "class",
|
|
14
|
+
storageKey: "theme",
|
|
15
|
+
enableSystem: true,
|
|
16
|
+
enableColorScheme: true,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Cookie configuration for theme persistence
|
|
21
|
+
*/
|
|
22
|
+
export const THEME_COOKIE: {
|
|
23
|
+
readonly maxAge: number;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly sameSite: "lax";
|
|
26
|
+
} = {
|
|
27
|
+
maxAge: 60 * 60 * 24 * 365, // 1 year
|
|
28
|
+
path: "/",
|
|
29
|
+
sameSite: "lax",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve theme config by applying defaults.
|
|
34
|
+
* Accepts `true` to enable with all defaults, or a config object.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveThemeConfig(
|
|
37
|
+
config: ThemeConfig | true,
|
|
38
|
+
): ResolvedThemeConfig {
|
|
39
|
+
// Handle `theme: true` shorthand
|
|
40
|
+
if (config === true) {
|
|
41
|
+
config = {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const themes = config.themes ?? [...THEME_DEFAULTS.themes];
|
|
45
|
+
|
|
46
|
+
// Build value mapping - default to identity mapping
|
|
47
|
+
const value: Record<string, string> = {};
|
|
48
|
+
for (const theme of themes) {
|
|
49
|
+
value[theme] = config.value?.[theme] ?? theme;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
defaultTheme: config.defaultTheme ?? THEME_DEFAULTS.defaultTheme,
|
|
54
|
+
themes,
|
|
55
|
+
attribute: config.attribute ?? THEME_DEFAULTS.attribute,
|
|
56
|
+
storageKey: config.storageKey ?? THEME_DEFAULTS.storageKey,
|
|
57
|
+
enableSystem: config.enableSystem ?? THEME_DEFAULTS.enableSystem,
|
|
58
|
+
enableColorScheme:
|
|
59
|
+
config.enableColorScheme ?? THEME_DEFAULTS.enableColorScheme,
|
|
60
|
+
value,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme module exports for @rangojs/router/theme
|
|
3
|
+
*
|
|
4
|
+
* This module provides the public theme API:
|
|
5
|
+
* - useTheme: Hook for accessing theme state in client components
|
|
6
|
+
* - ThemeProvider: Component for manual theme provider setup (typically not needed)
|
|
7
|
+
* - ThemeScript: FOUC-prevention script component for document/head usage
|
|
8
|
+
* - Types for theme configuration
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* // In a client component
|
|
13
|
+
* import { useTheme } from "@rangojs/router/theme";
|
|
14
|
+
*
|
|
15
|
+
* function ThemeToggle() {
|
|
16
|
+
* const { theme, setTheme, themes } = useTheme();
|
|
17
|
+
* return (
|
|
18
|
+
* <select value={theme} onChange={e => setTheme(e.target.value)}>
|
|
19
|
+
* {themes.map(t => <option key={t}>{t}</option>)}
|
|
20
|
+
* </select>
|
|
21
|
+
* );
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Main hook for accessing theme
|
|
27
|
+
export { useTheme } from "./use-theme.js";
|
|
28
|
+
|
|
29
|
+
// Provider (typically auto-included via NavigationProvider when theme is enabled)
|
|
30
|
+
export { ThemeProvider } from "./ThemeProvider.js";
|
|
31
|
+
|
|
32
|
+
// Script component for FOUC prevention (use in document head)
|
|
33
|
+
export { ThemeScript, type ThemeScriptProps } from "./ThemeScript.js";
|
|
34
|
+
|
|
35
|
+
// Types
|
|
36
|
+
export type {
|
|
37
|
+
Theme,
|
|
38
|
+
ResolvedTheme,
|
|
39
|
+
ThemeAttribute,
|
|
40
|
+
ThemeConfig,
|
|
41
|
+
ResolvedThemeConfig,
|
|
42
|
+
UseThemeReturn,
|
|
43
|
+
ThemeProviderProps,
|
|
44
|
+
ThemeContextValue,
|
|
45
|
+
} from "./types.js";
|
|
46
|
+
|
|
47
|
+
// Constants
|
|
48
|
+
export { THEME_DEFAULTS, THEME_COOKIE } from "./constants.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Theme context for sharing theme state and configuration.
|
|
5
|
+
*
|
|
6
|
+
* Used by:
|
|
7
|
+
* - ThemeProvider to provide theme state
|
|
8
|
+
* - useTheme hook to access theme state
|
|
9
|
+
* - MetaTags to check if theme is enabled and render script
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createContext, useContext, type Context } from "react";
|
|
13
|
+
import type { ThemeContextValue } from "./types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* React context for theme state
|
|
17
|
+
* null when theme is not enabled in router config
|
|
18
|
+
*/
|
|
19
|
+
export const ThemeContext: Context<ThemeContextValue | null> =
|
|
20
|
+
createContext<ThemeContextValue | null>(null);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get theme context (internal use)
|
|
24
|
+
* Returns null if theme is not enabled
|
|
25
|
+
*/
|
|
26
|
+
export function useThemeContext(): ThemeContextValue | null {
|
|
27
|
+
return useContext(ThemeContext);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get theme context, throwing if not available
|
|
32
|
+
* Use this in useTheme hook
|
|
33
|
+
*/
|
|
34
|
+
export function requireThemeContext(): ThemeContextValue {
|
|
35
|
+
const ctx = useContext(ThemeContext);
|
|
36
|
+
if (!ctx) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"useTheme must be used within a ThemeProvider. " +
|
|
39
|
+
"Make sure theme is enabled in your router config: " +
|
|
40
|
+
"createRouter({ theme: { ... } })",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return ctx;
|
|
44
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates an inline script for FOUC prevention.
|
|
3
|
+
*
|
|
4
|
+
* This script runs synchronously before page paint to:
|
|
5
|
+
* 1. Read theme from cookie or localStorage
|
|
6
|
+
* 2. Detect system preference if theme is "system"
|
|
7
|
+
* 3. Apply theme to HTML element via class or data attribute
|
|
8
|
+
* 4. Optionally set color-scheme CSS property
|
|
9
|
+
*
|
|
10
|
+
* The script is minified and inlined in <head> before any other content.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ResolvedThemeConfig } from "./types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate the inline script for theme initialization
|
|
17
|
+
*
|
|
18
|
+
* The script is designed to:
|
|
19
|
+
* - Run synchronously before paint (blocking)
|
|
20
|
+
* - Be as small as possible to minimize blocking time
|
|
21
|
+
* - Work without any external dependencies
|
|
22
|
+
* - Handle all edge cases (no localStorage, no cookie, etc.)
|
|
23
|
+
*/
|
|
24
|
+
export function generateThemeScript(config: ResolvedThemeConfig): string {
|
|
25
|
+
// Build the script as a string, then minify
|
|
26
|
+
const script = `
|
|
27
|
+
(function() {
|
|
28
|
+
var storageKey = ${JSON.stringify(config.storageKey)};
|
|
29
|
+
var defaultTheme = ${JSON.stringify(config.defaultTheme)};
|
|
30
|
+
var attribute = ${JSON.stringify(config.attribute)};
|
|
31
|
+
var enableSystem = ${config.enableSystem};
|
|
32
|
+
var enableColorScheme = ${config.enableColorScheme};
|
|
33
|
+
var valueMap = ${JSON.stringify(config.value)};
|
|
34
|
+
var themes = ${JSON.stringify(config.themes)};
|
|
35
|
+
|
|
36
|
+
// Read theme from cookie or localStorage
|
|
37
|
+
function getStoredTheme() {
|
|
38
|
+
// Try cookie first (for SSR consistency)
|
|
39
|
+
var cookies = document.cookie.split(';');
|
|
40
|
+
for (var i = 0; i < cookies.length; i++) {
|
|
41
|
+
var cookie = cookies[i].trim();
|
|
42
|
+
if (cookie.indexOf(storageKey + '=') === 0) {
|
|
43
|
+
try { return decodeURIComponent(cookie.substring(storageKey.length + 1)); }
|
|
44
|
+
catch (e) { return cookie.substring(storageKey.length + 1); }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Fall back to localStorage
|
|
48
|
+
try {
|
|
49
|
+
return localStorage.getItem(storageKey);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Get system preference
|
|
56
|
+
function getSystemTheme() {
|
|
57
|
+
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
58
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
59
|
+
}
|
|
60
|
+
return 'light';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Resolve "system" to actual theme
|
|
64
|
+
function resolveTheme(theme) {
|
|
65
|
+
if (theme === 'system' && enableSystem) {
|
|
66
|
+
return getSystemTheme();
|
|
67
|
+
}
|
|
68
|
+
return theme;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Apply theme to HTML element
|
|
72
|
+
function applyTheme(theme) {
|
|
73
|
+
var resolved = resolveTheme(theme);
|
|
74
|
+
var value = valueMap[resolved] || resolved;
|
|
75
|
+
var el = document.documentElement;
|
|
76
|
+
|
|
77
|
+
// Apply attribute
|
|
78
|
+
if (attribute === 'class') {
|
|
79
|
+
// Remove all theme classes, then add current
|
|
80
|
+
for (var i = 0; i < themes.length; i++) {
|
|
81
|
+
var v = valueMap[themes[i]] || themes[i];
|
|
82
|
+
el.classList.remove(v);
|
|
83
|
+
}
|
|
84
|
+
el.classList.add(value);
|
|
85
|
+
} else {
|
|
86
|
+
el.setAttribute(attribute, value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Set color-scheme for native dark mode support
|
|
90
|
+
if (enableColorScheme) {
|
|
91
|
+
el.style.colorScheme = resolved;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Get stored theme or use default
|
|
96
|
+
var stored = getStoredTheme();
|
|
97
|
+
var theme = stored && (stored === 'system' || themes.indexOf(stored) !== -1)
|
|
98
|
+
? stored
|
|
99
|
+
: defaultTheme;
|
|
100
|
+
|
|
101
|
+
// Apply immediately
|
|
102
|
+
applyTheme(theme);
|
|
103
|
+
|
|
104
|
+
// Listen for system preference changes (for "system" theme)
|
|
105
|
+
if (enableSystem && typeof window !== 'undefined' && window.matchMedia) {
|
|
106
|
+
try {
|
|
107
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
|
|
108
|
+
var current = getStoredTheme() || defaultTheme;
|
|
109
|
+
if (current === 'system') {
|
|
110
|
+
applyTheme('system');
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
} catch (e) {
|
|
114
|
+
// Older browsers may not support addEventListener on MediaQueryList
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
// Minify by removing comments, extra whitespace, and newlines
|
|
121
|
+
return minifyScript(script);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Basic script minification
|
|
126
|
+
* Removes comments, extra whitespace, and unnecessary newlines
|
|
127
|
+
*/
|
|
128
|
+
function minifyScript(script: string): string {
|
|
129
|
+
return (
|
|
130
|
+
script
|
|
131
|
+
// Remove single-line comments
|
|
132
|
+
.replace(/\/\/.*$/gm, "")
|
|
133
|
+
// Remove multi-line comments
|
|
134
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
135
|
+
// Remove leading/trailing whitespace from lines
|
|
136
|
+
.split("\n")
|
|
137
|
+
.map((line) => line.trim())
|
|
138
|
+
.filter((line) => line.length > 0)
|
|
139
|
+
.join("")
|
|
140
|
+
// Collapse multiple spaces to single space
|
|
141
|
+
.replace(/\s+/g, " ")
|
|
142
|
+
// Remove spaces around operators and punctuation
|
|
143
|
+
.replace(/\s*([{};,=!<>()[\]+\-*/&|?:])\s*/g, "$1")
|
|
144
|
+
// Add back necessary spaces (e.g., "var x")
|
|
145
|
+
.replace(/(var|function|return|if|for|try|catch|typeof|else)\(/g, "$1 (")
|
|
146
|
+
.replace(/\)([a-zA-Z])/g, ") $1")
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generate nonce attribute string if nonce is provided
|
|
152
|
+
*/
|
|
153
|
+
export function getNonceAttribute(nonce?: string): string {
|
|
154
|
+
return nonce ? ` nonce="${nonce}"` : "";
|
|
155
|
+
}
|