@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,168 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { RouteDefinition } from "../generators/index.ts";
|
|
4
|
+
import { scanSourceFiles } from "../generators/index.ts";
|
|
5
|
+
import type { PrerenderRoute } from "../prerender/index.ts";
|
|
6
|
+
import { prerender } from "../prerender/index.ts";
|
|
7
|
+
import type { ServerHandler } from "../server-handler/index.ts";
|
|
8
|
+
import type { FlareStore } from "../store/index.ts";
|
|
9
|
+
import { createFileSystemStore } from "../store/filesystem.ts";
|
|
10
|
+
import type { FlarePluginConfig, VitePlugin } from "./index.ts";
|
|
11
|
+
import type { ResolvedEntries } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert virtual path to URL pathname.
|
|
15
|
+
* `_root_/about` → `/about`, `_root_/blog/[slug]` → `/blog/[slug]`
|
|
16
|
+
*/
|
|
17
|
+
function virtualPathToPathname(virtualPath: string): string {
|
|
18
|
+
const parts = virtualPath.split("/");
|
|
19
|
+
const rootIdx = parts.findIndex((p) => p.startsWith("_") && p.endsWith("_") && p.length >= 3);
|
|
20
|
+
const urlParts: string[] = [];
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < parts.length; i++) {
|
|
23
|
+
const part = parts[i] ?? "";
|
|
24
|
+
if (rootIdx >= 0 && i <= rootIdx) continue;
|
|
25
|
+
if (part.startsWith("(") && part.endsWith(")")) continue;
|
|
26
|
+
if (part === "") continue;
|
|
27
|
+
urlParts.push(part);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return `/${urlParts.join("/")}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Filter route definitions to only static (SSG) non-dynamic routes
|
|
35
|
+
* suitable for dev-mode prerendering. Dynamic routes and ISR routes
|
|
36
|
+
* are excluded — dynamic requires params, ISR has its own serving path.
|
|
37
|
+
*/
|
|
38
|
+
export function filterDevPrerenderRoutes(defs: RouteDefinition[]): PrerenderRoute[] {
|
|
39
|
+
const routes: PrerenderRoute[] = [];
|
|
40
|
+
|
|
41
|
+
for (const def of defs) {
|
|
42
|
+
if (def.type !== "page") continue;
|
|
43
|
+
if (def.responseRoute) continue;
|
|
44
|
+
if (!def.cache.ssg) continue;
|
|
45
|
+
|
|
46
|
+
const pathname = virtualPathToPathname(def.virtualPath);
|
|
47
|
+
|
|
48
|
+
/* skip dynamic routes — they need params we don't have at dev time */
|
|
49
|
+
if (pathname.includes("[")) continue;
|
|
50
|
+
|
|
51
|
+
routes.push({ mode: "static", pathname });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return routes;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface SsrEnvironment {
|
|
58
|
+
runner?: { import: (id: string) => Promise<Record<string, unknown>> };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface DevPrerenderServer {
|
|
62
|
+
config?: { root?: string };
|
|
63
|
+
environments?: Record<string, SsrEnvironment>;
|
|
64
|
+
httpServer?: { on?: (event: string, fn: () => void) => void } | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Vite plugin that pre-renders SSG routes on dev server start,
|
|
69
|
+
* writes entries to FileSystemStore, and invalidates on HMR.
|
|
70
|
+
*/
|
|
71
|
+
export function createDevPrerenderPlugin(config: FlarePluginConfig, entries: ResolvedEntries): VitePlugin {
|
|
72
|
+
const ignorePrefix = config.ignorePrefix ?? "_";
|
|
73
|
+
let store: FlareStore;
|
|
74
|
+
let root: string;
|
|
75
|
+
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
76
|
+
|
|
77
|
+
function getRoutes(): PrerenderRoute[] {
|
|
78
|
+
const defs = scanSourceFiles({ ignorePrefix, rootDir: root });
|
|
79
|
+
return filterDevPrerenderRoutes(defs);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function prerenderRoutes(routes: PrerenderRoute[], ssrRunner: SsrEnvironment["runner"]): Promise<void> {
|
|
83
|
+
if (routes.length === 0 || !ssrRunner) return;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const mod = await ssrRunner.import(`./${entries.server}`);
|
|
87
|
+
const defaultExport = mod["default"] as Record<string, unknown> | undefined;
|
|
88
|
+
const handler = (mod["handler"] ?? defaultExport?.["handler"]) as ServerHandler | undefined;
|
|
89
|
+
if (!handler) return;
|
|
90
|
+
|
|
91
|
+
const result = await prerender({
|
|
92
|
+
handler,
|
|
93
|
+
origin: "http://localhost",
|
|
94
|
+
routes,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/* Write entries to store */
|
|
98
|
+
for (const entry of result.entries) {
|
|
99
|
+
await store.set(`static:${entry.pathname}`, {
|
|
100
|
+
data: {
|
|
101
|
+
headers: entry.headers,
|
|
102
|
+
html: entry.html,
|
|
103
|
+
ndjson: entry.ndjson,
|
|
104
|
+
},
|
|
105
|
+
storedAt: Date.now(),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (result.entries.length > 0) {
|
|
110
|
+
process.stderr.write(`[flare:dev-prerender] Pre-rendered ${result.entries.length} SSG route(s)\n`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const err of result.errors) {
|
|
114
|
+
process.stderr.write(`[flare:dev-prerender] Error ${err.pathname}: ${err.message}\n`);
|
|
115
|
+
}
|
|
116
|
+
} catch (e: unknown) {
|
|
117
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
118
|
+
process.stderr.write(`[flare:dev-prerender] Failed: ${msg}\n`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
configureServer(server: unknown) {
|
|
124
|
+
const vite = server as DevPrerenderServer;
|
|
125
|
+
root = vite.config?.root ?? process.cwd();
|
|
126
|
+
store = createFileSystemStore({ cacheDir: join(root, ".flare/cache") });
|
|
127
|
+
|
|
128
|
+
const ssrEnv = vite.environments?.ssr;
|
|
129
|
+
if (!ssrEnv?.runner) return undefined;
|
|
130
|
+
|
|
131
|
+
/* Pre-render on server start */
|
|
132
|
+
vite.httpServer?.on?.("listening", () => {
|
|
133
|
+
const routes = getRoutes();
|
|
134
|
+
prerenderRoutes(routes, ssrEnv.runner).catch(() => {
|
|
135
|
+
/* swallow — logged internally */
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/* HMR invalidation — watch route files for changes */
|
|
140
|
+
const srcRoutes = join(root, "src/routes");
|
|
141
|
+
const routeWatcher = watch(srcRoutes, { recursive: true }, (_event, filename) => {
|
|
142
|
+
if (!filename) return;
|
|
143
|
+
const name = String(filename);
|
|
144
|
+
if (!name.endsWith(".tsx") && !name.endsWith(".ts")) return;
|
|
145
|
+
|
|
146
|
+
clearTimeout(debounceTimer);
|
|
147
|
+
debounceTimer = setTimeout(() => {
|
|
148
|
+
const routes = getRoutes();
|
|
149
|
+
|
|
150
|
+
/* Delete stale entries then re-prerender */
|
|
151
|
+
const deleteKeys = routes.map((r) => `static:${r.pathname}`);
|
|
152
|
+
if (store.deleteByKeys && deleteKeys.length > 0) {
|
|
153
|
+
store.deleteByKeys(deleteKeys).catch(() => {});
|
|
154
|
+
}
|
|
155
|
+
prerenderRoutes(routes, ssrEnv.runner).catch(() => {});
|
|
156
|
+
}, 200);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
vite.httpServer?.on?.("close", () => {
|
|
160
|
+
clearTimeout(debounceTimer);
|
|
161
|
+
routeWatcher.close();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return undefined;
|
|
165
|
+
},
|
|
166
|
+
name: "flare:dev-prerender",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import type { ResolvedEntries, VitePlugin } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
interface NodeReq {
|
|
6
|
+
headers: Record<string, string | string[] | undefined>;
|
|
7
|
+
method?: string;
|
|
8
|
+
on: (event: string, fn: (chunk?: unknown) => void) => void;
|
|
9
|
+
socket?: { encrypted?: boolean };
|
|
10
|
+
url?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface NodeRes {
|
|
14
|
+
end: (data?: unknown) => void;
|
|
15
|
+
on: (event: string, fn: () => void) => void;
|
|
16
|
+
writableEnded?: boolean;
|
|
17
|
+
write: (chunk: unknown) => void;
|
|
18
|
+
writeHead: (status: number, headers: Record<string, string | string[]>) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SsrEnvironment {
|
|
22
|
+
runner?: { import: (id: string) => Promise<Record<string, unknown>> };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ViteDevServer {
|
|
26
|
+
config?: { root?: string; server?: { middlewareMode?: boolean } };
|
|
27
|
+
environments?: Record<string, SsrEnvironment>;
|
|
28
|
+
middlewares: {
|
|
29
|
+
use: (fn: (req: NodeReq, res: NodeRes, next: (err?: unknown) => void) => void) => void;
|
|
30
|
+
};
|
|
31
|
+
ssrFixStacktrace: (e: Error) => void;
|
|
32
|
+
transformIndexHtml: (url: string, html: string) => Promise<string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nodeToWebRequest(req: NodeReq, url: URL): Request {
|
|
36
|
+
const headers = new Headers();
|
|
37
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
38
|
+
/* skip HTTP/2 pseudo-headers (e.g. :method, :path, :scheme) */
|
|
39
|
+
if (key.startsWith(":")) continue;
|
|
40
|
+
if (value) {
|
|
41
|
+
const values = Array.isArray(value) ? value : [value];
|
|
42
|
+
for (const v of values) {
|
|
43
|
+
headers.append(key, v);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
49
|
+
const body = hasBody
|
|
50
|
+
? new ReadableStream({
|
|
51
|
+
start(controller) {
|
|
52
|
+
req.on("data", (chunk: unknown) => {
|
|
53
|
+
controller.enqueue(chunk);
|
|
54
|
+
});
|
|
55
|
+
req.on("end", () => {
|
|
56
|
+
controller.close();
|
|
57
|
+
});
|
|
58
|
+
req.on("error", (err: unknown) => {
|
|
59
|
+
controller.error(err);
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
: undefined;
|
|
64
|
+
|
|
65
|
+
return new Request(url.href, {
|
|
66
|
+
body: body ?? null,
|
|
67
|
+
duplex: hasBody ? "half" : undefined,
|
|
68
|
+
headers,
|
|
69
|
+
method: req.method ?? "GET",
|
|
70
|
+
} as RequestInit);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function streamResponse(response: Response, res: NodeRes): Promise<void> {
|
|
74
|
+
const responseHeaders: Record<string, string | string[]> = {};
|
|
75
|
+
/* Set-Cookie must be sent as separate headers — Node.js writeHead
|
|
76
|
+
* accepts string[] for multi-value headers like Set-Cookie */
|
|
77
|
+
const setCookies =
|
|
78
|
+
"getSetCookie" in response.headers
|
|
79
|
+
? (response.headers as Headers & { getSetCookie(): string[] }).getSetCookie()
|
|
80
|
+
: [];
|
|
81
|
+
response.headers.forEach((value, key) => {
|
|
82
|
+
if (key === "set-cookie") return;
|
|
83
|
+
responseHeaders[key] = value;
|
|
84
|
+
});
|
|
85
|
+
if (setCookies.length > 0) {
|
|
86
|
+
responseHeaders["set-cookie"] = setCookies;
|
|
87
|
+
}
|
|
88
|
+
res.writeHead(response.status, responseHeaders);
|
|
89
|
+
|
|
90
|
+
if (response.body) {
|
|
91
|
+
const reader = response.body.getReader();
|
|
92
|
+
const pump = async (): Promise<void> => {
|
|
93
|
+
const { done, value } = await reader.read();
|
|
94
|
+
if (done) {
|
|
95
|
+
res.end();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
res.write(value);
|
|
99
|
+
return pump();
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
await pump();
|
|
103
|
+
} finally {
|
|
104
|
+
reader.cancel().catch(() => {});
|
|
105
|
+
if (!res.writableEnded) res.end();
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
res.end();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const INLINE_PLACEHOLDER = (i: number) => `<!--flare-inline-${i}-->`;
|
|
113
|
+
const INLINE_PLACEHOLDER_RE = /<!--flare-inline-(\d+)-->/g;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Vite's transformIndexHtml virtualises every inline <script>/<style> as
|
|
117
|
+
* `?html-proxy` modules. Theme IIFEs must stay blocking, and the body
|
|
118
|
+
* `import(entry)` must stay a real inline module — a NUL-prefixed virtual
|
|
119
|
+
* id 500s and hydrate never starts. Peel, transform, restore in place.
|
|
120
|
+
*/
|
|
121
|
+
export function peelInlineTags(html: string): { html: string; tags: string[] } {
|
|
122
|
+
const tags: string[] = [];
|
|
123
|
+
const next = html.replace(/<(script|style)(\s[^>]*)?>([\s\S]*?)<\/\1>/gi, (full, tag: string, attrs = "") => {
|
|
124
|
+
if (tag.toLowerCase() === "script" && /\ssrc\s*=/i.test(attrs)) return full;
|
|
125
|
+
const i = tags.length;
|
|
126
|
+
tags.push(full);
|
|
127
|
+
return INLINE_PLACEHOLDER(i);
|
|
128
|
+
});
|
|
129
|
+
return { html: next, tags };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function restoreInlineTags(html: string, tags: string[]): string {
|
|
133
|
+
if (tags.length === 0) return html;
|
|
134
|
+
return html.replace(INLINE_PLACEHOLDER_RE, (_, n) => tags[Number(n)] ?? "");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createDevServerPlugin(entries: ResolvedEntries, _assetsBase: string = "/assets"): VitePlugin {
|
|
138
|
+
return {
|
|
139
|
+
configureServer(server: unknown) {
|
|
140
|
+
const vite = server as ViteDevServer;
|
|
141
|
+
|
|
142
|
+
/* post-phase callback — runs AFTER Vite's built-in middleware */
|
|
143
|
+
return () => {
|
|
144
|
+
const ssrEnv = vite.environments?.ssr;
|
|
145
|
+
|
|
146
|
+
/* Auto-skip: middlewareMode means external HTTP server owns routing */
|
|
147
|
+
if (vite.config?.server?.middlewareMode) return;
|
|
148
|
+
|
|
149
|
+
/* Auto-skip: no SSR environment available */
|
|
150
|
+
if (!ssrEnv) return;
|
|
151
|
+
|
|
152
|
+
/*
|
|
153
|
+
* Auto-skip: platform plugin (CF, Nitro) provides its own SSR handler.
|
|
154
|
+
* Duck-type check — do not use isFetchableDevEnvironment() since Nitro
|
|
155
|
+
* implements the interface duck-style, not via class inheritance.
|
|
156
|
+
*/
|
|
157
|
+
if ("dispatchFetch" in ssrEnv) return;
|
|
158
|
+
|
|
159
|
+
/* Auto-skip: @cloudflare/vite-plugin owns request dispatch via its own middleware
|
|
160
|
+
(which delegates to miniflare with full env binding). It does not advertise
|
|
161
|
+
`dispatchFetch` on the ssrEnv, so the duck-type check above misses it. Detect
|
|
162
|
+
the plugin by name and yield to it. Without this, Flare's dev-server intercepts
|
|
163
|
+
first (lower middleware priority depending on plugin order) and calls
|
|
164
|
+
`handler.fetch(request)` without `env`, breaking service bindings. */
|
|
165
|
+
const plugins = (vite.config as { plugins?: { name?: string }[] }).plugins;
|
|
166
|
+
const cfPluginPresent =
|
|
167
|
+
Array.isArray(plugins) &&
|
|
168
|
+
plugins.some((p) => typeof p?.name === "string" && p.name.startsWith("vite-plugin-cloudflare"));
|
|
169
|
+
if (cfPluginPresent) return;
|
|
170
|
+
|
|
171
|
+
vite.middlewares.use(async (req, res, next) => {
|
|
172
|
+
if (!req.url) return next();
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const mod = ssrEnv.runner ? await ssrEnv.runner.import(`./${entries.server}`) : undefined;
|
|
176
|
+
|
|
177
|
+
if (!mod) return next();
|
|
178
|
+
|
|
179
|
+
const handler = (mod.server ?? mod.handler ?? mod.default) as {
|
|
180
|
+
fetch: (request: Request) => Promise<Response>;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/* HTTP/2 uses :authority instead of host header */
|
|
184
|
+
const url = new URL(
|
|
185
|
+
req.url,
|
|
186
|
+
`${req.socket?.encrypted ? "https" : "http"}://${req.headers.host ?? req.headers[":authority"] ?? "localhost"}`,
|
|
187
|
+
);
|
|
188
|
+
const webReq = nodeToWebRequest(req, url);
|
|
189
|
+
const response = await handler.fetch(webReq);
|
|
190
|
+
|
|
191
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
192
|
+
if (contentType.includes("text/html")) {
|
|
193
|
+
let html = await response.text();
|
|
194
|
+
/*
|
|
195
|
+
* Sanitize pathname for transformIndexHtml: Vite creates virtual
|
|
196
|
+
* module IDs for inline <script> tags based on the URL pathname.
|
|
197
|
+
* If the path has a file extension (e.g. /foo.json), Vite's
|
|
198
|
+
* built-in plugins (vite:json, vite:css) match the extension
|
|
199
|
+
* and try to parse the JS as JSON/CSS, causing a 500.
|
|
200
|
+
*/
|
|
201
|
+
const lastSlash = url.pathname.lastIndexOf("/");
|
|
202
|
+
const hasFileExt = url.pathname.indexOf(".", lastSlash) !== -1;
|
|
203
|
+
const { html: peeledHtml, tags } = peelInlineTags(html);
|
|
204
|
+
html = restoreInlineTags(
|
|
205
|
+
await vite.transformIndexHtml(hasFileExt ? "/" : url.pathname, peeledHtml),
|
|
206
|
+
tags,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const htmlHeaders: Record<string, string | string[]> = {
|
|
210
|
+
"content-type": "text/html; charset=utf-8",
|
|
211
|
+
};
|
|
212
|
+
const htmlSetCookies =
|
|
213
|
+
"getSetCookie" in response.headers
|
|
214
|
+
? (response.headers as Headers & { getSetCookie(): string[] }).getSetCookie()
|
|
215
|
+
: [];
|
|
216
|
+
response.headers.forEach((value, key) => {
|
|
217
|
+
if (key === "content-type" || key === "content-length" || key === "set-cookie") return;
|
|
218
|
+
htmlHeaders[key] = value;
|
|
219
|
+
});
|
|
220
|
+
if (htmlSetCookies.length > 0) {
|
|
221
|
+
htmlHeaders["set-cookie"] = htmlSetCookies;
|
|
222
|
+
}
|
|
223
|
+
res.writeHead(response.status, htmlHeaders);
|
|
224
|
+
res.end(html);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await streamResponse(response, res);
|
|
229
|
+
} catch (e: unknown) {
|
|
230
|
+
if (e instanceof Error) {
|
|
231
|
+
vite.ssrFixStacktrace(e);
|
|
232
|
+
}
|
|
233
|
+
next(e);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
name: "flare:dev-server",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* ── Preview Server ──────────────────────────────────────────────────── */
|
|
243
|
+
|
|
244
|
+
interface PreviewServer {
|
|
245
|
+
config?: { root?: string };
|
|
246
|
+
middlewares: {
|
|
247
|
+
use: (fn: (req: NodeReq, res: NodeRes, next: (err?: unknown) => void) => void) => void;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function createPreviewServerPlugin(assetsBase: string = "/assets"): VitePlugin {
|
|
252
|
+
return {
|
|
253
|
+
configurePreviewServer(server: unknown) {
|
|
254
|
+
const preview = server as PreviewServer;
|
|
255
|
+
const root = preview.config?.root ?? process.cwd();
|
|
256
|
+
const serverPath = join(root, "dist/server/server.js");
|
|
257
|
+
const clientDir = join(root, "dist/client");
|
|
258
|
+
|
|
259
|
+
let handlerPromise: Promise<{ fetch: (request: Request) => Promise<Response> }> | undefined;
|
|
260
|
+
|
|
261
|
+
function getHandler() {
|
|
262
|
+
if (!handlerPromise) {
|
|
263
|
+
handlerPromise = import(`${serverPath}?t=${Date.now()}`).then(
|
|
264
|
+
(mod) =>
|
|
265
|
+
(mod.server ?? mod.handler) as {
|
|
266
|
+
fetch: (request: Request) => Promise<Response>;
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
/* Clear on failure so next request retries instead of caching the rejection */
|
|
270
|
+
handlerPromise.catch((e: unknown) => {
|
|
271
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
272
|
+
process.stderr.write(`[flare:preview] handler import failed, will retry: ${msg}\n`);
|
|
273
|
+
handlerPromise = undefined;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return handlerPromise;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return () => {
|
|
280
|
+
preview.middlewares.use(async (req, res, next) => {
|
|
281
|
+
if (!req.url) return next();
|
|
282
|
+
|
|
283
|
+
/* Serve static client assets */
|
|
284
|
+
const urlPath = req.url.split("?")[0];
|
|
285
|
+
if (urlPath?.startsWith(`${assetsBase}/`)) {
|
|
286
|
+
const filePath = resolve(join(clientDir, urlPath));
|
|
287
|
+
const clientDirSlash = clientDir.endsWith("/") ? clientDir : `${clientDir}/`;
|
|
288
|
+
if (!filePath.startsWith(clientDirSlash)) return next();
|
|
289
|
+
try {
|
|
290
|
+
const content = readFileSync(filePath);
|
|
291
|
+
const ext = filePath.split(".").pop() ?? "";
|
|
292
|
+
const mimeTypes: Record<string, string> = {
|
|
293
|
+
css: "text/css",
|
|
294
|
+
js: "application/javascript",
|
|
295
|
+
json: "application/json",
|
|
296
|
+
svg: "image/svg+xml",
|
|
297
|
+
};
|
|
298
|
+
res.writeHead(200, {
|
|
299
|
+
"cache-control": "public, max-age=31536000, immutable",
|
|
300
|
+
"content-type": mimeTypes[ext] ?? "application/octet-stream",
|
|
301
|
+
});
|
|
302
|
+
res.end(content);
|
|
303
|
+
return;
|
|
304
|
+
} catch {
|
|
305
|
+
/* fall through to SSR */
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const handler = await getHandler();
|
|
311
|
+
/* HTTP/2 uses :authority instead of host header */
|
|
312
|
+
const url = new URL(
|
|
313
|
+
req.url,
|
|
314
|
+
`${req.socket?.encrypted ? "https" : "http"}://${req.headers.host ?? req.headers[":authority"] ?? "localhost"}`,
|
|
315
|
+
);
|
|
316
|
+
const webReq = nodeToWebRequest(req, url);
|
|
317
|
+
const response = await handler.fetch(webReq);
|
|
318
|
+
await streamResponse(response, res);
|
|
319
|
+
} catch (e: unknown) {
|
|
320
|
+
next(e);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
name: "flare:preview-server",
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { runGenerate, type RunGenerateOptions } from "../generators/index.ts";
|
|
2
|
+
|
|
3
|
+
const GEN_IGNORE_RE = /(_gen[/\\]|\.gen\.tsx?$)/;
|
|
4
|
+
|
|
5
|
+
export const FS_CODEGEN_SUFFIX_RE = /\.(page|layout|root-layout|path-segment)\.(tsx?|jsx?)$/;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether a filesystem watch event should re-run codegen.
|
|
9
|
+
* `fsVirtualPaths: true` only reacts to suffix route files, or add/delete (`rename`).
|
|
10
|
+
* `false` reacts to any non-generated source change.
|
|
11
|
+
*/
|
|
12
|
+
export function shouldTriggerGenerate(
|
|
13
|
+
filename: string | null | undefined,
|
|
14
|
+
event: string,
|
|
15
|
+
fsVirtualPaths: boolean,
|
|
16
|
+
): boolean {
|
|
17
|
+
if (!filename) return false;
|
|
18
|
+
const name = String(filename);
|
|
19
|
+
if (GEN_IGNORE_RE.test(name)) return false;
|
|
20
|
+
if (fsVirtualPaths && event !== "rename" && !FS_CODEGEN_SUFFIX_RE.test(name)) return false;
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Watch-path generate: log layout errors instead of taking down the dev server. */
|
|
25
|
+
export function safeRunGenerate(options: RunGenerateOptions): void {
|
|
26
|
+
try {
|
|
27
|
+
runGenerate(options);
|
|
28
|
+
} catch (e: unknown) {
|
|
29
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
30
|
+
console.error(`[flare:generate] ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, extname, resolve } from "node:path";
|
|
4
|
+
import type { VitePlugin } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
const IMAGE_EXTS = new Set([".avif", ".gif", ".jpeg", ".jpg", ".png", ".tiff", ".webp"]);
|
|
7
|
+
const ANIMATED_EXTS = new Set([".gif"]);
|
|
8
|
+
const IMAGE_DEFAULT_WIDTHS = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
9
|
+
const IMAGE_DEFAULT_QUALITY = 75;
|
|
10
|
+
|
|
11
|
+
interface SharpInstance {
|
|
12
|
+
metadata: () => Promise<{ height?: number; width?: number }>;
|
|
13
|
+
resize: (width: number) => SharpInstance;
|
|
14
|
+
toBuffer: () => Promise<Buffer>;
|
|
15
|
+
webp: (opts?: { quality?: number }) => SharpInstance;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type SharpFactory = (input: string) => SharpInstance;
|
|
19
|
+
|
|
20
|
+
async function importSharp(): Promise<SharpFactory> {
|
|
21
|
+
try {
|
|
22
|
+
const mod = await import("sharp");
|
|
23
|
+
return (mod.default ?? mod) as unknown as SharpFactory;
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error("sharp is required for image optimization. Install it: bun add sharp -D");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ImagePluginContext {
|
|
30
|
+
emitFile?: (file: { fileName: string; source: Buffer; type: "asset" }) => void;
|
|
31
|
+
environment?: { config?: { mode?: string } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface NodeReq {
|
|
35
|
+
url?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface NodeRes {
|
|
39
|
+
end: (data?: unknown) => void;
|
|
40
|
+
writeHead: (status: number, headers: Record<string, string | string[]>) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createImagePlugin(
|
|
44
|
+
config?: { image?: { exclude?: RegExp; quality?: number; widths?: number[] } },
|
|
45
|
+
assetsBase: string = "/assets",
|
|
46
|
+
assetsDir: string = "assets",
|
|
47
|
+
): VitePlugin {
|
|
48
|
+
const quality = config?.image?.quality ?? IMAGE_DEFAULT_QUALITY;
|
|
49
|
+
const configWidths = config?.image?.widths ?? IMAGE_DEFAULT_WIDTHS;
|
|
50
|
+
const excludeRe = config?.image?.exclude;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
configureServer(server: unknown) {
|
|
54
|
+
const srv = server as {
|
|
55
|
+
middlewares?: {
|
|
56
|
+
use: (fn: (req: NodeReq, res: NodeRes, next: (err?: unknown) => void) => void) => void;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
srv.middlewares?.use(async (req, res, next) => {
|
|
61
|
+
if (!req.url?.startsWith("/_flare/image?")) return next();
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(req.url, "http://localhost");
|
|
65
|
+
const src = url.searchParams.get("src");
|
|
66
|
+
const w = Number(url.searchParams.get("w"));
|
|
67
|
+
if (!src || !w) return next();
|
|
68
|
+
|
|
69
|
+
/* Guard against path traversal — resolve symlinks then check within cwd */
|
|
70
|
+
const resolvedSrc = resolve(src);
|
|
71
|
+
if (!resolvedSrc.startsWith(process.cwd())) return next();
|
|
72
|
+
let realSrc: string;
|
|
73
|
+
try {
|
|
74
|
+
realSrc = realpathSync(resolvedSrc);
|
|
75
|
+
} catch {
|
|
76
|
+
return next();
|
|
77
|
+
}
|
|
78
|
+
if (!realSrc.startsWith(process.cwd())) return next();
|
|
79
|
+
|
|
80
|
+
const sharp = await importSharp();
|
|
81
|
+
const sourceExt = extname(realSrc).toLowerCase();
|
|
82
|
+
const isWebPSource = sourceExt === ".webp";
|
|
83
|
+
const buf = isWebPSource
|
|
84
|
+
? await sharp(realSrc).resize(w).toBuffer()
|
|
85
|
+
: await sharp(realSrc).resize(w).webp({ quality }).toBuffer();
|
|
86
|
+
|
|
87
|
+
res.writeHead(200, {
|
|
88
|
+
"cache-control": "public, max-age=31536000, immutable",
|
|
89
|
+
"content-type": "image/webp",
|
|
90
|
+
});
|
|
91
|
+
res.end(buf);
|
|
92
|
+
} catch (e: unknown) {
|
|
93
|
+
next(e);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return undefined;
|
|
97
|
+
},
|
|
98
|
+
enforce: "pre",
|
|
99
|
+
|
|
100
|
+
async load(this: ImagePluginContext, id: string): Promise<{ code: string; moduleType: string } | null> {
|
|
101
|
+
if (!id.startsWith("\0flare-image:")) return null;
|
|
102
|
+
const imagePath = id.slice("\0flare-image:".length);
|
|
103
|
+
|
|
104
|
+
const sharp = await importSharp();
|
|
105
|
+
const img = sharp(imagePath);
|
|
106
|
+
const metadata = await img.metadata();
|
|
107
|
+
const width = metadata.width ?? 0;
|
|
108
|
+
const height = metadata.height ?? 0;
|
|
109
|
+
|
|
110
|
+
/* Blur placeholder: 8px wide WebP → base64 data URI */
|
|
111
|
+
const blurBuf = await sharp(imagePath).resize(8).webp({ quality: 20 }).toBuffer();
|
|
112
|
+
const blurDataURL = `data:image/webp;base64,${blurBuf.toString("base64")}`;
|
|
113
|
+
|
|
114
|
+
const isBuild = this.environment?.config?.mode !== "development";
|
|
115
|
+
const stem = basename(imagePath, extname(imagePath));
|
|
116
|
+
|
|
117
|
+
/* Compute variant widths — only ≤ original, always include original */
|
|
118
|
+
const widths = configWidths.filter((w) => w <= width);
|
|
119
|
+
if (!widths.includes(width)) widths.push(width);
|
|
120
|
+
widths.sort((a, b) => a - b);
|
|
121
|
+
|
|
122
|
+
if (isBuild) {
|
|
123
|
+
const variants: Record<number, string> = {};
|
|
124
|
+
for (const w of widths) {
|
|
125
|
+
const buf = await sharp(imagePath).resize(w).webp({ quality }).toBuffer();
|
|
126
|
+
const hash = createHash("sha256").update(buf).digest("hex").slice(0, 8);
|
|
127
|
+
const filename = `${stem}-${w}-${hash}.webp`;
|
|
128
|
+
const emitPath = assetsDir ? `${assetsDir}/${filename}` : filename;
|
|
129
|
+
this.emitFile?.({ fileName: emitPath, source: buf, type: "asset" });
|
|
130
|
+
variants[w] = `${assetsBase}/${filename}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const data = { blurDataURL, height, src: variants[width] ?? "", variants, width };
|
|
134
|
+
return { code: `export default ${JSON.stringify(data)}`, moduleType: "js" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Dev mode: variants served on-demand via middleware */
|
|
138
|
+
const variants: Record<number, string> = {};
|
|
139
|
+
for (const w of widths) {
|
|
140
|
+
variants[w] = `/_flare/image?src=${encodeURIComponent(imagePath)}&w=${w}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const data = { blurDataURL, height, src: imagePath, variants, width };
|
|
144
|
+
return { code: `export default ${JSON.stringify(data)}`, moduleType: "js" };
|
|
145
|
+
},
|
|
146
|
+
name: "flare:image",
|
|
147
|
+
|
|
148
|
+
resolveId(id: string, importer?: unknown): string | null {
|
|
149
|
+
/* Explicit Vite passthrough */
|
|
150
|
+
if (id.includes("?url") || id.includes("?raw")) return null;
|
|
151
|
+
|
|
152
|
+
const ext = extname(id).toLowerCase();
|
|
153
|
+
|
|
154
|
+
/* SVG and animated formats: always pass through */
|
|
155
|
+
if (ext === ".svg" || ANIMATED_EXTS.has(ext)) return null;
|
|
156
|
+
|
|
157
|
+
if (!IMAGE_EXTS.has(ext)) return null;
|
|
158
|
+
|
|
159
|
+
/* Resolve relative paths using the importer directory */
|
|
160
|
+
let absolutePath = id;
|
|
161
|
+
if (id.startsWith(".") && typeof importer === "string") {
|
|
162
|
+
absolutePath = resolve(dirname(importer), id);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Check exclude regex against resolved path */
|
|
166
|
+
if (excludeRe?.test(absolutePath)) return null;
|
|
167
|
+
|
|
168
|
+
return `\0flare-image:${absolutePath}`;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|