@lovrozagar/flare 0.1.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/LICENSE +21 -0
- package/README.md +11 -0
- package/package.json +146 -0
- package/scripts/download-fonts.ts +136 -0
- package/scripts/populate-fonts.ts +721 -0
- package/src/await.ts +2 -0
- package/src/boundaries/index.ts +99 -0
- package/src/broadcast/channel.ts +65 -0
- package/src/broadcast/context.ts +12 -0
- package/src/broadcast/index.ts +75 -0
- package/src/broadcast/provider.tsx +7 -0
- package/src/build-location.ts +2 -0
- package/src/caches/index.ts +384 -0
- package/src/client/index.ts +75 -0
- package/src/client.tsx +2 -0
- package/src/codegen/index.ts +41 -0
- package/src/components/await.tsx +140 -0
- package/src/components/dev-error-overlay.tsx +154 -0
- package/src/components/direction-script.tsx +22 -0
- package/src/components/index.ts +150 -0
- package/src/components/locale-script.tsx +26 -0
- package/src/components/reset-css.tsx +14 -0
- package/src/components/ssr-context.tsx +37 -0
- package/src/components/theme-script.tsx +22 -0
- package/src/components/view-transition-css.tsx +21 -0
- package/src/config/index.ts +84 -0
- package/src/create-client-only-fn.ts +1 -0
- package/src/create-isomorphic-fn.ts +2 -0
- package/src/create-layout.ts +1 -0
- package/src/create-page.ts +1 -0
- package/src/create-path-segment.ts +1 -0
- package/src/create-query-client-getter.ts +2 -0
- package/src/create-root-layout.ts +1 -0
- package/src/create-router.ts +8 -0
- package/src/create-server-only-fn.ts +1 -0
- package/src/dedupe/index.ts +151 -0
- package/src/defer/index.ts +82 -0
- package/src/direction/index.tsx +163 -0
- package/src/direction.ts +3 -0
- package/src/duration/index.ts +18 -0
- package/src/env-fn/index.ts +101 -0
- package/src/env-fn.ts +2 -0
- package/src/errors/index.ts +166 -0
- package/src/fonts/abeezee.ts +50 -0
- package/src/fonts/abel.ts +26 -0
- package/src/fonts/abril-fatface.ts +34 -0
- package/src/fonts/acme.ts +26 -0
- package/src/fonts/advent-pro.ts +92 -0
- package/src/fonts/alegreya-sans.ts +228 -0
- package/src/fonts/alegreya.ts +228 -0
- package/src/fonts/alex-brush.ts +42 -0
- package/src/fonts/alfa-slab-one.ts +42 -0
- package/src/fonts/alice.ts +48 -0
- package/src/fonts/amatic-sc.ts +94 -0
- package/src/fonts/amiri.ts +114 -0
- package/src/fonts/antic-slab.ts +26 -0
- package/src/fonts/anton.ts +42 -0
- package/src/fonts/archivo-black.ts +34 -0
- package/src/fonts/archivo-narrow.ts +114 -0
- package/src/fonts/archivo.ts +66 -0
- package/src/fonts/arimo.ts +256 -0
- package/src/fonts/arvo.ts +50 -0
- package/src/fonts/asap.ts +66 -0
- package/src/fonts/assistant.ts +64 -0
- package/src/fonts/baloo-2.ts +82 -0
- package/src/fonts/barlow-condensed.ts +114 -0
- package/src/fonts/barlow-semi-condensed.ts +114 -0
- package/src/fonts/barlow.ts +114 -0
- package/src/fonts/be-vietnam-pro.ts +114 -0
- package/src/fonts/bebas-neue.ts +34 -0
- package/src/fonts/besley.ts +82 -0
- package/src/fonts/biorhyme.ts +50 -0
- package/src/fonts/bitter.ts +94 -0
- package/src/fonts/black-ops-one.ts +49 -0
- package/src/fonts/bree-serif.ts +34 -0
- package/src/fonts/bricolage-grotesque.ts +66 -0
- package/src/fonts/cabin.ts +114 -0
- package/src/fonts/cairo.ts +66 -0
- package/src/fonts/cardo.ts +193 -0
- package/src/fonts/catamaran.ts +41 -0
- package/src/fonts/caveat.ts +78 -0
- package/src/fonts/chakra-petch.ts +142 -0
- package/src/fonts/chivo.ts +66 -0
- package/src/fonts/cinzel.ts +50 -0
- package/src/fonts/comfortaa.ts +109 -0
- package/src/fonts/commissioner.ts +64 -0
- package/src/fonts/component.tsx +27 -0
- package/src/fonts/cormorant-garamond.ts +171 -0
- package/src/fonts/cormorant.ts +170 -0
- package/src/fonts/courgette.ts +34 -0
- package/src/fonts/courier-prime.ts +82 -0
- package/src/fonts/create-font.ts +47 -0
- package/src/fonts/create-registry-font.ts +40 -0
- package/src/fonts/crete-round.ts +50 -0
- package/src/fonts/crimson-text.ts +114 -0
- package/src/fonts/css.ts +86 -0
- package/src/fonts/dancing-script.ts +66 -0
- package/src/fonts/dm-mono.ts +50 -0
- package/src/fonts/dm-sans.ts +50 -0
- package/src/fonts/dm-serif-display.ts +50 -0
- package/src/fonts/domine.ts +50 -0
- package/src/fonts/dosis.ts +66 -0
- package/src/fonts/eb-garamond.ts +228 -0
- package/src/fonts/eczar.ts +94 -0
- package/src/fonts/encode-sans.ts +42 -0
- package/src/fonts/epilogue.ts +66 -0
- package/src/fonts/exo-2.ts +94 -0
- package/src/fonts/exo.ts +66 -0
- package/src/fonts/figtree.ts +82 -0
- package/src/fonts/fira-code.ts +121 -0
- package/src/fonts/fira-sans-condensed.ts +228 -0
- package/src/fonts/fira-sans.ts +228 -0
- package/src/fonts/fjalla-one.ts +49 -0
- package/src/fonts/font-urls.gen.json +1962 -0
- package/src/fonts/fraunces.ts +66 -0
- package/src/fonts/fredoka.ts +64 -0
- package/src/fonts/gelasio.ts +114 -0
- package/src/fonts/gloria-hallelujah.ts +34 -0
- package/src/fonts/gothic-a1.ts +124 -0
- package/src/fonts/great-vibes.ts +64 -0
- package/src/fonts/gudea.ts +66 -0
- package/src/fonts/heebo.ts +57 -0
- package/src/fonts/hind-siliguri.ts +66 -0
- package/src/fonts/hind.ts +66 -0
- package/src/fonts/ibm-plex-mono.ts +172 -0
- package/src/fonts/ibm-plex-sans-arabic.ts +80 -0
- package/src/fonts/ibm-plex-sans.ts +199 -0
- package/src/fonts/ibm-plex-serif.ts +171 -0
- package/src/fonts/inconsolata.ts +66 -0
- package/src/fonts/index.ts +4 -0
- package/src/fonts/indie-flower.ts +34 -0
- package/src/fonts/instrument-sans.ts +82 -0
- package/src/fonts/instrument-serif.ts +50 -0
- package/src/fonts/inter-display.ts +52 -0
- package/src/fonts/inter-tight.ts +124 -0
- package/src/fonts/inter.ts +123 -0
- package/src/fonts/jetbrains-mono.ts +199 -0
- package/src/fonts/josefin-sans.ts +114 -0
- package/src/fonts/josefin-slab.ts +50 -0
- package/src/fonts/jost.ts +64 -0
- package/src/fonts/kalam.ts +66 -0
- package/src/fonts/kanit.ts +142 -0
- package/src/fonts/karla.ts +82 -0
- package/src/fonts/khand.ts +66 -0
- package/src/fonts/lato.ts +82 -0
- package/src/fonts/league-spartan.ts +42 -0
- package/src/fonts/lexend-deca.ts +42 -0
- package/src/fonts/lexend.ts +42 -0
- package/src/fonts/libre-baskerville.ts +82 -0
- package/src/fonts/libre-franklin.ts +95 -0
- package/src/fonts/lilita-one.ts +34 -0
- package/src/fonts/lobster-two.ts +50 -0
- package/src/fonts/lobster.ts +56 -0
- package/src/fonts/lora.ts +235 -0
- package/src/fonts/m-plus-rounded-1c.ts +138 -0
- package/src/fonts/manrope.ts +109 -0
- package/src/fonts/maven-pro.ts +66 -0
- package/src/fonts/merriweather-sans.ts +142 -0
- package/src/fonts/merriweather.ts +171 -0
- package/src/fonts/montserrat-alternates.ts +171 -0
- package/src/fonts/montserrat.ts +94 -0
- package/src/fonts/mukta.ts +66 -0
- package/src/fonts/mulish.ts +170 -0
- package/src/fonts/nanum-gothic.ts +34 -0
- package/src/fonts/nanum-myeongjo.ts +34 -0
- package/src/fonts/neuton.ts +66 -0
- package/src/fonts/newsreader.ts +114 -0
- package/src/fonts/noticia-text.ts +114 -0
- package/src/fonts/noto-sans-arabic.ts +58 -0
- package/src/fonts/noto-sans-hk.ts +49 -0
- package/src/fonts/noto-sans-jp.ts +49 -0
- package/src/fonts/noto-sans-kr.ts +49 -0
- package/src/fonts/noto-sans-sc.ts +49 -0
- package/src/fonts/noto-sans-tc.ts +49 -0
- package/src/fonts/noto-sans.ts +140 -0
- package/src/fonts/noto-serif-jp.ts +80 -0
- package/src/fonts/noto-serif.ts +140 -0
- package/src/fonts/nunito-sans.ts +170 -0
- package/src/fonts/nunito.ts +170 -0
- package/src/fonts/old-standard-tt.ts +133 -0
- package/src/fonts/onest.ts +48 -0
- package/src/fonts/open-sans.ts +340 -0
- package/src/fonts/orbitron.ts +34 -0
- package/src/fonts/oswald.ts +94 -0
- package/src/fonts/outfit.ts +34 -0
- package/src/fonts/overpass.ts +94 -0
- package/src/fonts/oxygen.ts +50 -0
- package/src/fonts/pacifico.ts +56 -0
- package/src/fonts/passion-one.ts +50 -0
- package/src/fonts/pathway-gothic-one.ts +34 -0
- package/src/fonts/permanent-marker.ts +26 -0
- package/src/fonts/philosopher.ts +172 -0
- package/src/fonts/play.ts +109 -0
- package/src/fonts/playfair-display.ts +142 -0
- package/src/fonts/plus-jakarta-sans.ts +142 -0
- package/src/fonts/poppins.ts +114 -0
- package/src/fonts/prompt.ts +142 -0
- package/src/fonts/pt-sans-narrow.ts +78 -0
- package/src/fonts/pt-sans.ts +138 -0
- package/src/fonts/pt-serif.ts +138 -0
- package/src/fonts/public-sans.ts +66 -0
- package/src/fonts/questrial.ts +42 -0
- package/src/fonts/quicksand.ts +66 -0
- package/src/fonts/rajdhani.ts +66 -0
- package/src/fonts/raleway.ts +94 -0
- package/src/fonts/recursive.ts +80 -0
- package/src/fonts/red-hat-display.ts +82 -0
- package/src/fonts/red-rose.ts +66 -0
- package/src/fonts/registry.gen.ts +173 -0
- package/src/fonts/righteous.ts +34 -0
- package/src/fonts/roboto-condensed.ts +124 -0
- package/src/fonts/roboto-flex.ts +64 -0
- package/src/fonts/roboto-mono.ts +199 -0
- package/src/fonts/roboto-slab.ts +72 -0
- package/src/fonts/roboto.ts +156 -0
- package/src/fonts/rokkitt.ts +66 -0
- package/src/fonts/rubik.ts +199 -0
- package/src/fonts/sacramento.ts +34 -0
- package/src/fonts/saira.ts +66 -0
- package/src/fonts/sarabun.ts +142 -0
- package/src/fonts/satisfy.ts +26 -0
- package/src/fonts/sawarabi-mincho.ts +41 -0
- package/src/fonts/shadows-into-light.ts +34 -0
- package/src/fonts/signika-negative.ts +66 -0
- package/src/fonts/signika.ts +66 -0
- package/src/fonts/slabo-27px.ts +34 -0
- package/src/fonts/sora.ts +50 -0
- package/src/fonts/source-code-pro.ts +228 -0
- package/src/fonts/source-sans-3.ts +228 -0
- package/src/fonts/source-serif-4.ts +199 -0
- package/src/fonts/space-grotesk.ts +66 -0
- package/src/fonts/space-mono.ts +114 -0
- package/src/fonts/spectral.ts +170 -0
- package/src/fonts/tajawal.ts +50 -0
- package/src/fonts/teko.ts +66 -0
- package/src/fonts/tenor-sans.ts +41 -0
- package/src/fonts/tinos.ts +256 -0
- package/src/fonts/titillium-web.ts +82 -0
- package/src/fonts/types.ts +43 -0
- package/src/fonts/ubuntu-condensed.ts +63 -0
- package/src/fonts/ubuntu-mono.ts +195 -0
- package/src/fonts/ubuntu.ts +195 -0
- package/src/fonts/unbounded.ts +94 -0
- package/src/fonts/unicode-ranges.gen.ts +28 -0
- package/src/fonts/urbanist.ts +50 -0
- package/src/fonts/varela-round.ts +49 -0
- package/src/fonts/vollkorn.ts +199 -0
- package/src/fonts/work-sans.ts +66 -0
- package/src/fonts/yanone-kaffeesatz.ts +128 -0
- package/src/fonts/yantramanav.ts +66 -0
- package/src/fonts/yellowtail.ts +34 -0
- package/src/fonts/young-serif.ts +34 -0
- package/src/fonts/zen-kaku-gothic-new.ts +64 -0
- package/src/fonts/zilla-slab.ts +82 -0
- package/src/form/index.tsx +205 -0
- package/src/generators/index.ts +1589 -0
- package/src/head-client/index.ts +649 -0
- package/src/history/index.ts +210 -0
- package/src/hooks.ts +61 -0
- package/src/hydrate/index.tsx +381 -0
- package/src/hydration/index.ts +187 -0
- package/src/i18n/index.ts +250 -0
- package/src/i18n/jsx.tsx +42 -0
- package/src/i18n/message-format.ts +174 -0
- package/src/image/index.tsx +473 -0
- package/src/image.ts +12 -0
- package/src/intercept-outlet/index.tsx +18 -0
- package/src/intercept-outlet.ts +2 -0
- package/src/internal/index.ts +143 -0
- package/src/internal/once-idle.ts +24 -0
- package/src/internal.ts +12 -0
- package/src/lazy/index.tsx +205 -0
- package/src/link/index.tsx +367 -0
- package/src/loader-pipeline/index.ts +639 -0
- package/src/locale/index.tsx +137 -0
- package/src/locale.ts +3 -0
- package/src/logger.ts +22 -0
- package/src/middleware/builtins/api-proxy.ts +64 -0
- package/src/middleware/builtins/cdn-proxy.ts +58 -0
- package/src/middleware/builtins/i18n.ts +256 -0
- package/src/middleware/builtins/keepalive.ts +24 -0
- package/src/middleware/builtins/markdown-negotiation.ts +141 -0
- package/src/middleware/builtins/static-assets.ts +32 -0
- package/src/middleware/index.ts +223 -0
- package/src/module-graph/index.ts +181 -0
- package/src/mount/index.ts +146 -0
- package/src/navigate.ts +1 -0
- package/src/navigation/index.ts +1278 -0
- package/src/navigation/types.ts +59 -0
- package/src/navigation-progress/index.tsx +247 -0
- package/src/navigation-progress.ts +1 -0
- package/src/ndjson-client/index.ts +376 -0
- package/src/ndjson-server/index.ts +284 -0
- package/src/outlet/index.tsx +633 -0
- package/src/outlet/types.ts +243 -0
- package/src/plugins/css-transform.ts +70 -0
- package/src/plugins/dev-cdn-cache.ts +454 -0
- package/src/plugins/dev-dashboard/client.tsx +2603 -0
- package/src/plugins/dev-dashboard/match-route-tree.ts +159 -0
- package/src/plugins/dev-dashboard/plugin.ts +404 -0
- package/src/plugins/dev-prerender.ts +168 -0
- package/src/plugins/dev-server.ts +327 -0
- package/src/plugins/generate-watch.ts +32 -0
- package/src/plugins/image-plugin.ts +171 -0
- package/src/plugins/index.ts +445 -0
- package/src/plugins/options.ts +37 -0
- package/src/plugins/parse-utils.ts +59 -0
- package/src/plugins/prerender-plugin.ts +144 -0
- package/src/plugins/purge.ts +242 -0
- package/src/plugins/server-fn.ts +641 -0
- package/src/plugins/service-worker.ts +225 -0
- package/src/plugins/solid-jsx.d.ts +12 -0
- package/src/plugins/sx-ast/atomic.ts +166 -0
- package/src/plugins/sx-ast/evaluator.ts +235 -0
- package/src/plugins/sx-ast/index.ts +372 -0
- package/src/plugins/sx-ast/parser.ts +22 -0
- package/src/plugins/sx-ast/rewrite.ts +763 -0
- package/src/plugins/sx.d.ts +14 -0
- package/src/plugins/tw-compile.ts +240 -0
- package/src/plugins/types.ts +41 -0
- package/src/plugins/virtual.d.ts +110 -0
- package/src/plugins/virtual.ts +184 -0
- package/src/preload/index.ts +77 -0
- package/src/prerender/index.ts +431 -0
- package/src/query-client/index.tsx +168 -0
- package/src/registry/index.ts +88 -0
- package/src/reset-css.ts +1 -0
- package/src/revalidation/index.ts +84 -0
- package/src/rewrite/index.ts +83 -0
- package/src/route-builder/create-layout.ts +655 -0
- package/src/route-builder/create-page.ts +806 -0
- package/src/route-builder/create-path-segment.ts +34 -0
- package/src/route-builder/create-root-layout.ts +608 -0
- package/src/route-builder/index.ts +49 -0
- package/src/route-builder/register.ts +242 -0
- package/src/route-builder/types.ts +294 -0
- package/src/router-config/index.ts +91 -0
- package/src/router-primitives/index.ts +25 -0
- package/src/router-primitives/location.ts +29 -0
- package/src/router-primitives/match-id.ts +111 -0
- package/src/router-primitives/paths.ts +109 -0
- package/src/router-primitives/tree.ts +243 -0
- package/src/router-primitives/types.ts +74 -0
- package/src/router.ts +31 -0
- package/src/search-engine/bing.ts +32 -0
- package/src/search-engine/google-jwt.ts +95 -0
- package/src/search-engine/google.ts +139 -0
- package/src/search-engine/index-now.ts +64 -0
- package/src/search-engine/index.ts +12 -0
- package/src/security/index.ts +230 -0
- package/src/server/index.ts +286 -0
- package/src/server-context/browser.ts +113 -0
- package/src/server-context/index.ts +217 -0
- package/src/server-fn/index.ts +710 -0
- package/src/server-fn-query.ts +169 -0
- package/src/server-handler/etag.ts +51 -0
- package/src/server-handler/headers.ts +5 -0
- package/src/server-handler/index.ts +1664 -0
- package/src/server-handler/types.ts +24 -0
- package/src/server-handler/validate-static-params.ts +130 -0
- package/src/server.ts +2 -0
- package/src/service-worker/template.ts +153 -0
- package/src/service-worker-hook/index.ts +70 -0
- package/src/sitemap/index.ts +231 -0
- package/src/ssr/critical-css.ts +150 -0
- package/src/ssr/head-prefix.ts +65 -0
- package/src/ssr/head.ts +259 -0
- package/src/ssr/index.tsx +879 -0
- package/src/state-parser/index.ts +266 -0
- package/src/store/filesystem.ts +131 -0
- package/src/store/index.ts +29 -0
- package/src/styles/index.ts +1200 -0
- package/src/styles/sx-types.ts +11 -0
- package/src/testing/index.ts +251 -0
- package/src/testing.ts +2 -0
- package/src/theme/index.tsx +232 -0
- package/src/theme.ts +3 -0
- package/src/tracing/index.ts +4 -0
- package/src/tracing/noop.ts +13 -0
- package/src/tracing/otel.ts +37 -0
- package/src/tracing/timing.ts +34 -0
- package/src/tracing/types.ts +9 -0
- package/src/url/index.ts +140 -0
- package/src/use-router.ts +11 -0
- package/src/use-suspense-query.ts +2 -0
- package/src/validation/index.ts +57 -0
- package/src/view-transition-css.ts +1 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ComputeMatchIdOptions, ParsedMatchId } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
function sortedParams(params: Record<string, string | string[]>): Record<string, string | string[]> {
|
|
4
|
+
const sorted: Record<string, string | string[]> = {};
|
|
5
|
+
for (const key of Object.keys(params).sort()) {
|
|
6
|
+
sorted[key] = params[key];
|
|
7
|
+
}
|
|
8
|
+
return sorted;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* JSON.stringify with sorted keys at every depth.
|
|
13
|
+
* Ensures objects with identical content but different insertion order
|
|
14
|
+
* produce the same string (avoids matchId cache misses).
|
|
15
|
+
* Uses WeakSet to detect circular references — emits null for cycles.
|
|
16
|
+
*/
|
|
17
|
+
function stableStringify(value: unknown, seen?: WeakSet<object>): string {
|
|
18
|
+
if (value === null || value === undefined) return JSON.stringify(value);
|
|
19
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
20
|
+
const s = seen ?? new WeakSet();
|
|
21
|
+
if (s.has(value)) return "null";
|
|
22
|
+
s.add(value);
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
let arr = "[";
|
|
25
|
+
for (let i = 0; i < value.length; i++) {
|
|
26
|
+
if (i > 0) arr += ",";
|
|
27
|
+
arr += stableStringify(value[i], s);
|
|
28
|
+
}
|
|
29
|
+
return `${arr}]`;
|
|
30
|
+
}
|
|
31
|
+
const obj = value as Record<string, unknown>;
|
|
32
|
+
const keys = Object.keys(obj).sort();
|
|
33
|
+
let result = "{";
|
|
34
|
+
for (let i = 0; i < keys.length; i++) {
|
|
35
|
+
if (i > 0) result += ",";
|
|
36
|
+
result += `${JSON.stringify(keys[i])}:${stableStringify(obj[keys[i]], s)}`;
|
|
37
|
+
}
|
|
38
|
+
return `${result}}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function computeMatchId(options: ComputeMatchIdOptions): string {
|
|
42
|
+
const params = sortedParams(options.params);
|
|
43
|
+
const deps = options.loaderDeps ? options.loaderDeps({ search: options.search }) : [];
|
|
44
|
+
return `${options.routeId}:${JSON.stringify(params)}:${stableStringify(deps)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find matching closing brace in a JSON-like string, respecting
|
|
49
|
+
* string literals and nested braces/brackets.
|
|
50
|
+
*/
|
|
51
|
+
function findClosingBrace(str: string, openPos: number): number {
|
|
52
|
+
const openChar = str[openPos];
|
|
53
|
+
const closeChar = openChar === "{" ? "}" : "]";
|
|
54
|
+
let depth = 0;
|
|
55
|
+
let inString = false;
|
|
56
|
+
let escape = false;
|
|
57
|
+
|
|
58
|
+
for (let i = openPos; i < str.length; i++) {
|
|
59
|
+
const ch = str[i];
|
|
60
|
+
if (escape) {
|
|
61
|
+
escape = false;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (inString) {
|
|
65
|
+
if (ch === "\\") {
|
|
66
|
+
escape = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (ch === '"') {
|
|
70
|
+
inString = false;
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === '"') {
|
|
75
|
+
inString = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === openChar) depth++;
|
|
79
|
+
if (ch === closeChar) {
|
|
80
|
+
depth--;
|
|
81
|
+
if (depth === 0) return i;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return -1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parseMatchId(matchId: string): ParsedMatchId | null {
|
|
88
|
+
/* Format: routeId:{params}:[deps] — use brace-depth scanning to handle ":[" in values */
|
|
89
|
+
const firstBrace = matchId.indexOf(":{");
|
|
90
|
+
if (firstBrace === -1) return null;
|
|
91
|
+
|
|
92
|
+
const routeId = matchId.slice(0, firstBrace);
|
|
93
|
+
const paramsStart = firstBrace + 1;
|
|
94
|
+
const paramsEnd = findClosingBrace(matchId, paramsStart);
|
|
95
|
+
if (paramsEnd === -1) return null;
|
|
96
|
+
|
|
97
|
+
const paramsStr = matchId.slice(paramsStart, paramsEnd + 1);
|
|
98
|
+
|
|
99
|
+
/* Next char must be ":" separator, followed by "[" for deps array */
|
|
100
|
+
if (matchId[paramsEnd + 1] !== ":" || matchId[paramsEnd + 2] !== "[") return null;
|
|
101
|
+
const depsStr = matchId.slice(paramsEnd + 2);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const rawParams: unknown = JSON.parse(paramsStr);
|
|
105
|
+
const rawDeps: unknown = JSON.parse(depsStr);
|
|
106
|
+
if (typeof rawParams !== "object" || rawParams === null || !Array.isArray(rawDeps)) return null;
|
|
107
|
+
return { deps: rawDeps, params: rawParams as Record<string, string | string[]>, routeId };
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function isGroupSegment(segment: string): boolean {
|
|
2
|
+
return segment.startsWith("(") && segment.endsWith(")");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isParamSegment(segment: string): boolean {
|
|
6
|
+
return segment.startsWith("[") && segment.endsWith("]");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isRootLayoutPath(path: string): boolean {
|
|
10
|
+
return path.length >= 3 && path.startsWith("_") && path.endsWith("_") && !path.includes("/");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function stripGroups(path: string): string {
|
|
14
|
+
return path
|
|
15
|
+
.split("/")
|
|
16
|
+
.filter((s) => !isGroupSegment(s))
|
|
17
|
+
.join("/");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findRootSegmentIndex(segments: string[]): number {
|
|
21
|
+
return segments.findIndex((s) => isRootLayoutPath(s));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function toUrlPath(virtualPath: string): string {
|
|
25
|
+
const segments = virtualPath.split("/");
|
|
26
|
+
const rootIdx = findRootSegmentIndex(segments);
|
|
27
|
+
const filtered = segments.filter((s, i) => {
|
|
28
|
+
if (rootIdx >= 0 && i <= rootIdx) return false;
|
|
29
|
+
if (isGroupSegment(s)) return false;
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
if (filtered.length === 0) return "/";
|
|
33
|
+
return `/${filtered.join("/")}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function toVirtualPath(urlPath: string, root: string): string {
|
|
37
|
+
const trimmed = urlPath.replace(/^\/+/, "");
|
|
38
|
+
if (trimmed.length === 0) return root;
|
|
39
|
+
return `${root}/${trimmed}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PARAM_RE = /\[(?:\.\.\.)?(\w+)\](?:\])?/g;
|
|
43
|
+
|
|
44
|
+
export function deriveParams(variablePath: string): string[] {
|
|
45
|
+
const matches = variablePath.matchAll(PARAM_RE);
|
|
46
|
+
const result: string[] = [];
|
|
47
|
+
for (const m of matches) {
|
|
48
|
+
result.push(m[1]);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function extractLayoutKey(virtualPath: string): string {
|
|
54
|
+
const segments = virtualPath.split("/");
|
|
55
|
+
const virtual: string[] = [];
|
|
56
|
+
for (const segment of segments) {
|
|
57
|
+
if (isRootLayoutPath(segment) || isGroupSegment(segment) || isParamSegment(segment)) {
|
|
58
|
+
virtual.push(segment);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return virtual.join("/");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function deriveLayouts(virtualPath: string): string[] {
|
|
65
|
+
const layoutKey = extractLayoutKey(virtualPath);
|
|
66
|
+
if (!layoutKey) return [];
|
|
67
|
+
const segments = layoutKey.split("/");
|
|
68
|
+
const layouts: string[] = [];
|
|
69
|
+
let accumulated = "";
|
|
70
|
+
let foundRoot = false;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < segments.length; i++) {
|
|
73
|
+
const segment = segments[i];
|
|
74
|
+
|
|
75
|
+
if (!foundRoot) {
|
|
76
|
+
/* Pre-root: accumulate param segments into compound key */
|
|
77
|
+
if (isRootLayoutPath(segment)) {
|
|
78
|
+
accumulated = accumulated ? `${accumulated}/${segment}` : segment;
|
|
79
|
+
layouts.push(accumulated);
|
|
80
|
+
foundRoot = true;
|
|
81
|
+
} else if (isParamSegment(segment)) {
|
|
82
|
+
accumulated = accumulated ? `${accumulated}/${segment}` : segment;
|
|
83
|
+
/* Push pre-root param segments — they can be path segments with cache config */
|
|
84
|
+
layouts.push(accumulated);
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Post-root: existing logic */
|
|
90
|
+
if (isGroupSegment(segment)) {
|
|
91
|
+
accumulated = `${accumulated}/${segment}`;
|
|
92
|
+
layouts.push(accumulated);
|
|
93
|
+
/* Each trailing param segment forms its own layout boundary.
|
|
94
|
+
Enables path-segments like (group)/[org]/[repo] to resolve independently. */
|
|
95
|
+
while (i + 1 < segments.length && isParamSegment(segments[i + 1])) {
|
|
96
|
+
i++;
|
|
97
|
+
accumulated = `${accumulated}/${segments[i]}`;
|
|
98
|
+
layouts.push(accumulated);
|
|
99
|
+
}
|
|
100
|
+
} else if (isParamSegment(segment)) {
|
|
101
|
+
/* Bare param segments (not trailing a group) also form layout boundaries.
|
|
102
|
+
Enables [locale]/_layout_.tsx without requiring a wrapping (group). */
|
|
103
|
+
accumulated = `${accumulated}/${segment}`;
|
|
104
|
+
layouts.push(accumulated);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return layouts;
|
|
109
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { MatchResult, RouteData, TreeNode } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/** Constrains which decoded segment values are accepted when consuming an optional param. */
|
|
4
|
+
export interface LocaleMatch {
|
|
5
|
+
paramName: string;
|
|
6
|
+
locales: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Builds a `LocaleMatch` from a `LocaleConfig`-shaped object. Absent cfg → greedy fallback. */
|
|
10
|
+
export function toLocaleMatch(
|
|
11
|
+
cfg: { locales: readonly string[]; paramName?: string } | undefined,
|
|
12
|
+
): LocaleMatch | undefined {
|
|
13
|
+
if (!cfg) return undefined;
|
|
14
|
+
return { locales: cfg.locales, paramName: cfg.paramName ?? "locale" };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function safeDecodeURIComponent(s: string): string | null {
|
|
18
|
+
try {
|
|
19
|
+
return decodeURIComponent(s);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** True when `decoded` may be stored as `paramName` under the current locale allow-list. */
|
|
26
|
+
function localeAllowsConsume(paramName: string, decoded: string, localeMatch?: LocaleMatch): boolean {
|
|
27
|
+
return !localeMatch || paramName !== localeMatch.paramName || localeMatch.locales.includes(decoded);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createTreeNode(paramName?: string): TreeNode {
|
|
31
|
+
const node: TreeNode = { s: Object.create(null) };
|
|
32
|
+
if (paramName !== undefined) {
|
|
33
|
+
node.n = paramName;
|
|
34
|
+
}
|
|
35
|
+
return node;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function splitSegments(path: string): string[] {
|
|
39
|
+
return path.split("/").filter((s) => s.length > 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function insertRoute(tree: TreeNode, path: string, route: RouteData): void {
|
|
43
|
+
const segments = splitSegments(path);
|
|
44
|
+
let node = tree;
|
|
45
|
+
|
|
46
|
+
for (const segment of segments) {
|
|
47
|
+
const optionalCatchAll = segment.match(/^\[\[\.\.\.(\w+)\]\]$/);
|
|
48
|
+
if (optionalCatchAll) {
|
|
49
|
+
if (!node.o) {
|
|
50
|
+
node.o = createTreeNode(optionalCatchAll[1]);
|
|
51
|
+
}
|
|
52
|
+
node = node.o;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const optionalParam = segment.match(/^\[\[(\w+)\]\]$/);
|
|
57
|
+
if (optionalParam) {
|
|
58
|
+
if (!node.q) {
|
|
59
|
+
node.q = createTreeNode(optionalParam[1]);
|
|
60
|
+
}
|
|
61
|
+
node = node.q;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const catchAll = segment.match(/^\[\.\.\.(\w+)\]$/);
|
|
66
|
+
if (catchAll) {
|
|
67
|
+
if (!node.c) {
|
|
68
|
+
node.c = createTreeNode(catchAll[1]);
|
|
69
|
+
}
|
|
70
|
+
node = node.c;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const param = segment.match(/^\[(\w+)\]$/);
|
|
75
|
+
if (param) {
|
|
76
|
+
if (!node.p) {
|
|
77
|
+
node.p = createTreeNode(param[1]);
|
|
78
|
+
}
|
|
79
|
+
node = node.p;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Store lowercase key for case-insensitive lookup (default).
|
|
84
|
+
* Generated patterns from filesystem are already lowercase by convention. */
|
|
85
|
+
const key = segment.toLowerCase();
|
|
86
|
+
let child = node.s[key];
|
|
87
|
+
if (!child) {
|
|
88
|
+
child = createTreeNode();
|
|
89
|
+
node.s[key] = child;
|
|
90
|
+
}
|
|
91
|
+
node = child;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
node.r = route;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function matchRoute(
|
|
98
|
+
tree: TreeNode,
|
|
99
|
+
pathname: string,
|
|
100
|
+
caseSensitive?: boolean,
|
|
101
|
+
localeMatch?: LocaleMatch,
|
|
102
|
+
): MatchResult | null {
|
|
103
|
+
const segments = splitSegments(pathname);
|
|
104
|
+
const params: Record<string, string | string[]> = {};
|
|
105
|
+
const result = matchNode(tree, segments, 0, params, caseSensitive, localeMatch);
|
|
106
|
+
if (result) {
|
|
107
|
+
return { params, route: result };
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function matchRoutePartial(
|
|
113
|
+
tree: TreeNode,
|
|
114
|
+
pathname: string,
|
|
115
|
+
caseSensitive?: boolean,
|
|
116
|
+
localeMatch?: LocaleMatch,
|
|
117
|
+
): MatchResult | null {
|
|
118
|
+
const segments = splitSegments(pathname);
|
|
119
|
+
for (let len = segments.length - 1; len >= 1; len--) {
|
|
120
|
+
const prefix = `/${segments.slice(0, len).join("/")}`;
|
|
121
|
+
const result = matchRoute(tree, prefix, caseSensitive, localeMatch);
|
|
122
|
+
if (result) return result;
|
|
123
|
+
}
|
|
124
|
+
/* Skip "/" fallback when localeMatch is set and the first segment is neither a declared
|
|
125
|
+
* locale nor a static child — returning root for a cross-worker path like /docs/x/y
|
|
126
|
+
* would falsely imply ownership and hijack the navigation. */
|
|
127
|
+
if (localeMatch && segments.length > 0) {
|
|
128
|
+
const first = safeDecodeURIComponent(segments[0]);
|
|
129
|
+
const isValidLocale = first !== null && localeMatch.locales.includes(first);
|
|
130
|
+
const staticKey = caseSensitive ? segments[0] : segments[0].toLowerCase();
|
|
131
|
+
const hasStaticChild = staticKey in tree.s;
|
|
132
|
+
if (!isValidLocale && !hasStaticChild) return null;
|
|
133
|
+
}
|
|
134
|
+
return matchRoute(tree, "/", caseSensitive, localeMatch);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function matchNode(
|
|
138
|
+
node: TreeNode,
|
|
139
|
+
segments: string[],
|
|
140
|
+
index: number,
|
|
141
|
+
params: Record<string, string | string[]>,
|
|
142
|
+
caseSensitive?: boolean,
|
|
143
|
+
localeMatch?: LocaleMatch,
|
|
144
|
+
): RouteData | null {
|
|
145
|
+
if (index === segments.length) {
|
|
146
|
+
if (node.r) {
|
|
147
|
+
return node.r;
|
|
148
|
+
}
|
|
149
|
+
/* optional single param skipped — try matching descendants directly */
|
|
150
|
+
if (node.q) {
|
|
151
|
+
const result = matchNode(node.q, segments, index, params, caseSensitive, localeMatch);
|
|
152
|
+
if (result) return result;
|
|
153
|
+
}
|
|
154
|
+
/* optional catch-all at terminal */
|
|
155
|
+
if (node.o?.r && node.o.n) {
|
|
156
|
+
params[node.o.n] = [];
|
|
157
|
+
return node.o.r;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const segment = segments[index];
|
|
163
|
+
|
|
164
|
+
/* static — s is always null-prototype (createTreeNode / generated S helper) */
|
|
165
|
+
const key = caseSensitive ? segment : segment.toLowerCase();
|
|
166
|
+
const staticChild = node.s[key];
|
|
167
|
+
if (staticChild) {
|
|
168
|
+
const result = matchNode(staticChild, segments, index + 1, params, caseSensitive, localeMatch);
|
|
169
|
+
if (result) return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* param */
|
|
173
|
+
if (node.p) {
|
|
174
|
+
const paramName = node.p.n;
|
|
175
|
+
if (paramName) {
|
|
176
|
+
const decoded = safeDecodeURIComponent(segment);
|
|
177
|
+
if (decoded === null) return null;
|
|
178
|
+
/* locale allow-list: reject consume when segment is not a declared locale —
|
|
179
|
+
* required [locale] must not swallow unknown first segments as a locale index */
|
|
180
|
+
if (localeAllowsConsume(paramName, decoded, localeMatch)) {
|
|
181
|
+
const hadKey = paramName in params;
|
|
182
|
+
const prevVal = params[paramName];
|
|
183
|
+
params[paramName] = decoded;
|
|
184
|
+
const result = matchNode(node.p, segments, index + 1, params, caseSensitive, localeMatch);
|
|
185
|
+
if (result) return result;
|
|
186
|
+
/* backtrack — restore only the single key we changed */
|
|
187
|
+
if (hadKey) {
|
|
188
|
+
params[paramName] = prevVal;
|
|
189
|
+
} else {
|
|
190
|
+
delete params[paramName];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
const result = matchNode(node.p, segments, index + 1, params, caseSensitive, localeMatch);
|
|
195
|
+
if (result) return result;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* optional single param — try skipping first (static children win), then consuming */
|
|
200
|
+
if (node.q) {
|
|
201
|
+
/* skip — match descendants without consuming segment */
|
|
202
|
+
const skipResult = matchNode(node.q, segments, index, params, caseSensitive, localeMatch);
|
|
203
|
+
if (skipResult) return skipResult;
|
|
204
|
+
|
|
205
|
+
/* consume — use segment as param value */
|
|
206
|
+
const paramName = node.q.n;
|
|
207
|
+
if (paramName) {
|
|
208
|
+
const decoded = safeDecodeURIComponent(segment);
|
|
209
|
+
/* locale allow-list: reject consume when segment is not a declared locale —
|
|
210
|
+
* prevents /[[locale]] from greedily matching cross-worker path segments like /docs */
|
|
211
|
+
if (decoded !== null && localeAllowsConsume(paramName, decoded, localeMatch)) {
|
|
212
|
+
const hadKey = paramName in params;
|
|
213
|
+
const prevVal = params[paramName];
|
|
214
|
+
params[paramName] = decoded;
|
|
215
|
+
const result = matchNode(node.q, segments, index + 1, params, caseSensitive, localeMatch);
|
|
216
|
+
if (result) return result;
|
|
217
|
+
if (hadKey) {
|
|
218
|
+
params[paramName] = prevVal;
|
|
219
|
+
} else {
|
|
220
|
+
delete params[paramName];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* catch-all */
|
|
227
|
+
if (node.c?.r && node.c.n) {
|
|
228
|
+
const decoded = segments.slice(index).map(safeDecodeURIComponent);
|
|
229
|
+
if (decoded.some((d) => d === null)) return null;
|
|
230
|
+
params[node.c.n] = decoded as string[];
|
|
231
|
+
return node.c.r;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* optional catch-all */
|
|
235
|
+
if (node.o?.r && node.o.n) {
|
|
236
|
+
const decoded = segments.slice(index).map(safeDecodeURIComponent);
|
|
237
|
+
if (decoded.some((d) => d === null)) return null;
|
|
238
|
+
params[node.o.n] = decoded as string[];
|
|
239
|
+
return node.o.r;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { SearchParams } from "../url/index.ts";
|
|
2
|
+
|
|
3
|
+
export type PrefetchStrategy = false | "intent" | "render" | "viewport";
|
|
4
|
+
|
|
5
|
+
export type RouteMetaClient = {
|
|
6
|
+
cacheDeferred?: boolean;
|
|
7
|
+
gcTime?: number;
|
|
8
|
+
prefetch?: PrefetchStrategy;
|
|
9
|
+
prefetchGcTime?: number;
|
|
10
|
+
prefetchStaleTime?: number;
|
|
11
|
+
staleTime?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type RouteMetaStatic = {
|
|
15
|
+
defer?: "resolve" | "stream";
|
|
16
|
+
dynamicParams?: boolean;
|
|
17
|
+
mode: "isr" | "static";
|
|
18
|
+
revalidate?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type RouteMeta = {
|
|
22
|
+
authenticate?: boolean;
|
|
23
|
+
client?: RouteMetaClient;
|
|
24
|
+
intercept?: { from: readonly string[]; render: string };
|
|
25
|
+
static?: RouteMetaStatic;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type RouteData = {
|
|
29
|
+
e: string;
|
|
30
|
+
f?: string;
|
|
31
|
+
o: RouteMeta;
|
|
32
|
+
p: () => Promise<{ default: unknown }>;
|
|
33
|
+
t: "r" | "x";
|
|
34
|
+
v: string;
|
|
35
|
+
x: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type TreeNode = {
|
|
39
|
+
c?: TreeNode;
|
|
40
|
+
n?: string;
|
|
41
|
+
o?: TreeNode;
|
|
42
|
+
p?: TreeNode;
|
|
43
|
+
q?: TreeNode;
|
|
44
|
+
r?: RouteData;
|
|
45
|
+
s: Record<string, TreeNode>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type MatchResult = {
|
|
49
|
+
params: Record<string, string | string[]>;
|
|
50
|
+
route: RouteData;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export interface Location<TParams = Record<string, string | string[]>, TSearch = SearchParams, THash = string> {
|
|
54
|
+
hash: THash;
|
|
55
|
+
params: TParams;
|
|
56
|
+
pathname: string;
|
|
57
|
+
search: TSearch;
|
|
58
|
+
url: URL;
|
|
59
|
+
variablePath: string;
|
|
60
|
+
virtualPath: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ComputeMatchIdOptions {
|
|
64
|
+
loaderDeps?: (ctx: { search: SearchParams }) => unknown[];
|
|
65
|
+
params: Record<string, string | string[]>;
|
|
66
|
+
routeId: string;
|
|
67
|
+
search: SearchParams;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ParsedMatchId {
|
|
71
|
+
deps: unknown[];
|
|
72
|
+
params: Record<string, string | string[]>;
|
|
73
|
+
routeId: string;
|
|
74
|
+
}
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export {
|
|
2
|
+
useBlocker,
|
|
3
|
+
useLoaderData,
|
|
4
|
+
useLoaderT,
|
|
5
|
+
useLocation,
|
|
6
|
+
useMatch,
|
|
7
|
+
useNavigate,
|
|
8
|
+
useParams,
|
|
9
|
+
usePreloaderContext,
|
|
10
|
+
usePreloaderT,
|
|
11
|
+
useSearch,
|
|
12
|
+
} from "./hooks.ts";
|
|
13
|
+
export type {
|
|
14
|
+
BlockerState,
|
|
15
|
+
FlareRouter,
|
|
16
|
+
LocationChangeInfo,
|
|
17
|
+
NavigateOptions,
|
|
18
|
+
PrefetchOptions,
|
|
19
|
+
ViewTransitionConfig,
|
|
20
|
+
ViewTransitionDirection,
|
|
21
|
+
ViewTransitionOptions,
|
|
22
|
+
} from "./outlet/index.tsx";
|
|
23
|
+
export { useRouter } from "./outlet/index.tsx";
|
|
24
|
+
export type {
|
|
25
|
+
PrefetchStrategy,
|
|
26
|
+
RouterCacheConfig,
|
|
27
|
+
RouterConfig,
|
|
28
|
+
TrailingSlashMode,
|
|
29
|
+
ViewTransitionDefaults,
|
|
30
|
+
} from "./router-config/index.ts";
|
|
31
|
+
export { createRouter } from "./router-config/index.ts";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* ── Types ────────────────────────────────────────────────────────────── */
|
|
2
|
+
|
|
3
|
+
export interface BingSubmitConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
siteUrl: string;
|
|
6
|
+
urls: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SubmitResult {
|
|
10
|
+
error?: string;
|
|
11
|
+
ok: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* ── Bing Webmaster URL submission ────────────────────────────────────── */
|
|
15
|
+
|
|
16
|
+
export async function submitUrlsToBing(config: BingSubmitConfig): Promise<SubmitResult> {
|
|
17
|
+
const url = `https://ssl.bing.com/webmaster/api.svc/json/SubmitUrlbatch?apikey=${encodeURIComponent(config.apiKey)}`;
|
|
18
|
+
|
|
19
|
+
const response = await fetch(url, {
|
|
20
|
+
body: JSON.stringify({ siteUrl: config.siteUrl, urlList: config.urls }),
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
method: "POST",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
await response.body?.cancel();
|
|
27
|
+
return { error: `Bing submit failed (${response.status})`, ok: false };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await response.body?.cancel();
|
|
31
|
+
return { ok: true };
|
|
32
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* ── Types ────────────────────────────────────────────────────────────── */
|
|
2
|
+
|
|
3
|
+
export interface ServiceAccountCredentials {
|
|
4
|
+
clientEmail: string;
|
|
5
|
+
privateKey: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface TokenResponse {
|
|
9
|
+
access_token: string;
|
|
10
|
+
expires_in: number;
|
|
11
|
+
token_type: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* ── Base64url ────────────────────────────────────────────────────────── */
|
|
15
|
+
|
|
16
|
+
function base64url(buffer: ArrayBuffer): string {
|
|
17
|
+
const bytes = new Uint8Array(buffer);
|
|
18
|
+
let binary = "";
|
|
19
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
20
|
+
binary += String.fromCharCode(bytes[i] ?? 0);
|
|
21
|
+
}
|
|
22
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function base64urlEncode(str: string): string {
|
|
26
|
+
const encoder = new TextEncoder();
|
|
27
|
+
return base64url(encoder.encode(str).buffer as ArrayBuffer);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ── PEM parsing ──────────────────────────────────────────────────────── */
|
|
31
|
+
|
|
32
|
+
function parsePemKey(pem: string): Uint8Array {
|
|
33
|
+
const lines = pem
|
|
34
|
+
.replace(/-----BEGIN [\w\s]+-----/, "")
|
|
35
|
+
.replace(/-----END [\w\s]+-----/, "")
|
|
36
|
+
.replace(/\s+/g, "");
|
|
37
|
+
const binary = atob(lines);
|
|
38
|
+
const buffer = new Uint8Array(binary.length);
|
|
39
|
+
for (let i = 0; i < binary.length; i++) {
|
|
40
|
+
buffer[i] = binary.charCodeAt(i);
|
|
41
|
+
}
|
|
42
|
+
return buffer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ── JWT signing ──────────────────────────────────────────────────────── */
|
|
46
|
+
|
|
47
|
+
export async function signJwt(payload: Record<string, unknown>, privateKeyPem: string): Promise<string> {
|
|
48
|
+
const keyBytes = parsePemKey(privateKeyPem);
|
|
49
|
+
const keyBuffer = new ArrayBuffer(keyBytes.length);
|
|
50
|
+
new Uint8Array(keyBuffer).set(keyBytes);
|
|
51
|
+
const key = await crypto.subtle.importKey("pkcs8", keyBuffer, { hash: "SHA-256", name: "RSASSA-PKCS1-v1_5" }, false, [
|
|
52
|
+
"sign",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const header = base64urlEncode(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
56
|
+
const claims = base64urlEncode(JSON.stringify(payload));
|
|
57
|
+
const input = `${header}.${claims}`;
|
|
58
|
+
|
|
59
|
+
const encoder = new TextEncoder();
|
|
60
|
+
const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, encoder.encode(input));
|
|
61
|
+
|
|
62
|
+
return `${input}.${base64url(signature)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ── Token exchange ───────────────────────────────────────────────────── */
|
|
66
|
+
|
|
67
|
+
const JWT_EXPIRY_SECONDS = 3600;
|
|
68
|
+
|
|
69
|
+
export async function getGoogleAccessToken(credentials: ServiceAccountCredentials, scope: string): Promise<string> {
|
|
70
|
+
const now = Math.floor(Date.now() / 1000);
|
|
71
|
+
const jwt = await signJwt(
|
|
72
|
+
{
|
|
73
|
+
aud: "https://oauth2.googleapis.com/token",
|
|
74
|
+
exp: now + JWT_EXPIRY_SECONDS,
|
|
75
|
+
iat: now,
|
|
76
|
+
iss: credentials.clientEmail,
|
|
77
|
+
scope,
|
|
78
|
+
},
|
|
79
|
+
credentials.privateKey,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const response = await fetch("https://oauth2.googleapis.com/token", {
|
|
83
|
+
body: `grant_type=${encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer")}&assertion=${encodeURIComponent(jwt)}`,
|
|
84
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
85
|
+
method: "POST",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
await response.body?.cancel();
|
|
90
|
+
throw new Error(`Google token exchange failed (${response.status})`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data = (await response.json()) as TokenResponse;
|
|
94
|
+
return data.access_token;
|
|
95
|
+
}
|