@lovrozagar/flare 0.9.2 → 0.9.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovrozagar/flare",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "Solid meta-framework. Server-driven, NDJSON streaming, renderToStream.",
5
5
  "keywords": [
6
6
  "flare",
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { hoistHydrationHeadMarkers } from "../ssr/hoist-head-markers.ts";
3
+ import { markSolidHeadStart, moveHeadPrependsAfterSolid } from "../ssr/head-prepend.ts";
4
4
  import type { ResolvedEntries, VitePlugin } from "./types.ts";
5
5
 
6
6
  interface NodeReq {
@@ -210,10 +210,9 @@ export function createDevServerPlugin(entries: ResolvedEntries, _assetsBase: str
210
210
  htmlPath = "/";
211
211
  }
212
212
  }
213
- const { html: peeledHtml, tags } = peelInlineTags(html);
214
- html = hoistHydrationHeadMarkers(
215
- restoreInlineTags(await vite.transformIndexHtml(htmlPath, peeledHtml), tags),
216
- );
213
+ const { html: peeledHtml, tags } = peelInlineTags(markSolidHeadStart(html));
214
+ const transformed = await vite.transformIndexHtml(htmlPath, peeledHtml);
215
+ html = moveHeadPrependsAfterSolid(restoreInlineTags(transformed, tags));
217
216
 
218
217
  const htmlHeaders: Record<string, string | string[]> = {
219
218
  "content-type": "text/html; charset=utf-8",
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Stylesheet composition shared by the build (flare-global.css) and the dev SSR runtime
3
+ * (virtual:flare-sx-dev-css imports this file into the server runner). No Node imports.
4
+ */
5
+
6
+ export const LAYER_PRELUDE = "@layer reset, sx, app, user.lib, user.app, inline;";
7
+
8
+ /* Tailwind's fallback for browsers without @property: the same initial values, set directly. */
9
+ const PROPERTIES_SUPPORTS =
10
+ "((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))";
11
+
12
+ /** Tailwind's element-local stacks (`--tw-shadow`, `--tw-scale-x`, …): never theme values. */
13
+ export function isLocalVar(name: string): boolean {
14
+ return name.startsWith("--tw-");
15
+ }
16
+
17
+ function propertiesFallback(properties: Map<string, string>): string {
18
+ const decls = [...properties].map(([name, rule]) => {
19
+ const initial = /initial-value:\s*([^;]+);/.exec(rule);
20
+ return `${name}: ${initial ? initial[1].trim() : "initial"};`;
21
+ });
22
+ return `@layer properties { @supports ${PROPERTIES_SUPPORTS} { *, ::before, ::after, ::backdrop { ${decls.join(" ")} } } }`;
23
+ }
24
+
25
+ /* At-rules last so @media utilities beat earlier base utilities of equal
26
+ * specificity. classPool insertion order is first-seen across the whole
27
+ * build, so a later page's `fontSize: 12px` would otherwise override an
28
+ * earlier `@media (min-width: 1px) { fontSize: 24px }`. */
29
+ function atLast(a: string, b: string): number {
30
+ return Number(a.trimStart().startsWith("@")) - Number(b.trimStart().startsWith("@"));
31
+ }
32
+
33
+ /** Compose the final CSS text from the class pool, wrapped in @layer blocks. */
34
+ export function composeCss(
35
+ classPool: Map<string, string>,
36
+ layerByClass: Map<string, "sx" | "app">,
37
+ skip: Set<string>,
38
+ twPrefaceCss: string,
39
+ properties: Map<string, string> = new Map(),
40
+ themeBlock = "",
41
+ ): string {
42
+ const sxRules: string[] = [];
43
+ const appRules: string[] = [];
44
+
45
+ for (const [cls, rule] of classPool) {
46
+ if (skip.has(cls)) continue;
47
+ /* istanbul ignore next -- layerByClass is always set alongside classPool in cssEmit */
48
+ const layer = layerByClass.get(cls) ?? "app";
49
+ if (layer === "sx") sxRules.push(rule);
50
+ else appRules.push(rule);
51
+ }
52
+
53
+ sxRules.sort(atLast);
54
+ appRules.sort(atLast);
55
+
56
+ const parts: string[] = [];
57
+ /* First statement, so the fallback layer ranks below every other layer. */
58
+ if (properties.size > 0) parts.push("@layer properties;");
59
+ if (twPrefaceCss) parts.push(twPrefaceCss);
60
+ if (themeBlock) parts.push(themeBlock);
61
+ parts.push(LAYER_PRELUDE);
62
+ if (sxRules.length > 0) parts.push(`@layer sx { ${sxRules.join(" ")} }`);
63
+ if (appRules.length > 0) parts.push(`@layer app { ${appRules.join(" ")} }`);
64
+ if (properties.size > 0) {
65
+ parts.push(...properties.values());
66
+ parts.push(propertiesFallback(properties));
67
+ }
68
+
69
+ return parts.join("\n");
70
+ }
71
+
72
+ /**
73
+ * `@layer theme { :root, :host { … } }` defining the theme vars `referenced` needs: each one and,
74
+ * transitively, the vars its value references. Locals (`--tw-*`), vars the preface already defines,
75
+ * and names Tailwind never emitted are skipped. Empty string when nothing is needed.
76
+ */
77
+ export function themeVarsBlock(referenced: Set<string>, themeVars: Map<string, string>, preface: string): string {
78
+ const decls: string[] = [];
79
+ const seen = new Set<string>();
80
+ const queue = [...referenced];
81
+ while (queue.length > 0) {
82
+ const name = queue.shift() as string;
83
+ if (seen.has(name) || isLocalVar(name)) continue;
84
+ seen.add(name);
85
+ if (new RegExp(`${name.replace(/[-]/g, "\\-")}\\s*:`).test(preface)) continue;
86
+ const value = themeVars.get(name);
87
+ if (value === undefined) continue;
88
+ decls.push(`${name}: ${value};`);
89
+ for (const m of value.matchAll(/var\((--[\w-]+)/g)) queue.push(m[1]);
90
+ }
91
+ return decls.length > 0 ? `@layer theme { :root, :host { ${decls.join(" ")} } }` : "";
92
+ }
@@ -1,16 +1,12 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import type { Plugin, ViteDevServer } from "vite";
4
5
  import { createThemeCn } from "../cn-theme.ts";
5
6
  import type { ThemeCn } from "../cn-theme.ts";
6
- import {
7
- extractDeclarations,
8
- extractPrefaceCss,
9
- extractPropertyRules,
10
- initTailwindCompiler,
11
- themeVarsBlock,
12
- } from "../tw-compile.ts";
7
+ import { extractDeclarations, extractPrefaceCss, extractPropertyRules, initTailwindCompiler } from "../tw-compile.ts";
13
8
  import type { TailwindCompiler } from "../tw-compile.ts";
9
+ import { composeCss, themeVarsBlock } from "./compose-css.ts";
14
10
  import { rewriteModule } from "./rewrite.ts";
15
11
 
16
12
  export interface SxStrictOptions {
@@ -59,8 +55,6 @@ export interface SxAstOptions {
59
55
  tw?: boolean;
60
56
  }
61
57
 
62
- const LAYER_PRELUDE = "@layer reset, sx, app, user.lib, user.app, inline;";
63
-
64
58
  /** CSS rule pool accumulated across all transforms in a build. */
65
59
  interface PluginState {
66
60
  /** className → CSS rule text */
@@ -160,63 +154,6 @@ function resolveLayer(
160
154
  return "app";
161
155
  }
162
156
 
163
- /** Compose the final CSS text from the class pool, wrapped in @layer blocks. */
164
- /* Tailwind's fallback for browsers without @property: the same initial values, set directly. */
165
- const PROPERTIES_SUPPORTS =
166
- "((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))";
167
-
168
- function propertiesFallback(properties: Map<string, string>): string {
169
- const decls = [...properties].map(([name, rule]) => {
170
- const initial = /initial-value:\s*([^;]+);/.exec(rule);
171
- return `${name}: ${initial ? initial[1].trim() : "initial"};`;
172
- });
173
- return `@layer properties { @supports ${PROPERTIES_SUPPORTS} { *, ::before, ::after, ::backdrop { ${decls.join(" ")} } } }`;
174
- }
175
-
176
- function composeCss(
177
- classPool: Map<string, string>,
178
- layerByClass: Map<string, "sx" | "app">,
179
- skip: Set<string>,
180
- twPrefaceCss: string,
181
- properties: Map<string, string> = new Map(),
182
- themeBlock = "",
183
- ): string {
184
- const sxRules: string[] = [];
185
- const appRules: string[] = [];
186
-
187
- for (const [cls, rule] of classPool) {
188
- if (skip.has(cls)) continue;
189
- /* istanbul ignore next -- layerByClass is always set alongside classPool in cssEmit */
190
- const layer = layerByClass.get(cls) ?? "app";
191
- if (layer === "sx") sxRules.push(rule);
192
- else appRules.push(rule);
193
- }
194
-
195
- /* At-rules last so @media utilities beat earlier base utilities of equal
196
- * specificity. classPool insertion order is first-seen across the whole
197
- * build, so a later page's `fontSize: 12px` would otherwise override an
198
- * earlier `@media (min-width: 1px) { fontSize: 24px }`. */
199
- const atLast = (a: string, b: string) =>
200
- Number(a.trimStart().startsWith("@")) - Number(b.trimStart().startsWith("@"));
201
- sxRules.sort(atLast);
202
- appRules.sort(atLast);
203
-
204
- const parts: string[] = [];
205
- /* First statement, so the fallback layer ranks below every other layer. */
206
- if (properties.size > 0) parts.push("@layer properties;");
207
- if (twPrefaceCss) parts.push(twPrefaceCss);
208
- if (themeBlock) parts.push(themeBlock);
209
- parts.push(LAYER_PRELUDE);
210
- if (sxRules.length > 0) parts.push(`@layer sx { ${sxRules.join(" ")} }`);
211
- if (appRules.length > 0) parts.push(`@layer app { ${appRules.join(" ")} }`);
212
- if (properties.size > 0) {
213
- parts.push(...properties.values());
214
- parts.push(propertiesFallback(properties));
215
- }
216
-
217
- return parts.join("\n");
218
- }
219
-
220
157
  /* Flare's own `cn` tables module. With a twCssPath, the plugin serves tables compiled from that
221
158
  theme in its place, so `cn` merges the app's custom scale names (`rounded-control`). */
222
159
  const CN_TABLES_RE = /[\\/]styles[\\/]cn-vendor[\\/]tables\.generated\.ts(?:\?.*)?$/;
@@ -247,6 +184,40 @@ export function createSxAstPlugin(opts: SxAstOptions = {}, assetsBase: string =
247
184
  referencedVars: new Set(),
248
185
  };
249
186
  const referenceVars = opts.themeVars === "reference";
187
+ /*
188
+ * Dev SSR stylesheet: a registry living in the server runtime. Every module the server
189
+ * transforms registers its rules when it runs (see transform), so the stylesheet SSR reads
190
+ * at render time covers every module evaluated so far. A snapshot of plugin state baked into
191
+ * this module would go stale: the runner caches the module after its first import, while the
192
+ * pool keeps growing as later routes are first rendered. Composition is shared with the build.
193
+ */
194
+ const devCssModule = (): string => {
195
+ const themeVars = referenceVars && state.twCompiler ? [...state.twCompiler.themeVars] : null;
196
+ const composeCssPath = fileURLToPath(new URL("./compose-css.ts", import.meta.url));
197
+ return `import { composeCss, themeVarsBlock } from ${JSON.stringify(composeCssPath)};
198
+ const rules = new Map();
199
+ const layers = new Map();
200
+ const properties = new Map();
201
+ const referenced = new Set();
202
+ const skip = new Set(${JSON.stringify([...state.providedByLibs])});
203
+ const preface = ${JSON.stringify(state.twPrefaceCss)};
204
+ const themeVars = ${themeVars ? `new Map(${JSON.stringify(themeVars)})` : "null"};
205
+ export function registerDevSx(moduleRules, moduleProperties, moduleReferenced) {
206
+ for (const [cls, rule, layer] of moduleRules) {
207
+ rules.set(cls, rule);
208
+ layers.set(cls, layer);
209
+ }
210
+ for (const [name, rule] of moduleProperties) properties.set(name, rule);
211
+ for (const name of moduleReferenced) referenced.add(name);
212
+ }
213
+ export function getDevSxCss() {
214
+ return composeCss(rules, layers, skip, preface, properties, themeVars ? themeVarsBlock(referenced, themeVars, preface) : "");
215
+ }
216
+ export function getDevSxClasses() {
217
+ return [...rules.keys()];
218
+ }
219
+ `;
220
+ };
250
221
  const themeBlockOf = (): string =>
251
222
  referenceVars && state.twCompiler
252
223
  ? themeVarsBlock(state.referencedVars, state.twCompiler.themeVars, state.twPrefaceCss)
@@ -324,20 +295,7 @@ export function createSxAstPlugin(opts: SxAstOptions = {}, assetsBase: string =
324
295
  return theme ? { code: theme.source, moduleType: "js" } : null;
325
296
  }
326
297
  if (id !== DEV_CSS_RESOLVED_ID) return null;
327
- /* Each import() re-runs load — no caching — so SSR always gets latest state. */
328
- const css = composeCss(
329
- state.classPool,
330
- state.layerByClass,
331
- state.providedByLibs,
332
- state.twPrefaceCss,
333
- state.properties,
334
- themeBlockOf(),
335
- );
336
- const classNames = [...state.classPool.keys()];
337
- return {
338
- code: `export function getDevSxCss() { return ${JSON.stringify(css)} }\nexport function getDevSxClasses() { return ${JSON.stringify(classNames)} }`,
339
- moduleType: "js",
340
- };
298
+ return { code: devCssModule(), moduleType: "js" };
341
299
  },
342
300
 
343
301
  generateBundle() {
@@ -385,7 +343,12 @@ export function createSxAstPlugin(opts: SxAstOptions = {}, assetsBase: string =
385
343
 
386
344
  name: "flare:sx-ast",
387
345
 
388
- transform(code: string, id: string): { code: string; map: null } | null {
346
+ transform(
347
+ this: { environment?: { config?: { consumer?: string } } },
348
+ code: string,
349
+ id: string,
350
+ options?: { ssr?: boolean },
351
+ ): { code: string; map: null } | null {
389
352
  if (!id.endsWith(".tsx") && !id.endsWith(".jsx")) return null;
390
353
 
391
354
  /* Quick filter — skip files that obviously have no relevant attrs */
@@ -394,6 +357,8 @@ export function createSxAstPlugin(opts: SxAstOptions = {}, assetsBase: string =
394
357
  const layer = resolveLayer(id, libPaths, opts.layerOverride);
395
358
  const emittedForModule = new Set<string>();
396
359
  const moduleRules: Array<{ cls: string; rule: string }> = [];
360
+ const moduleProperties = new Map<string, string>();
361
+ const moduleReferenced = new Set<string>();
397
362
 
398
363
  const tw = state.twCompiler;
399
364
  const violations: string[] = [];
@@ -430,10 +395,16 @@ export function createSxAstPlugin(opts: SxAstOptions = {}, assetsBase: string =
430
395
  twCompile: tw
431
396
  ? (token: string) => {
432
397
  const output = tw.build([token]);
433
- for (const [name, rule] of extractPropertyRules(output)) state.properties.set(name, rule);
398
+ for (const [name, rule] of extractPropertyRules(output)) {
399
+ state.properties.set(name, rule);
400
+ moduleProperties.set(name, rule);
401
+ }
434
402
  const decls = extractDeclarations(output, [token], referenceVars ? undefined : tw.themeVars);
435
403
  if (referenceVars) {
436
- for (const m of decls.matchAll(/var\((--[\w-]+)/g)) state.referencedVars.add(m[1]);
404
+ for (const m of decls.matchAll(/var\((--[\w-]+)/g)) {
405
+ state.referencedVars.add(m[1]);
406
+ moduleReferenced.add(m[1]);
407
+ }
437
408
  }
438
409
  return decls || null;
439
410
  }
@@ -468,6 +439,24 @@ export function createSxAstPlugin(opts: SxAstOptions = {}, assetsBase: string =
468
439
  * result === null means the AST needed no code rewrite (pure class= literals, no sx/spread).
469
440
  * CSS was still collected into moduleRules via cssEmit — must still inject the snippet.
470
441
  */
442
+ if (
443
+ mode === "dev" &&
444
+ moduleRules.length > 0 &&
445
+ (options?.ssr || this.environment?.config?.consumer === "server")
446
+ ) {
447
+ /* Server runtime: register into the dev stylesheet SSR reads at render (devCssModule). */
448
+ const registration = JSON.stringify([
449
+ moduleRules.map(({ cls, rule }) => [cls, rule, layer]),
450
+ [...moduleProperties],
451
+ [...moduleReferenced],
452
+ ]);
453
+ const baseCode = result !== null ? result.code : code;
454
+ return {
455
+ code: `${baseCode}\nimport { registerDevSx as __flareRegisterDevSx__ } from "${DEV_CSS_VIRTUAL_ID}";\n__flareRegisterDevSx__(...${registration});\n`,
456
+ map: null,
457
+ };
458
+ }
459
+
471
460
  if (mode === "dev" && moduleRules.length > 0) {
472
461
  const layerName: "sx" | "app" = layer;
473
462
  const perClassJson = JSON.stringify(moduleRules.map(({ cls, rule }) => [cls, rule]));
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { isLocalVar } from "./sx-ast/compose-css.ts";
5
6
 
6
7
  export interface TailwindCompiler {
7
8
  build: (classes: string[]) => string;
@@ -124,11 +125,6 @@ function extractDeclsInner(css: string, selectorSet?: Set<string>): string {
124
125
  return result.join(";");
125
126
  }
126
127
 
127
- /** Tailwind's element-local stacks (`--tw-shadow`, `--tw-scale-x`, …): never theme values. */
128
- function isLocalVar(name: string): boolean {
129
- return name.startsWith("--tw-");
130
- }
131
-
132
128
  /**
133
129
  * The `@property` rules in a build output, keyed by variable name, whitespace-normalized
134
130
  * (`@property --tw-shadow { syntax: "*"; inherits: false; initial-value: 0 0 #0000; }`).
@@ -147,28 +143,6 @@ export function extractPropertyRules(cssOutput: string): Map<string, string> {
147
143
  return rules;
148
144
  }
149
145
 
150
- /**
151
- * `@layer theme { :root, :host { … } }` defining the theme vars `referenced` needs: each one and,
152
- * transitively, the vars its value references. Locals (`--tw-*`), vars the preface already defines,
153
- * and names Tailwind never emitted are skipped. Empty string when nothing is needed.
154
- */
155
- export function themeVarsBlock(referenced: Set<string>, themeVars: Map<string, string>, preface: string): string {
156
- const decls: string[] = [];
157
- const seen = new Set<string>();
158
- const queue = [...referenced];
159
- while (queue.length > 0) {
160
- const name = queue.shift() as string;
161
- if (seen.has(name) || isLocalVar(name)) continue;
162
- seen.add(name);
163
- if (new RegExp(`${name.replace(/[-]/g, "\\-")}\\s*:`).test(preface)) continue;
164
- const value = themeVars.get(name);
165
- if (value === undefined) continue;
166
- decls.push(`${name}: ${value};`);
167
- for (const m of value.matchAll(/var\((--[\w-]+)/g)) queue.push(m[1]);
168
- }
169
- return decls.length > 0 ? `@layer theme { :root, :host { ${decls.join(" ")} } }` : "";
170
- }
171
-
172
146
  function resolveThemeVars(css: string, themeVars: Map<string, string>): string {
173
147
  let result = css;
174
148
  for (let i = 0; i < 5; i++) {
@@ -57,7 +57,13 @@ declare module "virtual:flare-sx-manifest" {
57
57
  }
58
58
 
59
59
  declare module "virtual:flare-sx-dev-css" {
60
- /** Returns the current accumulated sx CSS string. Re-imported each SSR request to pick up HMR additions. */
60
+ /** Dev SSR transforms call this when a module runs, with the rules, `@property` rules and theme vars it emits. */
61
+ export function registerDevSx(
62
+ rules: Array<[cls: string, rule: string, layer: "sx" | "app"]>,
63
+ properties: Array<[name: string, rule: string]>,
64
+ referenced: string[],
65
+ ): void;
66
+ /** The sx stylesheet for every module the server runtime has run so far. */
61
67
  export function getDevSxCss(): string;
62
68
  /** Returns the current accumulated class-name list. Seeded into window state so client module injects dedupe against SSR-emitted classes. */
63
69
  export function getDevSxClasses(): string[];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Solid 2 hydrates `<head>` by walking its children from `head.firstChild` in render order, static
3
+ * elements (meta, title) and component regions (`<!--$-->…<!--/-->`) alike. Vite's
4
+ * transformIndexHtml prepends tags (`/@vite/client`, plugin head-prepend tags) ahead of them, so
5
+ * hydration would meet those first. Mark where Solid's output starts before the transform, then
6
+ * move whatever landed ahead of the mark to the end of `<head>`.
7
+ */
8
+ const SOLID_HEAD_START = "<!--flare-solid-head-->";
9
+
10
+ /** Marks the start of Solid's head children. Call on the SSR HTML before transformIndexHtml. */
11
+ export function markSolidHeadStart(html: string): string {
12
+ return html.replace(/<head(\s[^>]*)?>/i, (open) => `${open}${SOLID_HEAD_START}`);
13
+ }
14
+
15
+ /** Moves tags injected ahead of the mark to just before `</head>` and drops the mark. */
16
+ export function moveHeadPrependsAfterSolid(html: string): string {
17
+ const open = /<head(\s[^>]*)?>/i.exec(html);
18
+ if (!open) return html;
19
+ const start = open.index + open[0].length;
20
+ const mark = html.indexOf(SOLID_HEAD_START, start);
21
+ const end = html.indexOf("</head>", start);
22
+ if (mark === -1 || end === -1 || mark > end) return html;
23
+ const prepended = html.slice(start, mark);
24
+ const solidAndRest = html.slice(mark + SOLID_HEAD_START.length, end);
25
+ return html.slice(0, start) + solidAndRest + prepended + html.slice(end);
26
+ }
package/src/ssr/index.tsx CHANGED
@@ -32,7 +32,6 @@ import { ThemeProvider } from "../theme.ts";
32
32
  import { GLOBAL_DEFER, GLOBAL_QUERIES } from "../protocol.ts";
33
33
  import { parseSearchParams, type SearchParams } from "../url/index.ts";
34
34
  import { renderHeadToHtml } from "./head.ts";
35
- import { hoistHydrationHeadMarkers } from "./hoist-head-markers.ts";
36
35
  import { buildHeadPrefix } from "./head-prefix.ts";
37
36
 
38
37
  export { mergeHeadConfigs } from "../internal.ts";
@@ -548,7 +547,7 @@ function buildComponentTree(config: SSRConfig, flareStateScript: string): () =>
548
547
  * Returns updated buffer with head content injected, or original buffer if </head> not found.
549
548
  *
550
549
  * Head structure:
551
- * 1. Solid's hydratable head children (`<!--$-->` first — required for Solid 2)
550
+ * 1. Solid's head children, in render order (hydration walks them from `head.firstChild`)
552
551
  * 2. CSP nonce / viewport / theme / direction / locale scripts (headPrefix)
553
552
  * 3. modulepreload + stylesheets
554
553
  * 4. Resolved <head> tags (title, meta, etc.)
@@ -630,8 +629,7 @@ function injectHeadContent(
630
629
  headSuffix += `<style nonce="${escapedNonce}">body{${cssText}}</style>`;
631
630
  }
632
631
 
633
- const result = buffer.replace("</head>", `${headPrefix}${headSuffix}${extraSuffix}</head>`);
634
- return hoistHydrationHeadMarkers(result);
632
+ return buffer.replace("</head>", `${headPrefix}${headSuffix}${extraSuffix}</head>`);
635
633
  }
636
634
 
637
635
  /**
@@ -725,9 +723,9 @@ export function renderToStream(config: SSRConfig): SSRResult {
725
723
  try {
726
724
  /*
727
725
  * Resolve dev sx CSS before the read loop so both injectHeadContent call sites
728
- * (in-loop and flush) get the same snapshot. Dynamic import re-runs the virtual
729
- * module's load hook each time — no caching — so HMR additions are reflected.
730
- * Guarded by import.meta.env.DEV so prod builds tree-shake this entirely.
726
+ * (in-loop and flush) get the same snapshot. The virtual module is a registry every
727
+ * server-transformed module registers its rules into as it runs, so it covers every
728
+ * module this render used. Guarded by import.meta.env.DEV so prod builds tree-shake it.
731
729
  */
732
730
  let resolvedDevSxCss = "";
733
731
  let resolvedDevSxClasses: string[] = [];
@@ -1,25 +0,0 @@
1
- /**
2
- * Solid 2 hydratable `<head>` walks `head.firstChild` as `<!--$-->`.
3
- * Vite (`/@vite/client`) and Flare headPrefix used to prepend nodes, so
4
- * getNextMarker started on the wrong child and hydration threw
5
- * `Cannot read properties of null (reading 'firstChild'|'nextSibling')`.
6
- *
7
- * Move the Solid `<!--$-->…<!--/-->` region to the start of `<head>`.
8
- * Injected scripts/meta stay in `<head>` (still before `</head>` / body).
9
- */
10
- export function hoistHydrationHeadMarkers(html: string): string {
11
- const open = /<head[^>]*>/i.exec(html);
12
- if (!open) return html;
13
- const start = open.index + open[0].length;
14
- const end = html.indexOf("</head>", start);
15
- if (end === -1) return html;
16
- const inner = html.slice(start, end);
17
- const first = inner.indexOf("<!--$-->");
18
- if (first <= 0) return html;
19
- const last = inner.lastIndexOf("<!--/-->");
20
- if (last === -1 || last < first) return html;
21
- const closeAt = last + "<!--/-->".length;
22
- const solid = inner.slice(first, closeAt);
23
- const rest = inner.slice(0, first) + inner.slice(closeAt);
24
- return html.slice(0, start) + solid + rest + html.slice(end);
25
- }