@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,454 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { weakMatch } from "../server-handler/etag.ts";
|
|
3
|
+
import { createFileSystemStore } from "../store/filesystem.ts";
|
|
4
|
+
import type { FlareStore, FlareStoreEntry } from "../store/index.ts";
|
|
5
|
+
import type { VitePlugin } from "./index.ts";
|
|
6
|
+
|
|
7
|
+
/* ── Types ───────────────────────────────────────────────────────────── */
|
|
8
|
+
|
|
9
|
+
export interface CdnCacheEntry {
|
|
10
|
+
body: string;
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
status: number;
|
|
13
|
+
storedAt: number;
|
|
14
|
+
surrogateKeys: string[];
|
|
15
|
+
varyHeaders: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ParsedCacheControl {
|
|
19
|
+
maxAge: number | null;
|
|
20
|
+
noStore: boolean;
|
|
21
|
+
private: boolean;
|
|
22
|
+
sMaxAge: number | null;
|
|
23
|
+
staleWhileRevalidate: number | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* ── Cache-Control Parsing ───────────────────────────────────────────── */
|
|
27
|
+
|
|
28
|
+
export function parseCacheControl(header: string | null | undefined): ParsedCacheControl {
|
|
29
|
+
const result: ParsedCacheControl = {
|
|
30
|
+
maxAge: null,
|
|
31
|
+
noStore: false,
|
|
32
|
+
private: false,
|
|
33
|
+
sMaxAge: null,
|
|
34
|
+
staleWhileRevalidate: null,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (!header) return result;
|
|
38
|
+
|
|
39
|
+
const directives = header.split(",").map((d) => d.trim().toLowerCase());
|
|
40
|
+
|
|
41
|
+
for (const directive of directives) {
|
|
42
|
+
if (directive === "no-store") {
|
|
43
|
+
result.noStore = true;
|
|
44
|
+
} else if (directive === "private") {
|
|
45
|
+
result.private = true;
|
|
46
|
+
} else if (directive.startsWith("s-maxage=")) {
|
|
47
|
+
const val = Number.parseInt(directive.slice(9), 10);
|
|
48
|
+
if (!Number.isNaN(val)) result.sMaxAge = val;
|
|
49
|
+
} else if (directive.startsWith("max-age=")) {
|
|
50
|
+
const val = Number.parseInt(directive.slice(8), 10);
|
|
51
|
+
if (!Number.isNaN(val)) result.maxAge = val;
|
|
52
|
+
} else if (directive.startsWith("stale-while-revalidate=")) {
|
|
53
|
+
const val = Number.parseInt(directive.slice(23), 10);
|
|
54
|
+
if (!Number.isNaN(val)) result.staleWhileRevalidate = val;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── Cache Key Building ──────────────────────────────────────────────── */
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build CDN cache key incorporating Vary header dimensions.
|
|
65
|
+
* `cdn:GET:/about` with no Vary, or `cdn:GET:/about:accept-language=en` with Vary.
|
|
66
|
+
*/
|
|
67
|
+
export function buildCdnCacheKey(
|
|
68
|
+
method: string,
|
|
69
|
+
pathname: string,
|
|
70
|
+
varyHeaders: string[],
|
|
71
|
+
requestHeaders: Record<string, string | undefined>,
|
|
72
|
+
): string {
|
|
73
|
+
let key = `cdn:${method}:${pathname}`;
|
|
74
|
+
|
|
75
|
+
if (varyHeaders.length > 0) {
|
|
76
|
+
const sorted = [...varyHeaders].sort();
|
|
77
|
+
const parts = sorted.map((h) => {
|
|
78
|
+
const val = requestHeaders[h.toLowerCase()] ?? "";
|
|
79
|
+
return `${h.toLowerCase()}=${val}`;
|
|
80
|
+
});
|
|
81
|
+
key += `:${parts.join(",")}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return key;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract Vary header names from a response, filtering out `*` (uncacheable).
|
|
89
|
+
* Returns empty array if Vary is `*` or absent.
|
|
90
|
+
*/
|
|
91
|
+
export function parseVaryHeader(header: string | null | undefined): string[] {
|
|
92
|
+
if (!header || header.trim() === "*") return [];
|
|
93
|
+
return header
|
|
94
|
+
.split(",")
|
|
95
|
+
.map((h) => h.trim().toLowerCase())
|
|
96
|
+
.filter((h) => h.length > 0 && h !== "*");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ── Freshness Check ─────────────────────────────────────────────────── */
|
|
100
|
+
|
|
101
|
+
export type FreshnessResult =
|
|
102
|
+
| { state: "fresh" }
|
|
103
|
+
| { state: "miss" }
|
|
104
|
+
| { state: "stale-revalidate" }
|
|
105
|
+
| { state: "stale-expired" };
|
|
106
|
+
|
|
107
|
+
export function checkFreshness(entry: CdnCacheEntry, cc: ParsedCacheControl, now: number): FreshnessResult {
|
|
108
|
+
const age = (now - entry.storedAt) / 1000;
|
|
109
|
+
const maxAge = cc.sMaxAge ?? cc.maxAge;
|
|
110
|
+
|
|
111
|
+
if (maxAge === null) return { state: "fresh" };
|
|
112
|
+
|
|
113
|
+
if (age <= maxAge) return { state: "fresh" };
|
|
114
|
+
|
|
115
|
+
if (cc.staleWhileRevalidate !== null && age <= maxAge + cc.staleWhileRevalidate) {
|
|
116
|
+
return { state: "stale-revalidate" };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { state: "stale-expired" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* ── Surrogate-Key Parsing ───────────────────────────────────────────── */
|
|
123
|
+
|
|
124
|
+
export function parseSurrogateKey(header: string | null | undefined): string[] {
|
|
125
|
+
if (!header) return [];
|
|
126
|
+
return header
|
|
127
|
+
.split(/\s+/)
|
|
128
|
+
.map((k) => k.trim())
|
|
129
|
+
.filter((k) => k.length > 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ── Store Integration ───────────────────────────────────────────────── */
|
|
133
|
+
|
|
134
|
+
export function cdnEntryToStoreEntry(entry: CdnCacheEntry): FlareStoreEntry {
|
|
135
|
+
return {
|
|
136
|
+
data: entry,
|
|
137
|
+
storedAt: entry.storedAt,
|
|
138
|
+
tags: entry.surrogateKeys.length > 0 ? entry.surrogateKeys : undefined,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function storeEntryToCdnEntry(entry: FlareStoreEntry): CdnCacheEntry | null {
|
|
143
|
+
const data = entry.data as CdnCacheEntry | undefined;
|
|
144
|
+
if (!data || typeof data !== "object" || !("body" in data)) return null;
|
|
145
|
+
return data;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ── Cacheable check ─────────────────────────────────────────────────── */
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Determine if a response should be cached by the CDN.
|
|
152
|
+
* Only caches 2xx GET with explicit max-age/s-maxage, not private/no-store.
|
|
153
|
+
*/
|
|
154
|
+
export function isCacheable(status: number, cc: ParsedCacheControl): boolean {
|
|
155
|
+
if (status < 200 || status >= 300) return false;
|
|
156
|
+
if (cc.noStore || cc.private) return false;
|
|
157
|
+
return cc.sMaxAge !== null || cc.maxAge !== null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ── Vite Plugin ─────────────────────────────────────────────────────── */
|
|
161
|
+
|
|
162
|
+
interface CdnDevServer {
|
|
163
|
+
config?: { root?: string; server?: { middlewareMode?: boolean } };
|
|
164
|
+
environments?: Record<string, unknown>;
|
|
165
|
+
httpServer?: { on?: (event: string, fn: () => void) => void } | null;
|
|
166
|
+
middlewares: {
|
|
167
|
+
use: (fn: (req: IncomingLike, res: OutgoingLike, next: () => void) => void) => void;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface IncomingLike {
|
|
172
|
+
headers: Record<string, string | string[] | undefined>;
|
|
173
|
+
method?: string;
|
|
174
|
+
url?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface OutgoingLike {
|
|
178
|
+
end: (body?: unknown) => void;
|
|
179
|
+
getHeader?: (name: string) => string | string[] | number | undefined;
|
|
180
|
+
headersSent?: boolean;
|
|
181
|
+
on?: (event: string, fn: () => void) => void;
|
|
182
|
+
setHeader?: (name: string, value: string | string[]) => void;
|
|
183
|
+
statusCode?: number;
|
|
184
|
+
write?: (chunk?: unknown) => boolean | undefined;
|
|
185
|
+
writeHead: (status: number, headers?: Record<string, string | string[]>) => void;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* CDN cache simulation middleware for dev mode.
|
|
190
|
+
* Intercepts SSR responses, reads Cache-Control / Vary / Surrogate-Key,
|
|
191
|
+
* and simulates CDN caching with max-age, s-maxage, stale-while-revalidate.
|
|
192
|
+
*
|
|
193
|
+
* Also handles conditional requests (If-None-Match → 304) like a real CDN.
|
|
194
|
+
*/
|
|
195
|
+
export function createDevCdnCachePlugin(): VitePlugin {
|
|
196
|
+
let store: FlareStore;
|
|
197
|
+
const revalidating = new Set<string>();
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
configureServer(server: unknown) {
|
|
201
|
+
const vite = server as CdnDevServer;
|
|
202
|
+
const root = vite.config?.root ?? process.cwd();
|
|
203
|
+
store = createFileSystemStore({ cacheDir: join(root, ".flare/cache") });
|
|
204
|
+
|
|
205
|
+
/* Return post-phase callback — runs AFTER Vite's built-in middleware
|
|
206
|
+
but BEFORE flare:dev-server (which is next in plugin order).
|
|
207
|
+
This means CDN cache sits between Vite static serving and Flare SSR. */
|
|
208
|
+
return () => {
|
|
209
|
+
vite.middlewares.use((req, res, next) => {
|
|
210
|
+
handleCdnRequest(req, res, next, store, revalidating).catch(next);
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
enforce: "pre",
|
|
215
|
+
name: "flare:dev-cdn-cache",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Core CDN middleware logic — exported for testing.
|
|
221
|
+
*/
|
|
222
|
+
export async function handleCdnRequest(
|
|
223
|
+
req: IncomingLike,
|
|
224
|
+
res: OutgoingLike,
|
|
225
|
+
next: () => void,
|
|
226
|
+
store: FlareStore,
|
|
227
|
+
revalidating: Set<string>,
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
if (!req.url) {
|
|
230
|
+
next();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* CDN only caches GET requests */
|
|
235
|
+
const method = req.method ?? "GET";
|
|
236
|
+
if (method !== "GET") {
|
|
237
|
+
next();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const url = new URL(req.url, `http://${getHeader(req, "host") ?? "localhost"}`);
|
|
242
|
+
const pathname = url.pathname;
|
|
243
|
+
|
|
244
|
+
/* Skip static assets (files with extensions like .js, .css, .png) */
|
|
245
|
+
if (pathname.includes(".") && !pathname.endsWith("/")) {
|
|
246
|
+
next();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Probe cache — first without Vary to discover what Vary headers were stored */
|
|
251
|
+
const probeKey = buildCdnCacheKey(method, pathname, [], {});
|
|
252
|
+
const probeEntry = await store.get(probeKey);
|
|
253
|
+
const probeCdn = probeEntry ? storeEntryToCdnEntry(probeEntry) : null;
|
|
254
|
+
|
|
255
|
+
let cacheKey: string;
|
|
256
|
+
let cached: CdnCacheEntry | null = null;
|
|
257
|
+
|
|
258
|
+
if (probeCdn && probeCdn.varyHeaders.length > 0) {
|
|
259
|
+
/* Re-key with Vary dimensions from the stored response */
|
|
260
|
+
const reqHeaders = extractRequestHeaders(req, probeCdn.varyHeaders);
|
|
261
|
+
cacheKey = buildCdnCacheKey(method, pathname, probeCdn.varyHeaders, reqHeaders);
|
|
262
|
+
const variedEntry = await store.get(cacheKey);
|
|
263
|
+
cached = variedEntry ? storeEntryToCdnEntry(variedEntry) : null;
|
|
264
|
+
} else if (probeCdn) {
|
|
265
|
+
cacheKey = probeKey;
|
|
266
|
+
cached = probeCdn;
|
|
267
|
+
} else {
|
|
268
|
+
cacheKey = probeKey;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (cached && cached.body.includes("html-proxy")) {
|
|
272
|
+
/* Vite-virtualised inline scripts are not a valid cache body — treat as miss. */
|
|
273
|
+
cached = null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (cached) {
|
|
277
|
+
const cc = parseCacheControl(cached.headers["cache-control"]);
|
|
278
|
+
const freshness = checkFreshness(cached, cc, Date.now());
|
|
279
|
+
|
|
280
|
+
if (freshness.state === "fresh") {
|
|
281
|
+
/* Conditional request: If-None-Match → 304 */
|
|
282
|
+
const ifNoneMatch = getHeader(req, "if-none-match");
|
|
283
|
+
const etag = cached.headers.etag;
|
|
284
|
+
if (ifNoneMatch && etag && weakMatch(ifNoneMatch, etag)) {
|
|
285
|
+
serve304(res, cached);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
serveCached(res, cached, "HIT");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (freshness.state === "stale-revalidate") {
|
|
294
|
+
serveCached(res, cached, "STALE");
|
|
295
|
+
/* Background: delete stale entry so next request gets fresh SSR */
|
|
296
|
+
if (!revalidating.has(cacheKey)) {
|
|
297
|
+
revalidating.add(cacheKey);
|
|
298
|
+
store.delete(cacheKey).catch(() => {});
|
|
299
|
+
if (probeCdn && probeCdn.varyHeaders.length > 0) {
|
|
300
|
+
store.delete(probeKey).catch(() => {});
|
|
301
|
+
}
|
|
302
|
+
setTimeout(() => revalidating.delete(cacheKey), 100);
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* stale-expired → fall through to SSR */
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* Cache miss — intercept response from downstream (Flare SSR) */
|
|
311
|
+
interceptResponse(req, res, next, store, method, pathname);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* ── Helpers ─────────────────────────────────────────────────────────── */
|
|
315
|
+
|
|
316
|
+
function getHeader(req: IncomingLike, name: string): string | undefined {
|
|
317
|
+
const val = req.headers[name];
|
|
318
|
+
return Array.isArray(val) ? val[0] : val;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function extractRequestHeaders(req: IncomingLike, varyHeaders: string[]): Record<string, string | undefined> {
|
|
322
|
+
const result: Record<string, string | undefined> = {};
|
|
323
|
+
for (const h of varyHeaders) {
|
|
324
|
+
result[h] = getHeader(req, h);
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function serveCached(res: OutgoingLike, entry: CdnCacheEntry, cacheStatus: string): void {
|
|
330
|
+
const headers: Record<string, string> = { ...entry.headers };
|
|
331
|
+
headers["flare-cdn"] = cacheStatus;
|
|
332
|
+
const age = Math.round((Date.now() - entry.storedAt) / 1000);
|
|
333
|
+
headers.age = String(age);
|
|
334
|
+
res.writeHead(entry.status, headers);
|
|
335
|
+
res.end(entry.body);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function serve304(res: OutgoingLike, entry: CdnCacheEntry): void {
|
|
339
|
+
const headers: Record<string, string> = {};
|
|
340
|
+
/* 304 only carries metadata headers, not body headers */
|
|
341
|
+
if (entry.headers.etag) headers.etag = entry.headers.etag;
|
|
342
|
+
if (entry.headers["cache-control"]) headers["cache-control"] = entry.headers["cache-control"];
|
|
343
|
+
if (entry.headers.vary) headers.vary = entry.headers.vary;
|
|
344
|
+
headers["flare-cdn"] = "HIT";
|
|
345
|
+
res.writeHead(304, headers);
|
|
346
|
+
res.end();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function chunkToString(chunk: unknown): string {
|
|
350
|
+
if (chunk == null) return "";
|
|
351
|
+
if (typeof chunk === "string") return chunk;
|
|
352
|
+
if (chunk instanceof Uint8Array) return new TextDecoder().decode(chunk);
|
|
353
|
+
if (ArrayBuffer.isView(chunk)) {
|
|
354
|
+
const view = chunk as ArrayBufferView;
|
|
355
|
+
return new TextDecoder().decode(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
|
356
|
+
}
|
|
357
|
+
return "";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function captureHeaderValue(headers: Record<string, string>, key: string, val: string | string[]): void {
|
|
361
|
+
headers[key.toLowerCase()] = Array.isArray(val) ? (val[0] ?? "") : val;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function interceptResponse(
|
|
365
|
+
req: IncomingLike,
|
|
366
|
+
res: OutgoingLike,
|
|
367
|
+
next: () => void,
|
|
368
|
+
store: FlareStore,
|
|
369
|
+
method: string,
|
|
370
|
+
pathname: string,
|
|
371
|
+
): void {
|
|
372
|
+
const originalWriteHead = res.writeHead.bind(res);
|
|
373
|
+
const originalEnd = res.end.bind(res);
|
|
374
|
+
const originalWrite = res.write?.bind(res);
|
|
375
|
+
const originalSetHeader = res.setHeader?.bind(res);
|
|
376
|
+
let capturedStatus = 200;
|
|
377
|
+
const capturedHeaders: Record<string, string> = {};
|
|
378
|
+
let capturedBody = "";
|
|
379
|
+
let ended = false;
|
|
380
|
+
let wroteHead = false;
|
|
381
|
+
|
|
382
|
+
res.writeHead = (status: number, headers?: Record<string, string | string[]>) => {
|
|
383
|
+
wroteHead = true;
|
|
384
|
+
capturedStatus = status;
|
|
385
|
+
if (headers) {
|
|
386
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
387
|
+
captureHeaderValue(capturedHeaders, key, val);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return originalWriteHead(status, headers);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (originalSetHeader) {
|
|
394
|
+
res.setHeader = (name: string, value: string | string[]) => {
|
|
395
|
+
captureHeaderValue(capturedHeaders, name, value);
|
|
396
|
+
return originalSetHeader(name, value);
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (originalWrite) {
|
|
401
|
+
res.write = (chunk?: unknown) => {
|
|
402
|
+
capturedBody += chunkToString(chunk);
|
|
403
|
+
return originalWrite(chunk);
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
res.end = (body?: unknown) => {
|
|
408
|
+
if (ended) return originalEnd(body);
|
|
409
|
+
ended = true;
|
|
410
|
+
capturedBody += chunkToString(body);
|
|
411
|
+
|
|
412
|
+
if (!wroteHead && typeof res.statusCode === "number") capturedStatus = res.statusCode;
|
|
413
|
+
const headerFromRes = res.getHeader?.("cache-control");
|
|
414
|
+
if (!capturedHeaders["cache-control"] && headerFromRes != null) {
|
|
415
|
+
captureHeaderValue(
|
|
416
|
+
capturedHeaders,
|
|
417
|
+
"cache-control",
|
|
418
|
+
Array.isArray(headerFromRes) ? headerFromRes : String(headerFromRes),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const cc = parseCacheControl(capturedHeaders["cache-control"]);
|
|
423
|
+
|
|
424
|
+
/* workerd / Cloudflare Vite streams via write(chunk)+end() with no
|
|
425
|
+
final body. An empty capture is a miss, not a cacheable document. */
|
|
426
|
+
if (isCacheable(capturedStatus, cc) && capturedBody.length > 0 && !capturedBody.includes("html-proxy")) {
|
|
427
|
+
const varyHeaders = parseVaryHeader(capturedHeaders.vary);
|
|
428
|
+
const surrogateKeys = parseSurrogateKey(capturedHeaders["surrogate-key"]);
|
|
429
|
+
const reqHeaders = extractRequestHeaders(req, varyHeaders);
|
|
430
|
+
const cacheKey = buildCdnCacheKey(method, pathname, varyHeaders, reqHeaders);
|
|
431
|
+
|
|
432
|
+
const entry: CdnCacheEntry = {
|
|
433
|
+
body: capturedBody,
|
|
434
|
+
headers: capturedHeaders,
|
|
435
|
+
status: capturedStatus,
|
|
436
|
+
storedAt: Date.now(),
|
|
437
|
+
surrogateKeys,
|
|
438
|
+
varyHeaders,
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
store.set(cacheKey, cdnEntryToStoreEntry(entry)).catch(() => {});
|
|
442
|
+
|
|
443
|
+
/* Also store a probe entry for Vary discovery on next lookup */
|
|
444
|
+
if (varyHeaders.length > 0) {
|
|
445
|
+
const probeKey = buildCdnCacheKey(method, pathname, [], {});
|
|
446
|
+
store.set(probeKey, cdnEntryToStoreEntry(entry)).catch(() => {});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return originalEnd(body);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
next();
|
|
454
|
+
}
|