@chr33s/solarflare 0.0.2 → 0.0.3

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/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@chr33s/solarflare",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "license": "MIT",
5
- "bin": "./src/build.ts",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/chr33s/solarflare"
8
+ },
9
+ "bin": {
10
+ "solarflare": "src/build.ts"
11
+ },
6
12
  "files": [
7
13
  "src",
8
14
  "tsconfig.json",
@@ -20,33 +26,36 @@
20
26
  },
21
27
  "scripts": {
22
28
  "check": "oxfmt --check && oxlint --type-aware --type-check",
29
+ "examples:clean": "rm -rvf ./examples/*/.wrangler ./examples/*/.shopify ./examples/*/dist ./examples/*/node_modules",
30
+ "examples:install": "for dir in basic dsd minimal node shopify-app; do npm install --prefix ./examples/${dir} --silent; done && bun install --cwd=./examples/bun && cd examples/deno && deno install",
23
31
  "fix": "oxlint --type-aware --type-check --fix && oxfmt --write",
24
32
  "test": "WRANGLER_LOG=error; node --test src/*.test.ts"
25
33
  },
26
34
  "dependencies": {
27
- "@preact/signals": "2.8.0",
35
+ "@preact/signals": "2.8.1",
28
36
  "lightningcss": "1.31.1",
29
37
  "picomatch": "4.0.3",
30
38
  "preact": "11.0.0-beta.1",
31
39
  "preact-custom-element": "4.6.0",
32
- "preact-render-to-string": "6.6.5",
33
- "rolldown": "1.0.0-rc.4",
40
+ "preact-render-to-string": "6.6.6",
41
+ "rolldown": "1.0.0-rc.5",
34
42
  "turbo-stream": "3.1.0"
35
43
  },
36
44
  "devDependencies": {
37
- "@preact/signals-debug": "1.4.1",
38
- "@types/node": "25.2.3",
39
- "oxfmt": "0.32.0",
40
- "oxlint": "1.47.0",
41
- "oxlint-tsgolint": "0.13.0",
45
+ "@preact/signals-debug": "1.4.2",
46
+ "@types/node": "25.3.0",
47
+ "oxfmt": "0.35.0",
48
+ "oxlint": "1.50.0",
49
+ "oxlint-tsgolint": "0.14.2",
42
50
  "playwright": "1.58.2",
43
51
  "typescript": "5.9.3",
44
- "wrangler": "4.65.0"
52
+ "wrangler": "4.68.0"
45
53
  },
46
54
  "optionalDependencies": {
47
55
  "ts-morph": "27.0.2"
48
56
  },
49
57
  "engines": {
58
+ "bun": ">=1.3.0",
50
59
  "node": ">=24.12.0"
51
60
  }
52
61
  }
package/readme.md CHANGED
@@ -132,7 +132,6 @@ import { Deferred } from "@chr33s/solarflare/client";
132
132
  <!-- performance -->
133
133
  <meta name="sf:preconnect" content="https://cdn.example.com" />
134
134
  <meta name="sf:early-flush" content="true" />
135
- <meta name="sf:critical-css" content="true" />
136
135
  <meta name="sf:cache-max-age" content="300" />
137
136
  <meta name="sf:cache-swr" content="3600" />
138
137
  <meta name="sf:prefetch" content="/about, /faq, /blog/*" />
@@ -160,6 +159,7 @@ export default define(MyComponent, { shadow: true });
160
159
  - [Basic](examples/basic/readme.md) — Layouts, dynamic routes, API, components
161
160
  - [Bun](examples/bun/readme.md) — Bun runtime example
162
161
  - [Deno](examples/deno/readme.md) — Deno runtime example
162
+ - [DSD](examples/dsd/readme.md) — Declarative Shadow DOM example
163
163
  - [Minimal](examples/minimal/readme.md) — Single route
164
164
  - [Node](examples/node/readme.md) — Using `srvx` instead of Workers
165
165
  - [Shopify App](examples/shopify-app/readme.md) — Shopify app starter
