@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,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createEffect,
|
|
4
|
+
createSignal,
|
|
5
|
+
type JSX,
|
|
6
|
+
on,
|
|
7
|
+
onCleanup,
|
|
8
|
+
onMount,
|
|
9
|
+
sharedConfig,
|
|
10
|
+
useContext,
|
|
11
|
+
} from "solid-js";
|
|
12
|
+
import { BroadcastCtx } from "../broadcast/context.ts";
|
|
13
|
+
import { escapeJsString } from "../theme/index.tsx";
|
|
14
|
+
|
|
15
|
+
/* ── Types ─────────────────────────────────────────────────────────── */
|
|
16
|
+
|
|
17
|
+
export interface LocaleConfig {
|
|
18
|
+
broadcast?: boolean;
|
|
19
|
+
cookieName?: string;
|
|
20
|
+
defaultLocale: string;
|
|
21
|
+
locales: readonly string[];
|
|
22
|
+
paramName?: string;
|
|
23
|
+
skip?: readonly string[];
|
|
24
|
+
syncDirection?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LocaleContextValue {
|
|
28
|
+
defaultLocale: string;
|
|
29
|
+
locale: () => string;
|
|
30
|
+
locales: readonly string[];
|
|
31
|
+
setLocale: (locale: string) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ── getLocaleScript ───────────────────────────────────────────────── */
|
|
35
|
+
|
|
36
|
+
export function getLocaleScript(opts: LocaleConfig): string {
|
|
37
|
+
const defaultLocale = escapeJsString(opts.defaultLocale);
|
|
38
|
+
return `(d=>{var e=document.documentElement;e.setAttribute("lang",e.getAttribute("lang")||d)})("${defaultLocale}")`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ── Context ───────────────────────────────────────────────────────── */
|
|
42
|
+
|
|
43
|
+
const LocaleCtx = createContext<LocaleContextValue>();
|
|
44
|
+
|
|
45
|
+
export function LocaleProvider(props: { children: JSX.Element; config: LocaleConfig; initial?: string }): JSX.Element {
|
|
46
|
+
if (sharedConfig.context) {
|
|
47
|
+
return props.children;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const cfg = props.config;
|
|
51
|
+
const defaultLocale = cfg.defaultLocale;
|
|
52
|
+
const cookieName = cfg.cookieName ?? "flare.locale";
|
|
53
|
+
|
|
54
|
+
/* Capture context at component setup level (not inside onMount) */
|
|
55
|
+
const channel = cfg.broadcast ? useContext(BroadcastCtx) : undefined;
|
|
56
|
+
|
|
57
|
+
/* URL is truth: initial comes from server (URL params), not localStorage */
|
|
58
|
+
const initial = props.initial ?? defaultLocale;
|
|
59
|
+
const [locale, setLocaleSignal] = createSignal(initial);
|
|
60
|
+
|
|
61
|
+
/* Cross-tab sync: BroadcastChannel for locale (opt-in) */
|
|
62
|
+
onMount(() => {
|
|
63
|
+
if (!channel) return;
|
|
64
|
+
|
|
65
|
+
const unsubscribe = channel.onMessage((msg) => {
|
|
66
|
+
if (msg.type === "locale" && typeof msg.value === "string" && cfg.locales.includes(msg.value)) {
|
|
67
|
+
setLocaleSignal(msg.value);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
createEffect(
|
|
72
|
+
on(
|
|
73
|
+
locale,
|
|
74
|
+
(l) => {
|
|
75
|
+
channel.broadcast({ type: "locale", value: l });
|
|
76
|
+
},
|
|
77
|
+
{ defer: true },
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
onCleanup(unsubscribe);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
createEffect(() => {
|
|
85
|
+
const l = locale();
|
|
86
|
+
if (typeof document === "undefined") return;
|
|
87
|
+
document.documentElement.setAttribute("lang", l);
|
|
88
|
+
document.cookie = `${cookieName}=${l}; path=/; max-age=31536000; samesite=lax`;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const setLocale = (l: string): void => {
|
|
92
|
+
if (!cfg.locales.includes(l)) return;
|
|
93
|
+
setLocaleSignal(l);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const value: LocaleContextValue = {
|
|
97
|
+
defaultLocale,
|
|
98
|
+
locale,
|
|
99
|
+
locales: cfg.locales,
|
|
100
|
+
setLocale,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return <LocaleCtx.Provider value={value}>{props.children}</LocaleCtx.Provider>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function useLocale(): LocaleContextValue {
|
|
107
|
+
const ctx = useContext(LocaleCtx);
|
|
108
|
+
if (!ctx)
|
|
109
|
+
throw new Error(
|
|
110
|
+
"useLocale() called outside LocaleProvider. Ensure <LocaleProvider> wraps components using locale hooks.",
|
|
111
|
+
);
|
|
112
|
+
return ctx;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ── buildHreflangLinks ────────────────────────────────────────────── */
|
|
116
|
+
|
|
117
|
+
export function buildHreflangLinks(opts: {
|
|
118
|
+
baseUrl: string;
|
|
119
|
+
config: LocaleConfig;
|
|
120
|
+
pathname: string;
|
|
121
|
+
}): Record<string, string> {
|
|
122
|
+
const base = opts.baseUrl.replace(/\/$/, "");
|
|
123
|
+
const links: Record<string, string> = {};
|
|
124
|
+
|
|
125
|
+
for (const locale of opts.config.locales) {
|
|
126
|
+
if (locale === opts.config.defaultLocale) {
|
|
127
|
+
links[locale] = `${base}${opts.pathname}`;
|
|
128
|
+
} else {
|
|
129
|
+
const path = opts.pathname === "/" ? "" : opts.pathname;
|
|
130
|
+
links[locale] = `${base}/${locale}${path}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
links["x-default"] = links[opts.config.defaultLocale] ?? `${base}${opts.pathname}`;
|
|
135
|
+
|
|
136
|
+
return links;
|
|
137
|
+
}
|
package/src/locale.ts
ADDED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import defaultLevel from "virtual:flare-log-level";
|
|
2
|
+
|
|
3
|
+
export type LogLevel = "error" | "silent" | "verbose" | "warn";
|
|
4
|
+
|
|
5
|
+
const PRIORITY: Record<LogLevel, number> = { error: 1, silent: 0, verbose: 3, warn: 2 };
|
|
6
|
+
|
|
7
|
+
const level: LogLevel = defaultLevel;
|
|
8
|
+
|
|
9
|
+
export function warn(tag: string, msg: string, data?: unknown): void {
|
|
10
|
+
if (PRIORITY[level] < 2) return;
|
|
11
|
+
console.warn(`[flare:${tag}]`, msg, ...(data !== undefined ? [data] : []));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function error(tag: string, msg: string, data?: unknown): void {
|
|
15
|
+
if (PRIORITY[level] < 1) return;
|
|
16
|
+
console.error(`[flare:${tag}]`, msg, ...(data !== undefined ? [data] : []));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function verbose(tag: string, msg: string, data?: unknown): void {
|
|
20
|
+
if (PRIORITY[level] < 3) return;
|
|
21
|
+
console.log(`[flare:${tag}]`, msg, ...(data !== undefined ? [data] : []));
|
|
22
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { FlareMiddleware, MiddlewareContext } from "..";
|
|
2
|
+
|
|
3
|
+
interface ServiceBinding {
|
|
4
|
+
fetch: (request: Request) => Promise<Response>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface ApiProxyConfig<TEnv = unknown> {
|
|
8
|
+
enabled?: boolean | ((ctx: { env: TEnv }) => boolean);
|
|
9
|
+
headers?: (ctx: { env: TEnv; request: Request }) => Record<string, string>;
|
|
10
|
+
pathPrefix: string;
|
|
11
|
+
rewrite?: (path: string) => string;
|
|
12
|
+
target: (ctx: { env: TEnv }) => ServiceBinding;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function apiProxy<TEnv = unknown>(config: ApiProxyConfig<TEnv>): FlareMiddleware<TEnv> {
|
|
16
|
+
return async (ctx: MiddlewareContext<TEnv>) => {
|
|
17
|
+
/* check enabled */
|
|
18
|
+
if (config.enabled !== undefined) {
|
|
19
|
+
const enabled = typeof config.enabled === "function" ? config.enabled({ env: ctx.env }) : config.enabled;
|
|
20
|
+
if (!enabled) return ctx.next();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* check path match */
|
|
24
|
+
const pathname = ctx.url.pathname;
|
|
25
|
+
if (pathname !== config.pathPrefix && !pathname.startsWith(`${config.pathPrefix}/`)) {
|
|
26
|
+
return ctx.next();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
/* rewrite path */
|
|
31
|
+
let targetPath = pathname;
|
|
32
|
+
if (config.rewrite) {
|
|
33
|
+
targetPath = config.rewrite(pathname);
|
|
34
|
+
} else {
|
|
35
|
+
targetPath = pathname.slice(config.pathPrefix.length) || "/";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* build proxied request */
|
|
39
|
+
const targetUrl = new URL(targetPath, ctx.url.origin);
|
|
40
|
+
targetUrl.search = ctx.url.search;
|
|
41
|
+
const headers = new Headers(ctx.request.headers);
|
|
42
|
+
|
|
43
|
+
if (config.headers) {
|
|
44
|
+
const extra = config.headers({ env: ctx.env, request: ctx.request });
|
|
45
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
46
|
+
headers.set(k, v);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const proxyRequest = new Request(targetUrl.toString(), {
|
|
51
|
+
body: ctx.request.body,
|
|
52
|
+
duplex: "half",
|
|
53
|
+
headers,
|
|
54
|
+
method: ctx.request.method,
|
|
55
|
+
} as RequestInit);
|
|
56
|
+
|
|
57
|
+
const service = config.target({ env: ctx.env });
|
|
58
|
+
const response = await service.fetch(proxyRequest);
|
|
59
|
+
return ctx.bypass(response);
|
|
60
|
+
} catch {
|
|
61
|
+
return ctx.bypass(new Response("Bad Gateway", { status: 502 }));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { FlareMiddleware, MiddlewareContext } from "..";
|
|
2
|
+
|
|
3
|
+
interface R2Object {
|
|
4
|
+
body: ReadableStream;
|
|
5
|
+
etag: string;
|
|
6
|
+
httpMetadata?: { contentType?: string };
|
|
7
|
+
size: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface R2BucketBinding {
|
|
11
|
+
get: (key: string) => Promise<R2Object | null>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CdnProxyConfig<TEnv = unknown> {
|
|
15
|
+
bucket: (ctx: { env: TEnv }) => R2BucketBinding;
|
|
16
|
+
cacheControl?: string;
|
|
17
|
+
edgeCache?: boolean;
|
|
18
|
+
pathPrefix: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cdnProxy<TEnv = unknown>(config: CdnProxyConfig<TEnv>): FlareMiddleware<TEnv> {
|
|
22
|
+
const defaultCacheControl = config.cacheControl ?? "public, max-age=31536000, immutable";
|
|
23
|
+
|
|
24
|
+
return async (ctx: MiddlewareContext<TEnv>) => {
|
|
25
|
+
const pathname = ctx.url.pathname;
|
|
26
|
+
if (!pathname.startsWith(`${config.pathPrefix}/`)) return ctx.next();
|
|
27
|
+
|
|
28
|
+
const rawKey = pathname.slice(config.pathPrefix.length + 1);
|
|
29
|
+
let key: string;
|
|
30
|
+
try {
|
|
31
|
+
key = decodeURIComponent(rawKey);
|
|
32
|
+
} catch {
|
|
33
|
+
return ctx.bypass(new Response("Bad Request", { status: 400 }));
|
|
34
|
+
}
|
|
35
|
+
/* Reject path traversal, null bytes, backslashes, and CRLF */
|
|
36
|
+
if (key.includes("..") || key.includes("\0") || key.includes("\\") || key.includes("\r") || key.includes("\n")) {
|
|
37
|
+
return ctx.bypass(new Response("Bad Request", { status: 400 }));
|
|
38
|
+
}
|
|
39
|
+
const bucket = config.bucket({ env: ctx.env });
|
|
40
|
+
const object = await bucket.get(key);
|
|
41
|
+
|
|
42
|
+
if (!object) {
|
|
43
|
+
return ctx.bypass(new Response("Not Found", { status: 404 }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const headers: Record<string, string> = {
|
|
47
|
+
"Cache-Control": defaultCacheControl,
|
|
48
|
+
"Content-Length": String(object.size),
|
|
49
|
+
ETag: object.etag,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (object.httpMetadata?.contentType) {
|
|
53
|
+
headers["Content-Type"] = object.httpMetadata.contentType;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return ctx.bypass(new Response(object.body, { headers }));
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { match as cldrMatch } from "@formatjs/intl-localematcher";
|
|
2
|
+
import { isbot } from "isbot";
|
|
3
|
+
import Negotiator from "negotiator";
|
|
4
|
+
import type { FlareMiddleware } from "..";
|
|
5
|
+
import { matchRoute, toLocaleMatch } from "../../router-primitives/tree.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Matches BCP-47-like path segments with 2-letter primary subtag:
|
|
9
|
+
* xx, xx-YY, xx-Yyyy, xx-Yyyy-ZZ
|
|
10
|
+
* Covers: en, en-us, zh-hans, zh-hant-tw, sr-latn, pt-br
|
|
11
|
+
* Uses 2-letter primary only to avoid false positives on route
|
|
12
|
+
* segments like isr-test, env-fn-test, api-data.
|
|
13
|
+
*/
|
|
14
|
+
const LOCALE_LIKE_RE = /^[a-z]{2}(-[a-z]{2,4}){0,2}$/i;
|
|
15
|
+
|
|
16
|
+
export interface I18nMiddlewareOptions {
|
|
17
|
+
botDetection?: boolean;
|
|
18
|
+
cookie?: {
|
|
19
|
+
maxAge?: number;
|
|
20
|
+
secure?: boolean;
|
|
21
|
+
};
|
|
22
|
+
cookieName?: string;
|
|
23
|
+
skip?: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getLocaleFromPath(pathname: string, localeSet: Set<string>): string | null {
|
|
27
|
+
const slashIdx = pathname.indexOf("/", 1);
|
|
28
|
+
const segment = (slashIdx === -1 ? pathname.slice(1) : pathname.slice(1, slashIdx)).toLowerCase();
|
|
29
|
+
return localeSet.has(segment) ? segment : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getLocaleFromCookie(request: Request, cookieName: string, localeSet: Set<string>): string | null {
|
|
33
|
+
const cookieHeader = request.headers.get("cookie");
|
|
34
|
+
if (!cookieHeader) return null;
|
|
35
|
+
for (const pair of cookieHeader.split(";")) {
|
|
36
|
+
const eqIdx = pair.indexOf("=");
|
|
37
|
+
if (eqIdx === -1) continue;
|
|
38
|
+
const name = pair.slice(0, eqIdx).trim();
|
|
39
|
+
if (name !== cookieName) continue;
|
|
40
|
+
const locale = pair
|
|
41
|
+
.slice(eqIdx + 1)
|
|
42
|
+
.trim()
|
|
43
|
+
.toLowerCase();
|
|
44
|
+
return localeSet.has(locale) ? locale : null;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getLocaleFromAcceptLanguage(request: Request, locales: readonly string[], defaultLocale: string): string {
|
|
50
|
+
const acceptLanguage = request.headers.get("accept-language");
|
|
51
|
+
if (!acceptLanguage) return defaultLocale;
|
|
52
|
+
|
|
53
|
+
const negotiator = new Negotiator({ headers: { "accept-language": acceptLanguage } });
|
|
54
|
+
const requested = negotiator.languages();
|
|
55
|
+
if (requested.length === 0) return defaultLocale;
|
|
56
|
+
|
|
57
|
+
for (const req of requested) {
|
|
58
|
+
try {
|
|
59
|
+
const result = cldrMatch([req], locales as string[], "");
|
|
60
|
+
if (result && result !== "") {
|
|
61
|
+
return result.toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
/* no match, try next */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return defaultLocale;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildCookieHeader(
|
|
72
|
+
locale: string,
|
|
73
|
+
cookieName: string,
|
|
74
|
+
maxAge: number,
|
|
75
|
+
isHttps: boolean,
|
|
76
|
+
secure?: boolean,
|
|
77
|
+
): string {
|
|
78
|
+
/* Defense-in-depth: strip chars that could enable header/cookie injection.
|
|
79
|
+
* Primary gate is localeSet.has() before any call site, but this guards
|
|
80
|
+
* against future code paths that might bypass that check. */
|
|
81
|
+
const safe = locale.replace(/[\r\n;\0]/g, "");
|
|
82
|
+
const secureFlag = (secure ?? isHttps) ? "; Secure" : "";
|
|
83
|
+
return `${cookieName}=${safe}; Path=/; Max-Age=${maxAge}; SameSite=Lax${secureFlag}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function i18n(options?: I18nMiddlewareOptions): FlareMiddleware {
|
|
87
|
+
let cachedLocales: readonly string[] | null = null;
|
|
88
|
+
let cachedLocaleSet: Set<string> = new Set();
|
|
89
|
+
let cachedSkipPaths: string[] = [];
|
|
90
|
+
let cachedCookieName = "";
|
|
91
|
+
let cachedMaxAge = 0;
|
|
92
|
+
return async (ctx) => {
|
|
93
|
+
if (!ctx.locale) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
'i18n() middleware requires locale config on the router. Use router.locale({ locales: [...], defaultLocale: "..." }).',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const { defaultLocale, locales } = ctx.locale;
|
|
99
|
+
if (cachedLocales !== locales) {
|
|
100
|
+
cachedLocales = locales;
|
|
101
|
+
cachedLocaleSet = new Set(locales.map((l) => l.toLowerCase()));
|
|
102
|
+
cachedCookieName = options?.cookieName ?? ctx.locale.cookieName ?? "flare.locale";
|
|
103
|
+
cachedMaxAge = options?.cookie?.maxAge ?? 31536000;
|
|
104
|
+
const skip = options?.skip ?? ctx.locale.skip ?? [];
|
|
105
|
+
cachedSkipPaths = ["/_fn/", ...skip];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { pathname } = ctx.url;
|
|
109
|
+
const isHttps = ctx.url.protocol === "https:";
|
|
110
|
+
|
|
111
|
+
/* Prerender snapshots the exact expanded URL — do not locale-redirect. */
|
|
112
|
+
if (ctx.request.headers.get("x-flare-prerender") === "1") {
|
|
113
|
+
const prerenderLocale = getLocaleFromPath(pathname, cachedLocaleSet);
|
|
114
|
+
ctx.serverContext.locale = prerenderLocale ?? defaultLocale;
|
|
115
|
+
return ctx.next();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Bots get default locale for consistent SEO */
|
|
119
|
+
if (options?.botDetection !== false) {
|
|
120
|
+
const userAgent = ctx.request.headers.get("user-agent") ?? "";
|
|
121
|
+
if (isbot(userAgent)) {
|
|
122
|
+
ctx.serverContext.locale = defaultLocale;
|
|
123
|
+
return ctx.next();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Skip configured paths */
|
|
128
|
+
for (const skipPath of cachedSkipPaths) {
|
|
129
|
+
if (pathname.startsWith(skipPath)) {
|
|
130
|
+
ctx.serverContext.locale = defaultLocale;
|
|
131
|
+
return ctx.next();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Skip files with extensions */
|
|
136
|
+
const lastDot = pathname.lastIndexOf(".");
|
|
137
|
+
if (lastDot > pathname.lastIndexOf("/")) {
|
|
138
|
+
ctx.serverContext.locale = defaultLocale;
|
|
139
|
+
return ctx.next();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Normalize locale case (EN-US -> en-us) */
|
|
143
|
+
const slashIdx = pathname.indexOf("/", 1);
|
|
144
|
+
const firstSegment = slashIdx === -1 ? pathname.slice(1) : pathname.slice(1, slashIdx);
|
|
145
|
+
|
|
146
|
+
if (firstSegment && LOCALE_LIKE_RE.test(firstSegment)) {
|
|
147
|
+
const lowerSegment = firstSegment.toLowerCase();
|
|
148
|
+
if (firstSegment !== lowerSegment) {
|
|
149
|
+
const newUrl = new URL(ctx.url);
|
|
150
|
+
newUrl.pathname = pathname.replace(firstSegment, lowerSegment);
|
|
151
|
+
const headers = new Headers({ Location: newUrl.href });
|
|
152
|
+
if (cachedLocaleSet.has(lowerSegment)) {
|
|
153
|
+
headers.set(
|
|
154
|
+
"Set-Cookie",
|
|
155
|
+
buildCookieHeader(lowerSegment, cachedCookieName, cachedMaxAge, isHttps, options?.cookie?.secure),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return ctx.bypass(new Response(null, { headers, status: 302 }));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const pathLocale = getLocaleFromPath(pathname, cachedLocaleSet);
|
|
163
|
+
const cookieLocale = getLocaleFromCookie(ctx.request, cachedCookieName, cachedLocaleSet);
|
|
164
|
+
const hasInvalidLocale = !pathLocale && firstSegment !== "" && LOCALE_LIKE_RE.test(firstSegment);
|
|
165
|
+
|
|
166
|
+
/* Invalid/unsupported locale-like segment in path → strip it */
|
|
167
|
+
if (hasInvalidLocale) {
|
|
168
|
+
const newUrl = new URL(ctx.url);
|
|
169
|
+
newUrl.pathname = pathname.replace(`/${firstSegment}`, "") || "/";
|
|
170
|
+
const headers = new Headers({ Location: newUrl.href });
|
|
171
|
+
headers.set(
|
|
172
|
+
"Set-Cookie",
|
|
173
|
+
buildCookieHeader(defaultLocale, cachedCookieName, cachedMaxAge, isHttps, options?.cookie?.secure),
|
|
174
|
+
);
|
|
175
|
+
return ctx.bypass(new Response(null, { headers, status: 302 }));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Default locale in URL → strip prefix when the unprefixed path is a real route.
|
|
179
|
+
Required `[locale]/ssg-about` has no `/ssg-about` twin — keep `/en/ssg-about`. */
|
|
180
|
+
if (pathLocale === defaultLocale) {
|
|
181
|
+
const strippedPath = pathname.replace(`/${defaultLocale}`, "") || "/";
|
|
182
|
+
const canStrip =
|
|
183
|
+
!ctx.routeTree || matchRoute(ctx.routeTree, strippedPath, false, toLocaleMatch(ctx.locale)) !== null;
|
|
184
|
+
if (canStrip) {
|
|
185
|
+
const newUrl = new URL(ctx.url);
|
|
186
|
+
newUrl.pathname = strippedPath;
|
|
187
|
+
const headers = new Headers({ Location: newUrl.href });
|
|
188
|
+
headers.set(
|
|
189
|
+
"Set-Cookie",
|
|
190
|
+
buildCookieHeader(defaultLocale, cachedCookieName, cachedMaxAge, isHttps, options?.cookie?.secure),
|
|
191
|
+
);
|
|
192
|
+
return ctx.bypass(new Response(null, { headers, status: 302 }));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
* First visit to root — Accept-Language redirect.
|
|
198
|
+
* Only when no cookie exists (truly first visit).
|
|
199
|
+
*/
|
|
200
|
+
if (!pathLocale && pathname === "/" && !cookieLocale) {
|
|
201
|
+
const acceptLocale = getLocaleFromAcceptLanguage(ctx.request, locales, defaultLocale);
|
|
202
|
+
if (acceptLocale !== defaultLocale) {
|
|
203
|
+
const newUrl = new URL(ctx.url);
|
|
204
|
+
newUrl.pathname = `/${acceptLocale}`;
|
|
205
|
+
const headers = new Headers({ Location: newUrl.href });
|
|
206
|
+
headers.set(
|
|
207
|
+
"Set-Cookie",
|
|
208
|
+
buildCookieHeader(acceptLocale, cachedCookieName, cachedMaxAge, isHttps, options?.cookie?.secure),
|
|
209
|
+
);
|
|
210
|
+
return ctx.bypass(new Response(null, { headers, status: 302 }));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* Cookie-respect redirect: unprefixed SSR path + cookie has non-default locale.
|
|
216
|
+
* Only for full page loads (not NDJSON/prefetch — SPA router handles those).
|
|
217
|
+
* e.g. /about with cookie=fr → 302 → /fr/about
|
|
218
|
+
*/
|
|
219
|
+
const isDataRequest = ctx.request.headers.get("x-d") === "1";
|
|
220
|
+
if (!pathLocale && cookieLocale && cookieLocale !== defaultLocale && !isDataRequest) {
|
|
221
|
+
const newUrl = new URL(ctx.url);
|
|
222
|
+
newUrl.pathname = `/${cookieLocale}${pathname}`;
|
|
223
|
+
return ctx.bypass(
|
|
224
|
+
new Response(null, {
|
|
225
|
+
headers: new Headers({ Location: newUrl.href }),
|
|
226
|
+
status: 302,
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* URL is truth: locale comes from path, or default if no prefix */
|
|
232
|
+
const currentLocale = pathLocale ?? defaultLocale;
|
|
233
|
+
ctx.serverContext.locale = currentLocale;
|
|
234
|
+
|
|
235
|
+
/* Prefetch requests must not set cookies — SPA prefetches multiple
|
|
236
|
+
* locale URLs after hydration, and the last one to complete would
|
|
237
|
+
* overwrite the correct cookie from the actual page load. */
|
|
238
|
+
const isPrefetch = ctx.request.headers.get("x-p") === "1";
|
|
239
|
+
const needsCookie = !isPrefetch && currentLocale !== cookieLocale;
|
|
240
|
+
if (needsCookie) {
|
|
241
|
+
const setCookieHeader = buildCookieHeader(
|
|
242
|
+
currentLocale,
|
|
243
|
+
cachedCookieName,
|
|
244
|
+
cachedMaxAge,
|
|
245
|
+
isHttps,
|
|
246
|
+
options?.cookie?.secure,
|
|
247
|
+
);
|
|
248
|
+
ctx.onResponse((response) => {
|
|
249
|
+
response.headers.append("Set-Cookie", setCookieHeader);
|
|
250
|
+
return response;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return ctx.next();
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { error } from "../../logger.ts";
|
|
2
|
+
import type { FlareMiddleware, MiddlewareContext } from "..";
|
|
3
|
+
|
|
4
|
+
const PING_PATH = "/_flare/keepalive";
|
|
5
|
+
|
|
6
|
+
interface KeepaliveConfig<TEnv = unknown> {
|
|
7
|
+
handler?: (ctx: MiddlewareContext<TEnv>) => Promise<void> | void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function keepalive<TEnv = unknown>(config?: KeepaliveConfig<TEnv>): FlareMiddleware<TEnv> {
|
|
11
|
+
return async (ctx: MiddlewareContext<TEnv>) => {
|
|
12
|
+
if (ctx.url.pathname !== PING_PATH || ctx.request.method !== "GET") return ctx.next();
|
|
13
|
+
|
|
14
|
+
if (config?.handler) {
|
|
15
|
+
try {
|
|
16
|
+
await config.handler(ctx);
|
|
17
|
+
} catch (e: unknown) {
|
|
18
|
+
error("keepalive", "handler error", e);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return ctx.bypass(new Response(null, { status: 204 }));
|
|
23
|
+
};
|
|
24
|
+
}
|