@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,150 @@
|
|
|
1
|
+
/** Manifest emitted by the sx-ast Vite plugin at build time. */
|
|
2
|
+
export interface SxCssManifest {
|
|
3
|
+
version: 1;
|
|
4
|
+
hashVersion: string;
|
|
5
|
+
/** className → full CSS rule text (e.g. `.a1-abc12345 { color: red }`) */
|
|
6
|
+
rules: Record<string, string>;
|
|
7
|
+
/** Which layer each class belongs to. */
|
|
8
|
+
layerByRule: Record<string, "sx" | "app">;
|
|
9
|
+
/** moduleId → class names that module emits. Covers Show/Switch/handler branches. */
|
|
10
|
+
moduleManifest: Record<string, string[]>;
|
|
11
|
+
/** Final asset URL with Vite content hash. */
|
|
12
|
+
bundleHref: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CriticalCssResult {
|
|
16
|
+
css: string;
|
|
17
|
+
bundleHref?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const CRITICAL_SHEET_ID = "flare-critical";
|
|
21
|
+
|
|
22
|
+
/* Prod: a1-<8 base-36 chars>. Dev: sx-<prop>-<val>-<4 base-36 chars>. */
|
|
23
|
+
const ATOMIC_CLASS_RE = /\bclass="([^"]*)"/g;
|
|
24
|
+
const PROD_CLASS_RE = /^a1-[a-z0-9]{8}$/;
|
|
25
|
+
const DEV_CLASS_RE = /^sx-/;
|
|
26
|
+
|
|
27
|
+
function isAtomicClass(cls: string, manifestKeys?: Set<string>): boolean {
|
|
28
|
+
/* Manifest keys are the ground truth — raw Tailwind tokens (e.g. bg-accent) are valid
|
|
29
|
+
* when the sx plugin emitted a rule for them (pass-through / non-hashed mode). */
|
|
30
|
+
if (manifestKeys?.has(cls)) return true;
|
|
31
|
+
return PROD_CLASS_RE.test(cls) || DEV_CLASS_RE.test(cls);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Scan rendered HTML and return the set of atomic/manifest class names present. */
|
|
35
|
+
export function collectAtomicClasses(html: string, manifest?: SxCssManifest): Set<string> {
|
|
36
|
+
const manifestKeys = manifest ? new Set(Object.keys(manifest.rules)) : undefined;
|
|
37
|
+
const found = new Set<string>();
|
|
38
|
+
ATOMIC_CLASS_RE.lastIndex = 0;
|
|
39
|
+
let m: RegExpExecArray | null;
|
|
40
|
+
while ((m = ATOMIC_CLASS_RE.exec(html)) !== null) {
|
|
41
|
+
for (const cls of m[1].split(/\s+/)) {
|
|
42
|
+
if (cls && isAtomicClass(cls, manifestKeys)) found.add(cls);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute critical CSS from rendered HTML + module manifest.
|
|
50
|
+
*
|
|
51
|
+
* Returns empty string when manifest absent — caller skips injection.
|
|
52
|
+
*/
|
|
53
|
+
export function buildCriticalCss(
|
|
54
|
+
html: string,
|
|
55
|
+
renderedModules: string[],
|
|
56
|
+
manifest: SxCssManifest | undefined,
|
|
57
|
+
): CriticalCssResult {
|
|
58
|
+
if (!manifest) return { css: "" };
|
|
59
|
+
|
|
60
|
+
/* Union: classes from HTML + classes from module-manifest for loaded modules */
|
|
61
|
+
const classes = collectAtomicClasses(html, manifest);
|
|
62
|
+
for (const modId of renderedModules) {
|
|
63
|
+
const modClasses = manifest.moduleManifest[modId];
|
|
64
|
+
if (modClasses) {
|
|
65
|
+
for (const cls of modClasses) classes.add(cls);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sxRules: string[] = [];
|
|
70
|
+
const appRules: string[] = [];
|
|
71
|
+
|
|
72
|
+
for (const cls of classes) {
|
|
73
|
+
const rule = manifest.rules[cls];
|
|
74
|
+
if (!rule) continue;
|
|
75
|
+
|
|
76
|
+
const layer = manifest.layerByRule[cls] ?? "app";
|
|
77
|
+
if (layer === "sx") sxRules.push(rule);
|
|
78
|
+
else appRules.push(rule);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (sxRules.length === 0 && appRules.length === 0) {
|
|
82
|
+
return { bundleHref: manifest.bundleHref, css: "" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const parts: string[] = [];
|
|
86
|
+
/* Declare Tailwind's implicit layer order AND flare's sx/app layers before rules so
|
|
87
|
+
* browser positions `app` AFTER `base` (bundle registers same names later — same
|
|
88
|
+
* positions reused). Without these preludes the first `@layer app { }` encountered
|
|
89
|
+
* creates `app` at position 1 (lowest), causing base's `h1{font-size:inherit}` to
|
|
90
|
+
* beat `.text-4xl` and collapse typography. */
|
|
91
|
+
parts.push("@layer theme, base, components, utilities;");
|
|
92
|
+
parts.push("@layer reset, sx, app, user.lib, user.app, inline;");
|
|
93
|
+
if (sxRules.length > 0) parts.push(`@layer sx { ${sxRules.join(" ")} }`);
|
|
94
|
+
if (appRules.length > 0) parts.push(`@layer app { ${appRules.join(" ")} }`);
|
|
95
|
+
|
|
96
|
+
return { bundleHref: manifest.bundleHref, css: parts.join("\n") };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Inject empty critical-CSS placeholder + optional preload link before `</head>`.
|
|
101
|
+
* Called during head-injection phase (before body is known).
|
|
102
|
+
*/
|
|
103
|
+
export function injectCriticalPlaceholder(html: string, nonce: string, bundleHref?: string): string {
|
|
104
|
+
if (!html.includes("</head>")) return html;
|
|
105
|
+
|
|
106
|
+
const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
|
|
107
|
+
let inject = `<style id="${CRITICAL_SHEET_ID}"${nonceAttr}></style>`;
|
|
108
|
+
|
|
109
|
+
if (bundleHref) {
|
|
110
|
+
const escapedHref = bundleHref.replace(/&/g, "&").replace(/"/g, """);
|
|
111
|
+
inject += `<link rel="preload" as="style" href="${escapedHref}" onload="this.rel='stylesheet'"${nonceAttr}/>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return html.replace("</head>", `${inject}</head>`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Scan body HTML, compute critical CSS, fill the `<style id="flare-critical">`
|
|
119
|
+
* placeholder in `<head>` (emitted upstream by injectHeadContent). Populating the
|
|
120
|
+
* head placeholder keeps the critical sheet outside Solid's render tree; emitting
|
|
121
|
+
* a fresh `<style>` near `</body>` left the tag as an unowned sibling that Solid's
|
|
122
|
+
* post-hydrate cleanup disposed during SPA nav, stripping utilities off the page.
|
|
123
|
+
*/
|
|
124
|
+
export function injectCriticalAppend(
|
|
125
|
+
html: string,
|
|
126
|
+
renderedModules: string[],
|
|
127
|
+
manifest: SxCssManifest | undefined,
|
|
128
|
+
nonce: string,
|
|
129
|
+
): string {
|
|
130
|
+
if (!manifest) return html;
|
|
131
|
+
|
|
132
|
+
const { css } = buildCriticalCss(html, renderedModules, manifest);
|
|
133
|
+
if (!css) return html;
|
|
134
|
+
|
|
135
|
+
/* Escape </style> inside CSS to prevent tag injection */
|
|
136
|
+
const safeCSS = css.replace(/<\/style\b/gi, "<\\/style");
|
|
137
|
+
|
|
138
|
+
/* Fill the existing empty `<style id="flare-critical">…</style>` placeholder. */
|
|
139
|
+
const filled = html.replace(
|
|
140
|
+
/(<style id="flare-critical"[^>]*>)<\/style>/,
|
|
141
|
+
(_match, open) => `${open}${safeCSS}</style>`,
|
|
142
|
+
);
|
|
143
|
+
if (filled !== html) return filled;
|
|
144
|
+
|
|
145
|
+
/* Fallback: placeholder missing (non-standard head injection path) — keep the
|
|
146
|
+
* old behaviour of appending before `</body>` so we don't silently drop styles. */
|
|
147
|
+
const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
|
|
148
|
+
const tag = `<style data-flare-critical-append${nonceAttr}>${safeCSS}</style>`;
|
|
149
|
+
return html.replace("</body>", `${tag}</body>`);
|
|
150
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getDirectionScript, type DirectionConfig } from "../direction/index.tsx";
|
|
2
|
+
import { getLocaleScript, type LocaleConfig } from "../locale/index.tsx";
|
|
3
|
+
import type { ModulePreloads } from "../module-graph/index.ts";
|
|
4
|
+
import type { HeadConfig } from "../route-builder/types.ts";
|
|
5
|
+
import { getThemeScript, type ThemeConfig } from "../theme/index.tsx";
|
|
6
|
+
|
|
7
|
+
function escapeAttr(str: string): string {
|
|
8
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function colorSchemeCss(attribute: string): string {
|
|
12
|
+
const attr = attribute.replace(/[^\w-]/g, "") || "data-theme";
|
|
13
|
+
return `html{color-scheme:light}html[${attr}=dark]{color-scheme:dark}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface HeadPrefixOptions {
|
|
17
|
+
direction?: DirectionConfig;
|
|
18
|
+
locale?: LocaleConfig;
|
|
19
|
+
modulePreloads?: ModulePreloads;
|
|
20
|
+
nonce: string;
|
|
21
|
+
resolvedHead: HeadConfig;
|
|
22
|
+
theme?: ThemeConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Blocking head prefix. Order is first-paint critical:
|
|
27
|
+
* 1. CSP nonce meta
|
|
28
|
+
* 2. viewport (if the app did not set one)
|
|
29
|
+
* 3. theme / direction / locale inline scripts — before any CSS or modulepreload
|
|
30
|
+
* 4. modulepreload + stylesheets
|
|
31
|
+
*
|
|
32
|
+
* Theme must run before stylesheets or first paint is unthemed until refresh
|
|
33
|
+
* (CSS cache + later ThemeProvider). Modulepreloads stay after the scripts so
|
|
34
|
+
* Chrome can still mark them isLinkPreload when CSP has no 'strict-dynamic'.
|
|
35
|
+
*/
|
|
36
|
+
export function buildHeadPrefix(options: HeadPrefixOptions): string {
|
|
37
|
+
const escapedNonce = escapeAttr(options.nonce);
|
|
38
|
+
let prefix = `<meta name="csp-nonce" nonce="${escapedNonce}">`;
|
|
39
|
+
if (options.resolvedHead.meta?.viewport === undefined) {
|
|
40
|
+
prefix += `<meta name="viewport" content="width=device-width, initial-scale=1">`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const nonceAttr = ` nonce="${escapedNonce}"`;
|
|
44
|
+
const themeAttr = options.theme?.attribute ?? "data-theme";
|
|
45
|
+
prefix += `<script${nonceAttr}>${getThemeScript(options.theme)}</script>`;
|
|
46
|
+
prefix += `<style${nonceAttr}>${colorSchemeCss(themeAttr)}</style>`;
|
|
47
|
+
|
|
48
|
+
if (options.direction) {
|
|
49
|
+
prefix += `<script${nonceAttr}>${getDirectionScript(options.direction)}</script>`;
|
|
50
|
+
}
|
|
51
|
+
if (options.locale) {
|
|
52
|
+
prefix += `<script${nonceAttr}>${getLocaleScript(options.locale)}</script>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (options.modulePreloads) {
|
|
56
|
+
for (const href of options.modulePreloads.js) {
|
|
57
|
+
prefix += `<link rel="modulepreload" href="${escapeAttr(href)}"/>`;
|
|
58
|
+
}
|
|
59
|
+
for (const href of options.modulePreloads.css) {
|
|
60
|
+
prefix += `<link rel="stylesheet" href="${escapeAttr(href)}"/>`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return prefix;
|
|
65
|
+
}
|
package/src/ssr/head.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { HeadConfig, SeoImage } from "../route-builder/types.ts";
|
|
2
|
+
|
|
3
|
+
function escapeHtml(str: string): string {
|
|
4
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function escapeAttr(str: string): string {
|
|
8
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const VALID_ATTR_RE = /^[a-zA-Z][a-zA-Z0-9-]*$/;
|
|
12
|
+
|
|
13
|
+
function isSafeAttrName(name: string): boolean {
|
|
14
|
+
if (!VALID_ATTR_RE.test(name)) return false;
|
|
15
|
+
if (name.length > 2 && name[0] === "o" && name[1] === "n") return false;
|
|
16
|
+
if (name.length > 2 && name[0] === "O" && name[1] === "N") return false;
|
|
17
|
+
return !name.toLowerCase().startsWith("on");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderOgImage(parts: string[], img: SeoImage): void {
|
|
21
|
+
parts.push(`<meta property="og:image" content="${escapeAttr(img.url)}">`);
|
|
22
|
+
if (img.width) parts.push(`<meta property="og:image:width" content="${img.width}">`);
|
|
23
|
+
if (img.height) parts.push(`<meta property="og:image:height" content="${img.height}">`);
|
|
24
|
+
if (img.type) parts.push(`<meta property="og:image:type" content="${escapeAttr(img.type)}">`);
|
|
25
|
+
if (img.alt) parts.push(`<meta property="og:image:alt" content="${escapeAttr(img.alt)}">`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function renderHeadToHtml(head: HeadConfig, nonce: string): string {
|
|
29
|
+
const parts: string[] = [];
|
|
30
|
+
|
|
31
|
+
if (head.title !== undefined) {
|
|
32
|
+
parts.push(`<title>${escapeHtml(head.title)}</title>`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (head.description !== undefined) {
|
|
36
|
+
parts.push(`<meta name="description" content="${escapeAttr(head.description)}">`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (head.canonical !== undefined) {
|
|
40
|
+
parts.push(`<link rel="canonical" href="${escapeAttr(head.canonical)}">`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (head.keywords !== undefined) {
|
|
44
|
+
parts.push(`<meta name="keywords" content="${escapeAttr(head.keywords)}">`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (head.robots) {
|
|
48
|
+
const directives: string[] = [];
|
|
49
|
+
const r = head.robots;
|
|
50
|
+
if (r.index === true) directives.push("index");
|
|
51
|
+
if (r.index === false) directives.push("noindex");
|
|
52
|
+
if (r.follow === true) directives.push("follow");
|
|
53
|
+
if (r.follow === false) directives.push("nofollow");
|
|
54
|
+
if (r.noarchive === true) directives.push("noarchive");
|
|
55
|
+
if (r.noimageindex === true) directives.push("noimageindex");
|
|
56
|
+
if (r["max-snippet"] !== undefined) directives.push(`max-snippet:${r["max-snippet"]}`);
|
|
57
|
+
if (r["max-image-preview"] !== undefined) directives.push(`max-image-preview:${r["max-image-preview"]}`);
|
|
58
|
+
if (r["max-video-preview"] !== undefined) directives.push(`max-video-preview:${r["max-video-preview"]}`);
|
|
59
|
+
if (directives.length > 0) {
|
|
60
|
+
parts.push(`<meta name="robots" content="${directives.join(",")}">`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (head.meta) {
|
|
65
|
+
const m = head.meta;
|
|
66
|
+
if (m.charset !== undefined) {
|
|
67
|
+
parts.push(`<meta charset="${escapeAttr(m.charset)}">`);
|
|
68
|
+
}
|
|
69
|
+
if (m.viewport !== undefined && m.viewport !== false) {
|
|
70
|
+
parts.push(`<meta name="viewport" content="${escapeAttr(m.viewport)}">`);
|
|
71
|
+
}
|
|
72
|
+
if (m.manifest !== undefined) {
|
|
73
|
+
parts.push(`<link rel="manifest" href="${escapeAttr(m.manifest)}">`);
|
|
74
|
+
}
|
|
75
|
+
if (m.appleMobileWebAppCapable !== undefined) {
|
|
76
|
+
parts.push(`<meta name="apple-mobile-web-app-capable" content="${escapeAttr(m.appleMobileWebAppCapable)}">`);
|
|
77
|
+
}
|
|
78
|
+
if (m.appleMobileWebAppStatusBarStyle !== undefined) {
|
|
79
|
+
parts.push(
|
|
80
|
+
`<meta name="apple-mobile-web-app-status-bar-style" content="${escapeAttr(m.appleMobileWebAppStatusBarStyle)}">`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (m.appleMobileWebAppTitle !== undefined) {
|
|
84
|
+
parts.push(`<meta name="apple-mobile-web-app-title" content="${escapeAttr(m.appleMobileWebAppTitle)}">`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const META_NAME_MAP: Record<string, string> = {
|
|
88
|
+
applicationName: "application-name",
|
|
89
|
+
mobileWebAppCapable: "mobile-web-app-capable",
|
|
90
|
+
};
|
|
91
|
+
const genericMetaKeys = [
|
|
92
|
+
"applicationName",
|
|
93
|
+
"author",
|
|
94
|
+
"creator",
|
|
95
|
+
"generator",
|
|
96
|
+
"mobileWebAppCapable",
|
|
97
|
+
"publisher",
|
|
98
|
+
] as const;
|
|
99
|
+
for (const key of genericMetaKeys) {
|
|
100
|
+
const val = m[key];
|
|
101
|
+
if (val !== undefined) {
|
|
102
|
+
const name = META_NAME_MAP[key] ?? key;
|
|
103
|
+
parts.push(`<meta name="${name}" content="${escapeAttr(val)}">`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (head.favicons) {
|
|
109
|
+
const f = head.favicons;
|
|
110
|
+
if (f.ico !== undefined) {
|
|
111
|
+
parts.push(`<link rel="icon" href="${escapeAttr(f.ico)}" sizes="any">`);
|
|
112
|
+
}
|
|
113
|
+
if (f.svg !== undefined) {
|
|
114
|
+
parts.push(`<link rel="icon" type="image/svg+xml" href="${escapeAttr(f.svg)}">`);
|
|
115
|
+
}
|
|
116
|
+
if (f.appleTouchIcon !== undefined) {
|
|
117
|
+
parts.push(`<link rel="apple-touch-icon" href="${escapeAttr(f.appleTouchIcon)}">`);
|
|
118
|
+
}
|
|
119
|
+
const sizedKeys = ["96x96", "192x192", "512x512"] as const;
|
|
120
|
+
for (const size of sizedKeys) {
|
|
121
|
+
const href = f[size];
|
|
122
|
+
if (href !== undefined) {
|
|
123
|
+
parts.push(`<link rel="icon" type="image/png" sizes="${size}" href="${escapeAttr(href)}">`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (head.images) {
|
|
129
|
+
for (const img of head.images) {
|
|
130
|
+
renderOgImage(parts, img);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (head.openGraph) {
|
|
135
|
+
const og = head.openGraph;
|
|
136
|
+
if (og.title !== undefined) parts.push(`<meta property="og:title" content="${escapeAttr(og.title)}">`);
|
|
137
|
+
if (og.description !== undefined)
|
|
138
|
+
parts.push(`<meta property="og:description" content="${escapeAttr(og.description)}">`);
|
|
139
|
+
if (og.type !== undefined) parts.push(`<meta property="og:type" content="${escapeAttr(og.type)}">`);
|
|
140
|
+
if (og.url !== undefined) parts.push(`<meta property="og:url" content="${escapeAttr(og.url)}">`);
|
|
141
|
+
if (og.siteName !== undefined) parts.push(`<meta property="og:site_name" content="${escapeAttr(og.siteName)}">`);
|
|
142
|
+
if (og.locale !== undefined) parts.push(`<meta property="og:locale" content="${escapeAttr(og.locale)}">`);
|
|
143
|
+
if (og.alternateLocale) {
|
|
144
|
+
for (const locale of og.alternateLocale) {
|
|
145
|
+
parts.push(`<meta property="og:locale:alternate" content="${escapeAttr(locale)}">`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (og.images) {
|
|
149
|
+
for (const img of og.images) {
|
|
150
|
+
renderOgImage(parts, img);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (og.videos) {
|
|
154
|
+
for (const video of og.videos) {
|
|
155
|
+
parts.push(`<meta property="og:video" content="${escapeAttr(video.url)}">`);
|
|
156
|
+
if (video.secureUrl)
|
|
157
|
+
parts.push(`<meta property="og:video:secure_url" content="${escapeAttr(video.secureUrl)}">`);
|
|
158
|
+
if (video.type) parts.push(`<meta property="og:video:type" content="${escapeAttr(video.type)}">`);
|
|
159
|
+
if (video.width) parts.push(`<meta property="og:video:width" content="${video.width}">`);
|
|
160
|
+
if (video.height) parts.push(`<meta property="og:video:height" content="${video.height}">`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (og.audio) {
|
|
164
|
+
for (const audio of og.audio) {
|
|
165
|
+
parts.push(`<meta property="og:audio" content="${escapeAttr(audio.url)}">`);
|
|
166
|
+
if (audio.secureUrl)
|
|
167
|
+
parts.push(`<meta property="og:audio:secure_url" content="${escapeAttr(audio.secureUrl)}">`);
|
|
168
|
+
if (audio.type) parts.push(`<meta property="og:audio:type" content="${escapeAttr(audio.type)}">`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (head.twitter) {
|
|
174
|
+
const tw = head.twitter;
|
|
175
|
+
if (tw.card !== undefined) parts.push(`<meta name="twitter:card" content="${escapeAttr(tw.card)}">`);
|
|
176
|
+
if (tw.site !== undefined) parts.push(`<meta name="twitter:site" content="${escapeAttr(tw.site)}">`);
|
|
177
|
+
if (tw.creator !== undefined) parts.push(`<meta name="twitter:creator" content="${escapeAttr(tw.creator)}">`);
|
|
178
|
+
if (tw.title !== undefined) parts.push(`<meta name="twitter:title" content="${escapeAttr(tw.title)}">`);
|
|
179
|
+
if (tw.description !== undefined)
|
|
180
|
+
parts.push(`<meta name="twitter:description" content="${escapeAttr(tw.description)}">`);
|
|
181
|
+
if (tw.images) {
|
|
182
|
+
for (const img of tw.images) {
|
|
183
|
+
parts.push(`<meta name="twitter:image" content="${escapeAttr(img.url)}">`);
|
|
184
|
+
if (img.alt) parts.push(`<meta name="twitter:image:alt" content="${escapeAttr(img.alt)}">`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (head.languages) {
|
|
190
|
+
for (const [lang, href] of Object.entries(head.languages)) {
|
|
191
|
+
parts.push(`<link rel="alternate" hreflang="${escapeAttr(lang)}" href="${escapeAttr(href)}">`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (head.jsonLd) {
|
|
196
|
+
const items = Array.isArray(head.jsonLd) ? head.jsonLd : [head.jsonLd];
|
|
197
|
+
for (const item of items) {
|
|
198
|
+
const json = JSON.stringify(item).replace(/<\/script\b/gi, "<\\/script");
|
|
199
|
+
parts.push(`<script type="application/ld+json" nonce="${escapeAttr(nonce)}">${json}</script>`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (head.css !== undefined) {
|
|
204
|
+
const cssItems = Array.isArray(head.css) ? head.css : [head.css];
|
|
205
|
+
for (const href of cssItems) {
|
|
206
|
+
parts.push(`<link rel="stylesheet" href="${escapeAttr(href)}">`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (head.custom) {
|
|
211
|
+
if (head.custom.links) {
|
|
212
|
+
for (const link of head.custom.links) {
|
|
213
|
+
let attrs = "";
|
|
214
|
+
for (const [k, v] of Object.entries(link)) {
|
|
215
|
+
if (isSafeAttrName(k)) {
|
|
216
|
+
if (attrs) attrs += " ";
|
|
217
|
+
attrs += `${k}="${escapeAttr(v)}"`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
parts.push(`<link ${attrs}>`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (head.custom.meta) {
|
|
224
|
+
for (const meta of head.custom.meta) {
|
|
225
|
+
let attrs = "";
|
|
226
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
227
|
+
if (isSafeAttrName(k)) {
|
|
228
|
+
if (attrs) attrs += " ";
|
|
229
|
+
attrs += `${k}="${escapeAttr(v)}"`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
parts.push(`<meta ${attrs}>`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (head.custom.scripts) {
|
|
236
|
+
for (const script of head.custom.scripts) {
|
|
237
|
+
const attrs: string[] = [`nonce="${escapeAttr(nonce)}"`];
|
|
238
|
+
if (script.async) attrs.push("async");
|
|
239
|
+
if (script.type) attrs.push(`type="${escapeAttr(script.type)}"`);
|
|
240
|
+
if (script.src) attrs.push(`src="${escapeAttr(script.src)}"`);
|
|
241
|
+
if (script.extra) {
|
|
242
|
+
for (const [k, v] of Object.entries(script.extra)) {
|
|
243
|
+
if (isSafeAttrName(k)) attrs.push(`${k}="${escapeAttr(v)}"`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const children = (script.children ?? "").replace(/<\/script\b/gi, "<\\/script");
|
|
247
|
+
parts.push(`<script ${attrs.join(" ")}>${children}</script>`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (head.custom.styles) {
|
|
251
|
+
for (const style of head.custom.styles) {
|
|
252
|
+
const safeChildren = (style.children ?? "").replace(/<\/style\b/gi, "<\\/style");
|
|
253
|
+
parts.push(`<style nonce="${escapeAttr(nonce)}">${safeChildren}</style>`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return parts.join("");
|
|
259
|
+
}
|