@@ -78,7 +78,7 @@ async function getComponentMeta(program: ts.Program, appDir: string, file: strin
78
78
  const contentHash = hash(content);
79
79
  const chunk = getChunkName(file, contentHash);
80
80
 
81
- return { file, tag: parsed.tag, props, parsed, chunk, hash: contentHash };
81
+ return { file, tag: parsed.tag, props, parsed, chunk, hash: contentHash, shadow: false };
82
82
  }
83
83
 
84
84
  export async function buildClient(options: BuildClientOptions) {
@@ -215,6 +215,10 @@ export async function buildClient(options: BuildClientOptions) {
215
215
  const componentPath = join(appDir, meta.file);
216
216
  const componentCssImports = await scanner.extractAllCssImports(componentPath);
217
217
 
218
+ if (await scanner.detectShadowOption(componentPath)) {
219
+ meta.shadow = true;
220
+ }
221
+
218
222
  const componentDir = meta.file.split("/").slice(0, -1).join("/");
219
223
  const componentCssOutputPaths = await resolveCssOutputs(componentCssImports, componentDir);
220
224
 
@@ -365,12 +369,18 @@ export async function buildClient(options: BuildClientOptions) {
365
369
  chunks: {},
366
370
  tags: {},
367
371
  styles: {},
372
+ shadow: {},
368
373
  devScripts: args.production ? undefined : ["/assets/console-forward.js"],
369
374
  };
370
375
 
371
376
  for (const meta of metas) {
372
377
  manifest.chunks[meta.parsed.pattern] = `/assets/${meta.chunk}`;
373
378
  manifest.tags[meta.tag] = `/assets/${meta.chunk}`;
379
+
380
+ const filePath = join(appDir, meta.file);
381
+ if (await scanner.detectShadowOption(filePath)) {
382
+ manifest.shadow![meta.tag] = true;
383
+ }
374
384
  }
375
385
 
376
386
  for (const meta of metas) {
@@ -10,6 +10,7 @@ export interface ComponentMeta {
10
10
  tag: string;
11
11
  props: string[];
12
12
  chunk: string;
13
+ shadow?: boolean;
13
14
  }
14
15
 
15
16
  function buildDebugImports(args: HmrEntryArgs) {
@@ -47,7 +48,7 @@ function buildEntryInit(meta: ComponentMeta, cssFiles: string[]) {
47
48
  return /* tsx */ `
48
49
  initHmrEntry({
49
50
  tag: '${meta.tag}',
50
- props: ${JSON.stringify(meta.props)},
51
+ props: ${JSON.stringify(meta.props)},${meta.shadow ? `\n shadow: true,` : ""}
51
52
  routesManifest,
52
53
  BaseComponent,
53
54
  hmr,
package/src/build.scan.ts CHANGED
@@ -167,6 +167,12 @@ export function createScanner(ctx: BuildScanContext) {
167
167
  return allCss;
168
168
  }
169
169
 
170
+ /** Detects `define(_, { shadow: true })` in a client component file. */
171
+ async function detectShadowOption(filePath: string) {
172
+ const content = await readFile(filePath, "utf-8");
173
+ return /define\s*\([^)]*\{[^}]*shadow\s*:\s*true/.test(content);
174
+ }
175
+
170
176
  return {
171
177
  scanFiles,
172
178
  getPackageImports,
@@ -178,5 +184,6 @@ export function createScanner(ctx: BuildScanContext) {
178
184
  extractComponentImports,
179
185
  resolveImportPath,
180
186
  extractAllCssImports,
187
+ detectShadowOption,
181
188
  };
182
189
  }
package/src/build.ts CHANGED
File without changes
package/src/client.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { type FunctionComponent } from "preact";
2
- import register from "preact-custom-element";
3
2
  import { parsePath } from "./paths.ts";
4
3
  import { hydrateStore, initHydrationCoordinator } from "./hydration.ts";
5
4
  import { installHeadHoisting, createHeadContext, setHeadContext } from "./head.ts";
@@ -34,6 +33,14 @@ export function registerInlineStyles(tag: string, styles: InlineStyleEntry[]) {
34
33
  if (!styles.length) return;
35
34
  if (!supportsConstructableStylesheets() || typeof document === "undefined") return;
36
35
 
36
+ // If DSD already provided inline <style> in the shadow root, migrate it to
37
+ // adoptedStyleSheets so StylesheetManager.update() can reach it during HMR.
38
+ const el = document.querySelector(tag);
39
+ const shadowStyle = el?.shadowRoot?.querySelector("style");
40
+ if (shadowStyle) {
41
+ shadowStyle.remove();
42
+ }
43
+
37
44
  for (const style of styles) {
38
45
  const preloaded = getPreloadedStylesheet(style.id);
39
46
  if (!preloaded) {
@@ -42,10 +49,18 @@ export function registerInlineStyles(tag: string, styles: InlineStyleEntry[]) {
42
49
  }
43
50
 
44
51
  const sheets = stylesheets.getForConsumer(tag);
45
- document.adoptedStyleSheets = [
46
- ...document.adoptedStyleSheets.filter((s) => !sheets.includes(s)),
47
- ...sheets,
48
- ];
52
+ const shadowRoot = el?.shadowRoot;
53
+ if (shadowRoot) {
54
+ shadowRoot.adoptedStyleSheets = [
55
+ ...shadowRoot.adoptedStyleSheets.filter((s) => !sheets.includes(s)),
56
+ ...sheets,
57
+ ];
58
+ } else {
59
+ document.adoptedStyleSheets = [
60
+ ...document.adoptedStyleSheets.filter((s) => !sheets.includes(s)),
61
+ ...sheets,
62
+ ];
63
+ }
49
64
  }
50
65
 
51
66
  /** Tag metadata from file path. */
@@ -145,46 +160,14 @@ export interface DefineOptions {
145
160
  validate?: boolean;
146
161
  }
147
162
 
148
- /** Registers a Preact component as a web component (build-time macro). */
163
+ /**
164
+ * Registers a Preact component as a web component.
165
+ * When used inside the solarflare build pipeline, `initHmrEntry` handles
166
+ * actual registration — `define()` just returns the component.
167
+ */
149
168
  export function define<P extends Record<string, any>>(
150
169
  Component: FunctionComponent<P>,
151
- options?: DefineOptions,
170
+ _options?: DefineOptions,
152
171
  ) {
153
- // Only register custom elements in the browser
154
- if (typeof window !== "undefined" && typeof HTMLElement !== "undefined") {
155
- const propNames = options?.observedAttributes ?? [];
156
- const filePath = import.meta.path ?? import.meta.url ?? "";
157
- const meta = parseTagMeta(filePath);
158
- const tag = options?.tag ?? meta.tag;
159
- const shadow = options?.shadow ?? false;
160
- const shouldValidate = options?.validate ?? import.meta.env?.DEV ?? false;
161
-
162
- if (customElements.get(tag)) {
163
- console.warn(`[solarflare] Custom element "${tag}" is already registered, skipping`);
164
- return Component;
165
- }
166
-
167
- if (shouldValidate) {
168
- const validation = validateTag({ ...meta, tag });
169
-
170
- for (const warning of validation.warnings) {
171
- console.warn(`[solarflare] ${warning}`);
172
- }
173
-
174
- for (const error of validation.errors) {
175
- console.error(`[solarflare] ${error}`);
176
- }
177
-
178
- if (!validation.valid) {
179
- console.error(
180
- `[solarflare] Tag validation failed for "${filePath}", component may not work correctly`,
181
- );
182
- }
183
- }
184
-
185
- // Register the component as a custom element
186
- register(Component, tag, propNames, { shadow });
187
- }
188
-
189
172
  return Component;
190
173
  }
@@ -64,6 +64,17 @@ async function updateNode(oldNode: Node, newNode: Node, walker: Walker) {
64
64
  }
65
65
 
66
66
  if (oldNode.nodeType === ELEMENT_TYPE) {
67
+ // Treat DSD custom elements as atomic: the old DOM has a real shadow root
68
+ // while the new HTML has a <template shadowrootmode> child — these can't
69
+ // be diffed structurally, so replace the entire element.
70
+ if (hasShadowRoot(oldNode as Element) && hasDsdTemplate(newNode as Element)) {
71
+ return walker[APPLY_TRANSITION](() => {
72
+ if (oldNode.parentNode) {
73
+ oldNode.parentNode.replaceChild(newNode.cloneNode(true), oldNode);
74
+ }
75
+ });
76
+ }
77
+
67
78
  await setChildNodes(oldNode, newNode, walker);
68
79
 
69
80
  walker[APPLY_TRANSITION](() => {
@@ -86,6 +97,19 @@ async function updateNode(oldNode: Node, newNode: Node, walker: Walker) {
86
97
  }
87
98
  }
88
99
 
100
+ /** Checks if an element has a live shadow root (from DSD or imperative). */
101
+ function hasShadowRoot(el: Element) {
102
+ return !!(el as HTMLElement).shadowRoot;
103
+ }
104
+
105
+ /** Checks if an element contains a `<template shadowrootmode>` child. */
106
+ function hasDsdTemplate(el: Element) {
107
+ for (let c = el.firstElementChild; c; c = c.nextElementSibling) {
108
+ if (c.nodeName === "TEMPLATE" && c.hasAttribute("shadowrootmode")) return true;
109
+ }
110
+ return false;
111
+ }
112
+
89
113
  /**
90
114
  * Utility that will update one list of attributes to match another.
91
115
  */
@@ -39,7 +39,6 @@ export function generateStaticShell(options: {
39
39
  export function createEarlyFlushStream(
40
40
  shell: StreamingShell,
41
41
  options: {
42
- criticalCss?: string;
43
42
  preloadHints?: string;
44
43
  contentStream: ReadableStream<Uint8Array>;
45
44
  headTags: string;
@@ -52,11 +51,7 @@ export function createEarlyFlushStream(
52
51
 
53
52
  return new ReadableStream<Uint8Array>({
54
53
  async start(controller) {
55
- const shellStart = [
56
- shell.preHead,
57
- options.preloadHints || "",
58
- options.criticalCss ? /* html */ `<style>${options.criticalCss}</style>` : "",
59
- ].join("");
54
+ const shellStart = [shell.preHead, options.preloadHints || ""].join("");
60
55
 
61
56
  controller.enqueue(encoder.encode(shellStart));
62
57
 
package/src/hmr.ts CHANGED
@@ -437,6 +437,7 @@ export function onHMREvent(
437
437
  interface HmrEntryOptions {
438
438
  tag: string;
439
439
  props: string[];
440
+ shadow?: boolean;
440
441
  routesManifest: RoutesManifest;
441
442
  BaseComponent: FunctionComponent<any>;
442
443
  hmr: HmrApi;
@@ -642,6 +643,18 @@ function createHmrEntryComponent(options: HmrEntryOptions) {
642
643
  export function initHmrEntry(options: HmrEntryOptions) {
643
644
  const Component = createHmrEntryComponent(options);
644
645
  if (!customElements.get(options.tag)) {
645
- register(Component, options.tag, options.props, { shadow: false });
646
+ const shadow = options.shadow ?? false;
647
+ if (shadow) {
648
+ // When DSD has already created a shadow root, patch attachShadow to
649
+ // return the existing root so preact-custom-element won't throw.
650
+ const orig = HTMLElement.prototype.attachShadow.bind(HTMLElement.prototype);
651
+ HTMLElement.prototype.attachShadow = function (init: ShadowRootInit) {
652
+ return this.shadowRoot ?? orig.call(this, init);
653
+ };
654
+ register(Component, options.tag, options.props, { shadow });
655
+ HTMLElement.prototype.attachShadow = orig;
656
+ } else {
657
+ register(Component, options.tag, options.props, { shadow });
658
+ }
646
659
  }
647
660
  }
@@ -19,6 +19,11 @@ export function getStylesheets(pattern: string) {
19
19
  return manifest.styles[pattern] ?? [];
20
20
  }
21
21
 
22
+ /** Gets whether a tag uses Shadow DOM (DSD). */
23
+ export function isShadowTag(tag: string) {
24
+ return manifest.shadow?.[tag] ?? false;
25
+ }
26
+
22
27
  /** Gets dev mode scripts from the chunk manifest. */
23
28
  export function getDevScripts() {
24
29
  return manifest.devScripts;
package/src/manifest.ts CHANGED
@@ -19,5 +19,7 @@ export interface ChunkManifest {
19
19
  chunks: Record<string, string>;
20
20
  tags: Record<string, string>;
21
21
  styles: Record<string, string[]>;
22
+ /** Tags that use Shadow DOM (DSD). */
23
+ shadow?: Record<string, boolean>;
22
24
  devScripts?: string[];
23
25
  }
package/src/server.ts CHANGED
@@ -222,6 +222,7 @@ export function renderComponent(
222
222
  Component: FunctionComponent<any>,
223
223
  tag: string,
224
224
  props: Record<string, unknown>,
225
+ options?: { shadow?: boolean },
225
226
  ) {
226
227
  const attrs: Record<string, string> = {};
227
228
  for (const [key, value] of Object.entries(props)) {
@@ -230,7 +231,11 @@ export function renderComponent(
230
231
  attrs[key] = String(value);
231
232
  }
232
233
  }
233
- return h(tag, attrs, h(Component, props));
234
+ const children = h(Component, props);
235
+ if (options?.shadow) {
236
+ return h(tag, attrs, h("template", { shadowrootmode: "open" }, children));
237
+ }
238
+ return h(tag, attrs, children);
234
239
  }
235
240
 
236
241
  /** Error page props interface. */
@@ -18,8 +18,7 @@ export interface WorkerMetaConfig {
18
18
  cacheConfig?: RouteCacheConfig;
19
19
  /** Enable early flush */
20
20
  earlyFlush: boolean;
21
- /** Enable critical CSS inlining */
22
- criticalCss: boolean;
21
+
23
22
  /** URLs/patterns to prefetch */
24
23
  prefetch: string[];
25
24
  /** URLs/patterns to prerender */
@@ -35,7 +34,6 @@ const DEFAULTS: WorkerMetaConfig = {
35
34
  lang: "en",
36
35
  preconnectOrigins: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"],
37
36
  earlyFlush: false,
38
- criticalCss: false,
39
37
  prefetch: [],
40
38
  prerender: [],
41
39
  speculationEagerness: "moderate",
@@ -80,11 +78,6 @@ export function parseMetaConfig(html: string) {
80
78
  config.earlyFlush = earlyFlush === "true";
81
79
  }
82
80
 
83
- const criticalCss = matchMeta("sf:critical-css");
84
- if (criticalCss) {
85
- config.criticalCss = criticalCss === "true";
86
- }
87
-
88
81
  const prefetch = matchMeta("sf:prefetch");
89
82
  if (prefetch) {
90
83
  config.prefetch = prefetch
@@ -138,7 +131,6 @@ export function workerConfigMeta(config: {
138
131
  cacheMaxAge?: number;
139
132
  cacheSwr?: number;
140
133
  earlyFlush?: boolean;
141
- criticalCss?: boolean;
142
134
  prefetch?: string[];
143
135
  prerender?: string[];
144
136
  prefetchSelector?: string;
@@ -164,10 +156,6 @@ export function workerConfigMeta(config: {
164
156
  meta.push({ name: "sf:early-flush", content: String(config.earlyFlush) });
165
157
  }
166
158
 
167
- if (config.criticalCss !== undefined) {
168
- meta.push({ name: "sf:critical-css", content: String(config.criticalCss) });
169
- }
170
-
171
159
  if (config.prefetch?.length) {
172
160
  meta.push({ name: "sf:prefetch", content: config.prefetch.join(",") });
173
161
  }
package/src/worker.ts CHANGED
@@ -10,12 +10,17 @@ import {
10
10
  generateResourceHints,
11
11
  type StreamingShell,
12
12
  } from "./early-flush.ts";
13
- import { extractCriticalCss, generateAsyncCssLoader } from "./critical-css.ts";
14
13
  import { collectEarlyHints, generateEarlyHintsHeader } from "./early-hints.ts";
15
14
  import { ResponseCache, withCache } from "./route-cache.ts";
16
15
  import { parseMetaConfig } from "./worker.config.ts";
17
16
  import { getHeadContext, type HeadTag } from "./head.ts";
18
- import { typedModules, getScriptPath, getStylesheets, getDevScripts } from "./manifest.runtime.ts";
17
+ import {
18
+ typedModules,
19
+ getScriptPath,
20
+ getStylesheets,
21
+ getDevScripts,
22
+ isShadowTag,
23
+ } from "./manifest.runtime.ts";
19
24
  import { findPairedModulePath } from "./paths.ts";
20
25
  import { encode } from "turbo-stream";
21
26
 
@@ -38,8 +43,6 @@ function getStaticShell(lang: string) {
38
43
  /** Worker optimization options. */
39
44
  export interface WorkerOptimizations {
40
45
  earlyFlush?: boolean;
41
- criticalCss?: boolean;
42
- readCss?: (path: string) => Promise<string>;
43
46
  }
44
47
 
45
48
  /** Server data loader function type. */
@@ -79,11 +82,8 @@ interface RenderPlan {
79
82
  status: number;
80
83
  statusText?: string;
81
84
  metaConfig: ReturnType<typeof parseMetaConfig>;
82
- stylesheets: string[];
83
85
  resourceHints: string;
84
86
  useEarlyFlush: boolean;
85
- useCriticalCss: boolean;
86
- pathname: string;
87
87
  }
88
88
 
89
89
  type MatchAndLoadResult =
@@ -296,6 +296,13 @@ async function matchAndLoad(request: Request, url: URL): Promise<MatchAndLoadRes
296
296
  deferredPromises[key] = value;
297
297
  } else {
298
298
  immediateData[key] = value;
299
+ // Non-primitive values can't be serialized as HTML attributes on the
300
+ // custom element. Emit them as instantly-resolved deferred data islands
301
+ // so the client-side hydration mechanism picks them up.
302
+ const t = typeof value;
303
+ if (value != null && t !== "string" && t !== "number" && t !== "boolean") {
304
+ deferredPromises[key] = Promise.resolve(value);
305
+ }
299
306
  }
300
307
  }
301
308
 
@@ -308,7 +315,9 @@ async function matchAndLoad(request: Request, url: URL): Promise<MatchAndLoadRes
308
315
  const clientMod = await typedModules.client[clientPath]();
309
316
  const Component = clientMod.default as FunctionComponent<any>;
310
317
 
311
- let content = server.renderComponent(Component, route.tag, props);
318
+ let content = server.renderComponent(Component, route.tag, props, {
319
+ shadow: isShadowTag(route.tag),
320
+ });
312
321
 
313
322
  const layouts = server.findLayouts(route.path, typedModules);
314
323
  if (layouts.length > 0) {
@@ -363,7 +372,6 @@ async function renderStream(
363
372
  });
364
373
 
365
374
  const useEarlyFlush = envOptimizations.earlyFlush ?? metaConfig.earlyFlush;
366
- const useCriticalCss = envOptimizations.criticalCss ?? metaConfig.criticalCss;
367
375
 
368
376
  const ssrStream = await server.renderToStream(content, {
369
377
  params,
@@ -395,45 +403,23 @@ async function renderStream(
395
403
  status: ssrStream.status ?? 200,
396
404
  statusText: ssrStream.statusText,
397
405
  metaConfig,
398
- stylesheets,
399
406
  resourceHints,
400
407
  useEarlyFlush,
401
- useCriticalCss,
402
- pathname: route.parsedPattern.pathname,
403
408
  };
404
409
  }
405
410
 
406
- async function applyPerfFeatures(plan: RenderPlan, envOptimizations: WorkerOptimizations) {
407
- const {
408
- ssrStream,
409
- finalHeaders,
410
- status,
411
- statusText,
412
- stylesheets,
413
- resourceHints,
414
- useEarlyFlush,
415
- useCriticalCss,
416
- pathname,
417
- metaConfig,
418
- } = plan;
411
+ async function applyPerfFeatures(plan: RenderPlan) {
412
+ const { ssrStream, finalHeaders, status, statusText, resourceHints, useEarlyFlush, metaConfig } =
413
+ plan;
419
414
 
420
415
  if (useEarlyFlush) {
421
416
  const staticShell = getStaticShell(metaConfig.lang);
422
417
 
423
- let criticalCss = "";
424
- if (useCriticalCss && envOptimizations.readCss) {
425
- criticalCss = await extractCriticalCss(pathname, stylesheets, {
426
- readCss: envOptimizations.readCss,
427
- cache: true,
428
- });
429
- }
430
-
431
418
  const optimizedStream = createEarlyFlushStream(staticShell, {
432
- criticalCss,
433
419
  preloadHints: resourceHints,
434
420
  contentStream: ssrStream,
435
421
  headTags: "",
436
- bodyTags: generateAsyncCssLoader(stylesheets),
422
+ bodyTags: "",
437
423
  });
438
424
 
439
425
  return new Response(optimizedStream, {
@@ -518,7 +504,7 @@ async function worker(request: Request, env?: WorkerEnv) {
518
504
 
519
505
  const render = async () => {
520
506
  const plan = await renderStream(context, headers, envOptimizations);
521
- return applyPerfFeatures(plan, envOptimizations);
507
+ return applyPerfFeatures(plan);
522
508
  };
523
509
 
524
510
  if (context.metaConfig.cacheConfig) {
@@ -1,103 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { escapeJsonForHtml } from "./serialize.ts";
3
-
4
- /** Critical CSS cache entry. */
5
- interface CriticalCssEntry {
6
- css: string;
7
- hash: string;
8
- timestamp: number;
9
- }
10
-
11
- /** In-memory cache for critical CSS (per route). */
12
- const criticalCssCache = new Map<string, CriticalCssEntry>();
13
-
14
- /** Max age for cached critical CSS (1 hour). */
15
- const CACHE_MAX_AGE = 60 * 60 * 1000;
16
-
17
- /** Extracts critical CSS for a route */
18
- export async function extractCriticalCss(
19
- routePattern: string,
20
- cssFiles: string[],
21
- options: {
22
- readCss: (path: string) => Promise<string>;
23
- maxSize?: number;
24
- cache?: boolean;
25
- },
26
- ) {
27
- const cacheKey = routePattern;
28
- const maxSize = options.maxSize ?? 14 * 1024;
29
-
30
- if (options.cache !== false) {
31
- const cached = criticalCssCache.get(cacheKey);
32
- if (cached && Date.now() - cached.timestamp < CACHE_MAX_AGE) {
33
- return cached.css;
34
- }
35
- }
36
-
37
- const cssContents: string[] = [];
38
- let totalSize = 0;
39
-
40
- for (const file of cssFiles) {
41
- try {
42
- const content = await options.readCss(file);
43
- const minified = minifyCss(content);
44
-
45
- if (totalSize + minified.length > maxSize) {
46
- break;
47
- }
48
-
49
- cssContents.push(minified);
50
- totalSize += minified.length;
51
- } catch {}
52
- }
53
-
54
- const criticalCss = cssContents.join("\n");
55
- const hash = createHash("md5").update(criticalCss).digest("hex").slice(0, 8);
56
-
57
- if (options.cache !== false) {
58
- criticalCssCache.set(cacheKey, {
59
- css: criticalCss,
60
- hash,
61
- timestamp: Date.now(),
62
- });
63
- }
64
-
65
- return criticalCss;
66
- }
67
-
68
- /** Simple CSS minification for critical CSS */
69
- function minifyCss(css: string) {
70
- return css
71
- .replace(/\/\*[\s\S]*?\*\//g, "")
72
- .replace(/\s+/g, " ")
73
- .replace(/\s*([{}: ;,])\s*/g, "$1")
74
- .trim();
75
- }
76
-
77
- /** Generates a noscript fallback for CSS loading. */
78
- export function generateCssFallback(stylesheets: string[]) {
79
- const links = stylesheets
80
- .map((href) => /* html */ `<link rel="stylesheet" href="${href}">`)
81
- .join("");
82
- return /* html */ `<noscript>${links}</noscript>`;
83
- }
84
-
85
- /** Generates async CSS loading script without blocking render on non-critical CSS */
86
- export function generateAsyncCssLoader(stylesheets: string[]) {
87
- if (stylesheets.length === 0) return "";
88
-
89
- const hrefs = escapeJsonForHtml(stylesheets);
90
-
91
- return /* html */ `
92
- <script>
93
- (function() {
94
- var ss=${hrefs};
95
- ss.forEach(function(h){
96
- var l=document.createElement('link');
97
- l.rel='stylesheet';l.href=h;
98
- document.head.appendChild(l);
99
- });
100
- })();
101
- </script>
102
- `;
103
- }