@barocss/server 0.5.0 → 0.7.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/README.md CHANGED
@@ -8,6 +8,107 @@
8
8
 
9
9
  @barocss/server provides server-side utilities for parsing Tailwind classes and generating CSS without browser-specific features. Perfect for SSR, static site generation, and server-side CSS processing.
10
10
 
11
+ ## Recipe: SSR with a Tailwind build (Next.js App Router, Astro)
12
+
13
+ > `generateCssForHtml` and `ssrStyleTag` are **available from 0.7.0**.
14
+ >
15
+ > BaroCSS is JS-only: there is no CSS entry, so never `@import "@barocss/kit"` in CSS.
16
+
17
+ Use this when pages link a build stylesheet (a Tailwind or BaroCSS build) but render classes the build never saw, such as CMS blocks or model output. At request time, generate only the missing CSS and inline it, so the first paint is already styled:
18
+
19
+ ```ts
20
+ import fs from 'node:fs';
21
+ import { ServerRuntime, ssrStyleTag } from '@barocss/server';
22
+
23
+ // Once per server process: the runtime caches per-class results; read the shipped build CSS once.
24
+ const runtime = new ServerRuntime({ cssVarPrefix: 'tw', theme: { extend: siteTheme } });
25
+ const BUILD_CSS = fs.readFileSync('dist/app.css', 'utf8');
26
+
27
+ // Per request: this response's delta only (never cumulative across requests).
28
+ const css = runtime.generateCssForHtml(html, { skip: BUILD_CSS });
29
+ const tag = ssrStyleTag(css); // '<style data-barocss-ssr>…</style>': put it in <head>, after the build <link>
30
+ ```
31
+
32
+ - `generateCssForHtml(htmlOrClasses, { skip })` takes HTML or a class list. From HTML it reads the `class` attributes (any quoting, entities decoded) and ignores comments and `<script>`/`<style>` contents.
33
+ - `skip` is either the build CSS text or a set of class names. With CSS text it:
34
+ - skips the classes that lead its selectors (`.p-4`, `.md\:p-4` inside `@media`, `:where(.divide-y > …)`)
35
+ - doesn't re-emit the theme vars its `:root`/`:host` blocks declare
36
+ - doesn't re-emit its `@property` or `@keyframes` names
37
+
38
+ The output never contains `@layer` statements.
39
+ - The result is one ordered sheet (#267): each referenced theme var once, each `@property` block once, rules in Tailwind variant order.
40
+ - Also exported: `ssrStyleTag(css, { nonce })`, `SSR_STYLE_ATTRIBUTE`.
41
+
42
+ **Next.js App Router** (a server component; `html` is the CMS or model markup you render):
43
+
44
+ ```tsx
45
+ export default async function Page() {
46
+ const html = await getBlocksHtml();
47
+ const css = runtime.generateCssForHtml(html, { skip: BUILD_CSS });
48
+ return (
49
+ <>
50
+ <style data-barocss-ssr="" dangerouslySetInnerHTML={{ __html: css.replace(/<\/style/gi, '<\\/style') }} />
51
+ <div dangerouslySetInnerHTML={{ __html: html }} />
52
+ </>
53
+ );
54
+ }
55
+ ```
56
+
57
+ Don't give this `<style>` a `precedence` or `href`, so React leaves it where it is. It only has to come before the content it styles. When you render components rather than an HTML string, pass the class list instead: `runtime.generateCssForHtml(['p-4 sm:p-6', …], { skip: BUILD_CSS })`.
58
+
59
+ **Astro, SSR** (`src/middleware.ts`; read the emitted build CSS once at startup):
60
+
61
+ ```ts
62
+ import { defineMiddleware } from 'astro:middleware';
63
+ const dir = path.resolve('dist/client/_astro');
64
+ const BUILD_CSS = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => f.endsWith('.css')).map((f) => fs.readFileSync(path.join(dir, f), 'utf8')).join('\n') : '';
65
+
66
+ export const onRequest = defineMiddleware(async (_ctx, next) => {
67
+ const res = await next();
68
+ if (!res.headers.get('content-type')?.includes('text/html')) return res;
69
+ const html = await res.text();
70
+ const css = runtime.generateCssForHtml(html, { skip: BUILD_CSS });
71
+ const headers = new Headers(res.headers);
72
+ headers.delete('content-length');
73
+ return new Response(css ? html.replace('</head>', `${ssrStyleTag(css)}</head>`) : html, { status: res.status, headers });
74
+ });
75
+ ```
76
+
77
+ **Astro, static output:** do the same once in an integration's `astro:build:done` hook: read every `.css` under `dir` as the skip CSS, then rewrite every `.html`. The full recipe (shared config, static hook, client companion) is in the docs: `guide/integration/astro`.
78
+
79
+ **Config to copy from the build CSS** (use the same object on server and client):
80
+
81
+ ```ts
82
+ const config = {
83
+ cssVarPrefix: 'tw', // prefix(tw) build: also set prefix: 'tw' (BOTH are needed)
84
+ darkMode: 'class',
85
+ darkModeSelector: '[data-theme=dark] &', // the selector inside `@custom-variant dark (...)`; shadcn v4: '.dark &'
86
+ theme: { extend: { // your own theme: literal values, not var(--build-vars)
87
+ colors: { brand: '#2563eb' },
88
+ spacing: { gutter: '1.5rem' }, // named spacing: p-gutter
89
+ borderRadius: { lg: '0.75rem' }, // override existing keys; new radius/font names go in utilities
90
+ fontFamily: { sans: ['Inter', 'sans-serif'] },
91
+ } },
92
+ utilities: { 'max-w-app': { 'max-width': '48rem', 'margin-inline': 'auto' } }, // static @utility rules; `@utility name-*` unsupported
93
+ };
94
+ ```
95
+
96
+ **Client companion** (only needed when the page adds classes after load). Load `@barocss/browser` as usual; it adopts the `<style data-barocss-ssr>` sheet:
97
+
98
+ ```js
99
+ import { getRuntime } from '@barocss/browser';
100
+ const rt = getRuntime({ skipExisting: true, config: { cssVarPrefix: 'tw', theme: { extend: siteTheme } } });
101
+ rt.observe(document.body, { scan: true });
102
+ ```
103
+
104
+ Only a marked sheet that is in `<head>` when the runtime starts (at construction or the first `observe()`) is adopted. Put the tag in `<head>`, which streaming SSR sends first. A `<style data-barocss-ssr>` injected later, or placed in `<body>` (for example inside model or user HTML), is treated as an ordinary sheet. The client never regenerates the server's classes, and GC never reclaims them. Later client rules keep Tailwind's combined order with the server's rules: a client `sm:` rule never lands after a server `lg:` rule.
105
+
106
+ Measured with `scripts/ssr-probe` (#266/#268):
107
+ - first paint matched a full Tailwind build (1.0)
108
+ - still 1.0 after a later client addition
109
+ - 0 duplicate rules and 0 re-emitted build definitions
110
+ - about 0.2 ms per request on a warm runtime
111
+
11
112
  ## ✨ Key Features
12
113
 
13
114
  - **🚀 Server-Side CSS Generation** - Generate CSS on the server without browser APIs
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("@barocss/kit");class l{constructor(s={}){this.context=a.createContext(s)}parseClass(s){return a.parseClassToAst(s,this.context)}generateCss(s){const r=a.generateCssRules(s,this.context),t=new Set(r.flatMap(({rootCssList:e})=>e).filter(Boolean)),c=r.map(({css:e})=>e).filter(Boolean),o=this.colorVarsBlock([...t,...c].join(`
2
- `));return[...o?[o]:[],...t,...c].join(`
3
- `)}colorVarsBlock(s){const r=e=>[...e.matchAll(/var\((--[\w-]*color-[\w-]+)/g)].map(n=>n[1]),t=r(s);if(t.length===0)return"";const c=new Map;for(const e of this.context.themeToCssVars().matchAll(/^\s*(--[\w-]+):\s*(.+);$/gm))c.set(e[1],e[2]);const o=new Map;for(;t.length;){const e=t.pop(),n=c.get(e);n===void 0||o.has(e)||(o.set(e,n),t.push(...r(n)))}return o.size===0?"":`:root,:host {
4
- `+[...o].map(([e,n])=>` ${e}: ${n};`).join(`
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("@barocss/kit"),g="data-barocss-ssr",C={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" ",colon:":",sol:"/",lsqb:"[",rsqb:"]",num:"#",percnt:"%",lpar:"(",rpar:")",comma:",",period:".",excl:"!",tab:" ",newline:`
2
+ `};function S(c){return c.includes("&")?c.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi,(s,t)=>{if(t[0]==="#"){const e=t[1]==="x"||t[1]==="X"?parseInt(t.slice(2),16):parseInt(t.slice(1),10);return e>0&&e<=1114111?String.fromCodePoint(e):s}return C[t.toLowerCase()]??s}):c}const u=/<[a-zA-Z][^\s/>]*/g,p=/\s+([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?|\s*\//y,d=/\s*>/y;function y(c){const s=c.toLowerCase(),t=/<!--|<(script|style)\b/g;let e="",i=0,r;for(;r=t.exec(s);){e+=c.slice(i,r.index)+" ";let n;if(r[1]){const o=s.indexOf("</"+r[1],r.index+r[0].length);n=o<0?-1:s.indexOf(">",o),n>=0&&(n+=1)}else{const o=s.indexOf("-->",r.index+4);n=o<0?-1:o+3}if(n<0)return e;i=t.lastIndex=n}return e+c.slice(i)}function w(c){const s=y(c),t=new Set;for(u.lastIndex=0;u.exec(s);){let e=u.lastIndex,i;const r=[];for(p.lastIndex=e;(i=p.exec(s))&&i[0];p.lastIndex=e)e=p.lastIndex,i[1]?.toLowerCase()==="class"&&r.push(i[2]??i[3]??i[4]??"");if(d.lastIndex=e,!d.test(s)){u.lastIndex=e;continue}u.lastIndex=d.lastIndex;for(const n of r)for(const o of S(n).split(/\s+/))o&&t.add(o)}return[...t]}function k(c){return c.replace(/\\([0-9a-fA-F]{1,6})\s?|\\(.)/g,(s,t,e)=>t?String.fromCodePoint(parseInt(t,16)):e)}function T(c){const s=[];let t=0,e=0;for(let i=0;i<c.length;i++){const r=c[i];r==="\\"?i++:r==="("||r==="["?t++:r===")"||r==="]"?t--:r===","&&t===0&&(s.push(c.slice(e,i)),e=i+1)}return s.push(c.slice(e)),s}const I=/^\s*(?::(?:where|is)\(\s*)?\.((?:\\[0-9a-fA-F]{1,6}\s?|\\.|[\w-]|[^\x00-\x7F])+)/,v=/(^|[\s,(]):(root|host)\b/;function A(c){const s={classes:new Set,vars:new Set,properties:new Set,keyframes:new Set,layerStatement:!1},t=[];let e="";const i=r=>{if(t.some(n=>v.test(n)))for(const n of r.matchAll(/(?:^|;)\s*(--[\w-]+)\s*:/g))s.vars.add(n[1])};for(let r=0;r<c.length;r++){const n=c[r];if(n==="/"&&c[r+1]==="*"){const o=c.indexOf("*/",r+2);r=o<0?c.length:o+1;continue}if(n==='"'||n==="'"){let o=r+1;for(;o<c.length&&c[o]!==n;)o+=c[o]==="\\"?2:1;e+=c.slice(r,o+1),r=o;continue}if(n==="\\"){e+=c.slice(r,r+2),r++;continue}if(n==="{"){const o=e.lastIndexOf(";");o>=0&&i(e.slice(0,o+1));const a=e.slice(o+1).trim();e="",t.push(a);const l=/^@([\w-]+)\s*([\s\S]*)$/.exec(a);if(l){const f=l[1].toLowerCase();f==="property"?s.properties.add(l[2].trim()):f.endsWith("keyframes")&&s.keyframes.add(l[2].trim().replace(/^["']|["']$/g,""))}else if(!a.startsWith("&"))for(const f of T(a)){const x=I.exec(f);x&&s.classes.add(k(x[1]))}continue}if(n==="}"){i(e),e="",t.pop();continue}if(n===";"&&t.length===0){/^\s*@layer\b/.test(e)&&(s.layerStatement=!0),e="";continue}e+=n}return s}function R(c,s={}){const t=s.nonce?` nonce="${s.nonce.replace(/[&"<>]/g,e=>`&#${e.charCodeAt(0)};`)}"`:"";return`<style ${g}${t}>${c.replace(/<\/(style)/gi,"<\\/$1")}</style>`}const m=c=>[...c.matchAll(/var\((--[\w-]+)/g)].map(s=>s[1]);class E{constructor(s={},t={}){this.classCache=new Map,this.themeDefs=null,this.skipCache=new Map,this.cacheSize=Math.max(0,t.cacheSize??1e4),this.setConfig(s)}setConfig(s){this.context=h.createContext(s),this.classCache.clear(),this.themeDefs=null}parseClass(s){return h.parseClassToAst(s,this.context)}generateCss(s){return this.sheet(s.split(/\s+/).filter(Boolean))}generateCssForHtml(s,t={}){const e=typeof s=="string"?w(s):[...new Set(s.flatMap(n=>n.split(/\s+/)).filter(Boolean))],i=t.skip;if(i===void 0)return this.sheet(e);if(typeof i=="string"){const n=this.skipDefs(i);return this.sheet(e.filter(o=>!n.classes.has(o)),n)}const r=i instanceof Set?i:new Set(i);return this.sheet(e.filter(n=>!r.has(n)))}skipDefs(s){let t=this.skipCache.get(s);return t||(this.skipCache.size>=4&&this.skipCache.delete(this.skipCache.keys().next().value),this.skipCache.set(s,t=A(s))),t}sheet(s,t){const e=s.map(a=>this.entryFor(a));let i=this.uniqueRoots(e.flatMap(a=>a.roots));t&&(i=i.filter(({css:a,dedupeKey:l})=>{if(l.startsWith("--"))return!t.properties.has(l);const f=/^\s*@(?:-[\w]+-)?keyframes\s+([^\s{]+)/.exec(a)?.[1];return!f||!t.keyframes.has(f)}));const r=this.sortRules(e.filter(a=>a.css)),n=[...i.flatMap(a=>a.refs),...r.flatMap(a=>a.refs)],o=this.themeVarsBlock(n,t?.vars);return[...o?[o]:[],...i.map(a=>a.css),...r.map(a=>a.css)].join(`
3
+ `)}entryFor(s){const t=this.classCache.get(s);if(t)return this.classCache.delete(s),this.classCache.set(s,t),t;const e={css:"",key:null,refs:[],roots:[]};for(const{css:i,rootCssList:r}of h.generateCssRules(s,this.context)){i&&(e.css=i);for(const n of r){if(!n)continue;const o=/^\s*@property\s+(--[\w-]+)/.exec(n)?.[1]??n;e.roots.push({css:n,dedupeKey:o,refs:m(n)})}}return e.css&&(e.key=h.ruleSortKey(e.css),e.refs=m(e.css)),this.cacheSize>0&&(this.classCache.size>=this.cacheSize&&this.classCache.delete(this.classCache.keys().next().value),this.classCache.set(s,e)),e}generateCssForClasses(s){return s.map(t=>({className:t,css:this.generateCss(t)}))}uniqueRoots(s){const t=new Set;return s.filter(({dedupeKey:e})=>!t.has(e)&&!!t.add(e))}sortRules(s){return s.map((t,e)=>({entry:t,i:e})).sort((t,e)=>h.compareKeys(t.entry.key,e.entry.key)||t.i-e.i).map(({entry:t})=>t)}themeVarsBlock(s,t){const e=s;if(e.length===0)return"";let i=this.themeDefs;if(!i){i=this.themeDefs=new Map;for(const n of this.context.themeToCssVars().matchAll(/^\s*(--[\w-]+):\s*(.+);$/gm))i.set(n[1],n[2])}const r=new Map;for(;e.length;){const n=e.pop(),o=i.get(n);o===void 0||r.has(n)||t?.has(n)||(r.set(n,o),e.push(...m(o)))}return r.size===0?"":`:root,:host {
4
+ `+[...r].map(([n,o])=>` ${n}: ${o};`).join(`
5
5
  `)+`
6
- }`}generateCssForClasses(s){return s.map(t=>({className:t,css:this.generateCss(t)}))}}exports.ServerRuntime=l;
6
+ }`}}exports.SSR_STYLE_ATTRIBUTE=g;exports.ServerRuntime=E;exports.ssrStyleTag=R;
package/dist/index.d.ts CHANGED
@@ -1,31 +1,70 @@
1
1
  import { Config } from '@barocss/kit';
2
- /**
3
- * Server-side runtime for Barocss
4
- *
5
- * This provides server-side utilities for parsing classes and generating CSS
6
- * without browser-specific features like DOM manipulation or MutationObserver.
7
- */
2
+ export { ssrStyleTag, SSR_STYLE_ATTRIBUTE } from './ssr';
3
+ /** #268: what `generateCssForHtml` leaves out: build CSS text, or class names. */
4
+ export interface GenerateCssForHtmlOptions {
5
+ /**
6
+ * Build CSS text (the stylesheet the page already links) or a set of class names. With CSS, the
7
+ * classes leading its selectors are skipped, and so are the theme vars its `:root`/`:host` blocks
8
+ * declare, its `@property` and `@keyframes` names. With class names only classes are skipped.
9
+ */
10
+ skip?: string | Iterable<string>;
11
+ }
12
+ export interface ServerRuntimeOptions {
13
+ /** Max classes kept in the per-class generation cache (LRU). Default 10000; 0 disables caching. */
14
+ cacheSize?: number;
15
+ }
8
16
  export declare class ServerRuntime {
9
17
  private context;
10
- constructor(config?: Config);
18
+ private readonly cacheSize;
19
+ /** class -> generated entry; Map insertion order doubles as LRU order (#272). */
20
+ private classCache;
21
+ /** Theme var definitions parsed once from themeToCssVars() (#272). */
22
+ private themeDefs;
23
+ /** #268: parsed skip CSS by text. */
24
+ private skipCache;
25
+ constructor(config?: Config, options?: ServerRuntimeOptions);
26
+ /** Replace the config (theme included). Drops every cached result derived from the previous one. */
27
+ setConfig(config: Config): void;
11
28
  /**
12
29
  * Parse a class name and return its AST
13
30
  */
14
31
  parseClass(className: string): import('@barocss/kit').AstNode[];
15
32
  /**
16
- * Generate CSS for a class name
33
+ * Generate CSS for a class name (or whitespace-separated class names) as one complete sheet (#267):
34
+ * one `:root,:host` block defining every theme var the output references, each root/@property block
35
+ * once, then the class rules in Tailwind variant order (base < sm < md < lg ...).
17
36
  */
18
37
  generateCss(className: string): string;
19
38
  /**
20
- * #228: theme colours are emitted as var(--color-*). Define only the ones this output references
21
- * (the full theme block is large), including --color-* vars those definitions reference in turn.
39
+ * #268: the complete sheet (#267 order) for the classes one server response uses, minus what the
40
+ * page's build CSS already provides. `htmlOrClasses` is HTML (classes are read from `class`
41
+ * attributes; `<script>`/`<style>` contents and comments ignored) or a class list. Stateless per
42
+ * call: it returns this request's delta, never classes emitted for earlier requests. Wrap the result
43
+ * with `ssrStyleTag()` so `@barocss/browser` adopts it.
22
44
  */
23
- private colorVarsBlock;
45
+ generateCssForHtml(htmlOrClasses: string | string[], opts?: GenerateCssForHtmlOptions): string;
46
+ /** Parsed build CSS, memoised by text (a server passes the same build CSS on every request). */
47
+ private skipDefs;
48
+ private sheet;
49
+ /** Generate (or reuse) one class's rules. Same per-class output generateCssRules gives for a class list. */
50
+ private entryFor;
24
51
  /**
25
- * Parse multiple classes and return their CSS
52
+ * CSS per class, in input order. Each entry is self-contained (its own `:root,:host` vars block,
53
+ * @property blocks and variant-sorted rules), so entries repeat shared blocks and are not ordered
54
+ * against each other. For one complete sheet use `generateCss(classes.join(' '))`.
26
55
  */
27
56
  generateCssForClasses(classes: string[]): {
28
57
  className: string;
29
58
  css: string;
30
59
  }[];
60
+ /** Dedupe root-level blocks by content, and @property blocks by property name. */
61
+ private uniqueRoots;
62
+ /** Stable sort by the #254 variant key shared with @barocss/browser. */
63
+ private sortRules;
64
+ /**
65
+ * #228 generalised (#267): define every var(--x) the output references that the theme defines
66
+ * (radius, text, spacing, shadow, font, colour ...), including vars those definitions reference.
67
+ * themeToCssVars() already omits self-referencing entries (#260).
68
+ */
69
+ private themeVarsBlock;
31
70
  }
package/dist/index.es.js CHANGED
@@ -1,52 +1,255 @@
1
- import { createContext as c, parseClassToAst as l, generateCssRules as i } from "@barocss/kit";
2
- class p {
3
- constructor(e = {}) {
4
- this.context = c(e);
1
+ import { createContext as x, parseClassToAst as C, generateCssRules as g, ruleSortKey as w, compareKeys as y } from "@barocss/kit";
2
+ const S = "data-barocss-ssr", k = {
3
+ amp: "&",
4
+ lt: "<",
5
+ gt: ">",
6
+ quot: '"',
7
+ apos: "'",
8
+ nbsp: " ",
9
+ colon: ":",
10
+ sol: "/",
11
+ lsqb: "[",
12
+ rsqb: "]",
13
+ num: "#",
14
+ percnt: "%",
15
+ lpar: "(",
16
+ rpar: ")",
17
+ comma: ",",
18
+ period: ".",
19
+ excl: "!",
20
+ tab: " ",
21
+ newline: `
22
+ `
23
+ };
24
+ function I(c) {
25
+ return c.includes("&") ? c.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (s, t) => {
26
+ if (t[0] === "#") {
27
+ const e = t[1] === "x" || t[1] === "X" ? parseInt(t.slice(2), 16) : parseInt(t.slice(1), 10);
28
+ return e > 0 && e <= 1114111 ? String.fromCodePoint(e) : s;
29
+ }
30
+ return k[t.toLowerCase()] ?? s;
31
+ }) : c;
32
+ }
33
+ const h = /<[a-zA-Z][^\s/>]*/g, p = /\s+([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?|\s*\//y, u = /\s*>/y;
34
+ function T(c) {
35
+ const s = c.toLowerCase(), t = /<!--|<(script|style)\b/g;
36
+ let e = "", i = 0, o;
37
+ for (; o = t.exec(s); ) {
38
+ e += c.slice(i, o.index) + " ";
39
+ let n;
40
+ if (o[1]) {
41
+ const r = s.indexOf("</" + o[1], o.index + o[0].length);
42
+ n = r < 0 ? -1 : s.indexOf(">", r), n >= 0 && (n += 1);
43
+ } else {
44
+ const r = s.indexOf("-->", o.index + 4);
45
+ n = r < 0 ? -1 : r + 3;
46
+ }
47
+ if (n < 0) return e;
48
+ i = t.lastIndex = n;
49
+ }
50
+ return e + c.slice(i);
51
+ }
52
+ function A(c) {
53
+ const s = T(c), t = /* @__PURE__ */ new Set();
54
+ for (h.lastIndex = 0; h.exec(s); ) {
55
+ let e = h.lastIndex, i;
56
+ const o = [];
57
+ for (p.lastIndex = e; (i = p.exec(s)) && i[0]; p.lastIndex = e)
58
+ e = p.lastIndex, i[1]?.toLowerCase() === "class" && o.push(i[2] ?? i[3] ?? i[4] ?? "");
59
+ if (u.lastIndex = e, !u.test(s)) {
60
+ h.lastIndex = e;
61
+ continue;
62
+ }
63
+ h.lastIndex = u.lastIndex;
64
+ for (const n of o) for (const r of I(n).split(/\s+/)) r && t.add(r);
65
+ }
66
+ return [...t];
67
+ }
68
+ function v(c) {
69
+ return c.replace(/\\([0-9a-fA-F]{1,6})\s?|\\(.)/g, (s, t, e) => t ? String.fromCodePoint(parseInt(t, 16)) : e);
70
+ }
71
+ function R(c) {
72
+ const s = [];
73
+ let t = 0, e = 0;
74
+ for (let i = 0; i < c.length; i++) {
75
+ const o = c[i];
76
+ o === "\\" ? i++ : o === "(" || o === "[" ? t++ : o === ")" || o === "]" ? t-- : o === "," && t === 0 && (s.push(c.slice(e, i)), e = i + 1);
77
+ }
78
+ return s.push(c.slice(e)), s;
79
+ }
80
+ const D = /^\s*(?::(?:where|is)\(\s*)?\.((?:\\[0-9a-fA-F]{1,6}\s?|\\.|[\w-]|[^\x00-\x7F])+)/, E = /(^|[\s,(]):(root|host)\b/;
81
+ function $(c) {
82
+ const s = { classes: /* @__PURE__ */ new Set(), vars: /* @__PURE__ */ new Set(), properties: /* @__PURE__ */ new Set(), keyframes: /* @__PURE__ */ new Set(), layerStatement: !1 }, t = [];
83
+ let e = "";
84
+ const i = (o) => {
85
+ if (t.some((n) => E.test(n)))
86
+ for (const n of o.matchAll(/(?:^|;)\s*(--[\w-]+)\s*:/g)) s.vars.add(n[1]);
87
+ };
88
+ for (let o = 0; o < c.length; o++) {
89
+ const n = c[o];
90
+ if (n === "/" && c[o + 1] === "*") {
91
+ const r = c.indexOf("*/", o + 2);
92
+ o = r < 0 ? c.length : r + 1;
93
+ continue;
94
+ }
95
+ if (n === '"' || n === "'") {
96
+ let r = o + 1;
97
+ for (; r < c.length && c[r] !== n; ) r += c[r] === "\\" ? 2 : 1;
98
+ e += c.slice(o, r + 1), o = r;
99
+ continue;
100
+ }
101
+ if (n === "\\") {
102
+ e += c.slice(o, o + 2), o++;
103
+ continue;
104
+ }
105
+ if (n === "{") {
106
+ const r = e.lastIndexOf(";");
107
+ r >= 0 && i(e.slice(0, r + 1));
108
+ const a = e.slice(r + 1).trim();
109
+ e = "", t.push(a);
110
+ const l = /^@([\w-]+)\s*([\s\S]*)$/.exec(a);
111
+ if (l) {
112
+ const f = l[1].toLowerCase();
113
+ f === "property" ? s.properties.add(l[2].trim()) : f.endsWith("keyframes") && s.keyframes.add(l[2].trim().replace(/^["']|["']$/g, ""));
114
+ } else if (!a.startsWith("&"))
115
+ for (const f of R(a)) {
116
+ const m = D.exec(f);
117
+ m && s.classes.add(v(m[1]));
118
+ }
119
+ continue;
120
+ }
121
+ if (n === "}") {
122
+ i(e), e = "", t.pop();
123
+ continue;
124
+ }
125
+ if (n === ";" && t.length === 0) {
126
+ /^\s*@layer\b/.test(e) && (s.layerStatement = !0), e = "";
127
+ continue;
128
+ }
129
+ e += n;
130
+ }
131
+ return s;
132
+ }
133
+ function M(c, s = {}) {
134
+ const t = s.nonce ? ` nonce="${s.nonce.replace(/[&"<>]/g, (e) => `&#${e.charCodeAt(0)};`)}"` : "";
135
+ return `<style ${S}${t}>${c.replace(/<\/(style)/gi, "<\\/$1")}</style>`;
136
+ }
137
+ const d = (c) => [...c.matchAll(/var\((--[\w-]+)/g)].map((s) => s[1]);
138
+ class z {
139
+ constructor(s = {}, t = {}) {
140
+ this.classCache = /* @__PURE__ */ new Map(), this.themeDefs = null, this.skipCache = /* @__PURE__ */ new Map(), this.cacheSize = Math.max(0, t.cacheSize ?? 1e4), this.setConfig(s);
141
+ }
142
+ /** Replace the config (theme included). Drops every cached result derived from the previous one. */
143
+ setConfig(s) {
144
+ this.context = x(s), this.classCache.clear(), this.themeDefs = null;
5
145
  }
6
146
  /**
7
147
  * Parse a class name and return its AST
8
148
  */
9
- parseClass(e) {
10
- return l(e, this.context);
149
+ parseClass(s) {
150
+ return C(s, this.context);
11
151
  }
12
152
  /**
13
- * Generate CSS for a class name
153
+ * Generate CSS for a class name (or whitespace-separated class names) as one complete sheet (#267):
154
+ * one `:root,:host` block defining every theme var the output references, each root/@property block
155
+ * once, then the class rules in Tailwind variant order (base < sm < md < lg ...).
14
156
  */
15
- generateCss(e) {
16
- const r = i(e, this.context), t = new Set(r.flatMap(({ rootCssList: s }) => s).filter(Boolean)), a = r.map(({ css: s }) => s).filter(Boolean), o = this.colorVarsBlock([...t, ...a].join(`
17
- `));
18
- return [...o ? [o] : [], ...t, ...a].join(`
157
+ generateCss(s) {
158
+ return this.sheet(s.split(/\s+/).filter(Boolean));
159
+ }
160
+ /**
161
+ * #268: the complete sheet (#267 order) for the classes one server response uses, minus what the
162
+ * page's build CSS already provides. `htmlOrClasses` is HTML (classes are read from `class`
163
+ * attributes; `<script>`/`<style>` contents and comments ignored) or a class list. Stateless per
164
+ * call: it returns this request's delta, never classes emitted for earlier requests. Wrap the result
165
+ * with `ssrStyleTag()` so `@barocss/browser` adopts it.
166
+ */
167
+ generateCssForHtml(s, t = {}) {
168
+ const e = typeof s == "string" ? A(s) : [...new Set(s.flatMap((n) => n.split(/\s+/)).filter(Boolean))], i = t.skip;
169
+ if (i === void 0) return this.sheet(e);
170
+ if (typeof i == "string") {
171
+ const n = this.skipDefs(i);
172
+ return this.sheet(e.filter((r) => !n.classes.has(r)), n);
173
+ }
174
+ const o = i instanceof Set ? i : new Set(i);
175
+ return this.sheet(e.filter((n) => !o.has(n)));
176
+ }
177
+ /** Parsed build CSS, memoised by text (a server passes the same build CSS on every request). */
178
+ skipDefs(s) {
179
+ let t = this.skipCache.get(s);
180
+ return t || (this.skipCache.size >= 4 && this.skipCache.delete(this.skipCache.keys().next().value), this.skipCache.set(s, t = $(s))), t;
181
+ }
182
+ sheet(s, t) {
183
+ const e = s.map((a) => this.entryFor(a));
184
+ let i = this.uniqueRoots(e.flatMap((a) => a.roots));
185
+ t && (i = i.filter(({ css: a, dedupeKey: l }) => {
186
+ if (l.startsWith("--")) return !t.properties.has(l);
187
+ const f = /^\s*@(?:-[\w]+-)?keyframes\s+([^\s{]+)/.exec(a)?.[1];
188
+ return !f || !t.keyframes.has(f);
189
+ }));
190
+ const o = this.sortRules(e.filter((a) => a.css)), n = [...i.flatMap((a) => a.refs), ...o.flatMap((a) => a.refs)], r = this.themeVarsBlock(n, t?.vars);
191
+ return [...r ? [r] : [], ...i.map((a) => a.css), ...o.map((a) => a.css)].join(`
19
192
  `);
20
193
  }
194
+ /** Generate (or reuse) one class's rules. Same per-class output generateCssRules gives for a class list. */
195
+ entryFor(s) {
196
+ const t = this.classCache.get(s);
197
+ if (t)
198
+ return this.classCache.delete(s), this.classCache.set(s, t), t;
199
+ const e = { css: "", key: null, refs: [], roots: [] };
200
+ for (const { css: i, rootCssList: o } of g(s, this.context)) {
201
+ i && (e.css = i);
202
+ for (const n of o) {
203
+ if (!n) continue;
204
+ const r = /^\s*@property\s+(--[\w-]+)/.exec(n)?.[1] ?? n;
205
+ e.roots.push({ css: n, dedupeKey: r, refs: d(n) });
206
+ }
207
+ }
208
+ return e.css && (e.key = w(e.css), e.refs = d(e.css)), this.cacheSize > 0 && (this.classCache.size >= this.cacheSize && this.classCache.delete(this.classCache.keys().next().value), this.classCache.set(s, e)), e;
209
+ }
21
210
  /**
22
- * #228: theme colours are emitted as var(--color-*). Define only the ones this output references
23
- * (the full theme block is large), including --color-* vars those definitions reference in turn.
211
+ * CSS per class, in input order. Each entry is self-contained (its own `:root,:host` vars block,
212
+ * @property blocks and variant-sorted rules), so entries repeat shared blocks and are not ordered
213
+ * against each other. For one complete sheet use `generateCss(classes.join(' '))`.
24
214
  */
25
- colorVarsBlock(e) {
26
- const r = (s) => [...s.matchAll(/var\((--[\w-]*color-[\w-]+)/g)].map((n) => n[1]), t = r(e);
27
- if (t.length === 0) return "";
28
- const a = /* @__PURE__ */ new Map();
29
- for (const s of this.context.themeToCssVars().matchAll(/^\s*(--[\w-]+):\s*(.+);$/gm)) a.set(s[1], s[2]);
215
+ generateCssForClasses(s) {
216
+ return s.map((t) => ({ className: t, css: this.generateCss(t) }));
217
+ }
218
+ /** Dedupe root-level blocks by content, and @property blocks by property name. */
219
+ uniqueRoots(s) {
220
+ const t = /* @__PURE__ */ new Set();
221
+ return s.filter(({ dedupeKey: e }) => !t.has(e) && !!t.add(e));
222
+ }
223
+ /** Stable sort by the #254 variant key shared with @barocss/browser. */
224
+ sortRules(s) {
225
+ return s.map((t, e) => ({ entry: t, i: e })).sort((t, e) => y(t.entry.key, e.entry.key) || t.i - e.i).map(({ entry: t }) => t);
226
+ }
227
+ /**
228
+ * #228 generalised (#267): define every var(--x) the output references that the theme defines
229
+ * (radius, text, spacing, shadow, font, colour ...), including vars those definitions reference.
230
+ * themeToCssVars() already omits self-referencing entries (#260).
231
+ */
232
+ themeVarsBlock(s, t) {
233
+ const e = s;
234
+ if (e.length === 0) return "";
235
+ let i = this.themeDefs;
236
+ if (!i) {
237
+ i = this.themeDefs = /* @__PURE__ */ new Map();
238
+ for (const n of this.context.themeToCssVars().matchAll(/^\s*(--[\w-]+):\s*(.+);$/gm)) i.set(n[1], n[2]);
239
+ }
30
240
  const o = /* @__PURE__ */ new Map();
31
- for (; t.length; ) {
32
- const s = t.pop(), n = a.get(s);
33
- n === void 0 || o.has(s) || (o.set(s, n), t.push(...r(n)));
241
+ for (; e.length; ) {
242
+ const n = e.pop(), r = i.get(n);
243
+ r === void 0 || o.has(n) || t?.has(n) || (o.set(n, r), e.push(...d(r)));
34
244
  }
35
245
  return o.size === 0 ? "" : `:root,:host {
36
- ` + [...o].map(([s, n]) => ` ${s}: ${n};`).join(`
246
+ ` + [...o].map(([n, r]) => ` ${n}: ${r};`).join(`
37
247
  `) + `
38
248
  }`;
39
249
  }
40
- /**
41
- * Parse multiple classes and return their CSS
42
- */
43
- generateCssForClasses(e) {
44
- return e.map((t) => ({
45
- className: t,
46
- css: this.generateCss(t)
47
- }));
48
- }
49
250
  }
50
251
  export {
51
- p as ServerRuntime
252
+ S as SSR_STYLE_ATTRIBUTE,
253
+ z as ServerRuntime,
254
+ M as ssrStyleTag
52
255
  };
package/dist/ssr.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * #268: SSR helpers: class extraction from HTML, parsing a build stylesheet into what it already
3
+ * defines (classes, theme vars, @property, @keyframes, @layer order), and the marked <style> tag the
4
+ * browser runtime recognises. Text only: no DOM, no CSS parser dependency.
5
+ */
6
+ /** Attribute that marks the server sheet. `@barocss/browser` adopts the rules of `<style data-barocss-ssr>`. */
7
+ export declare const SSR_STYLE_ATTRIBUTE = "data-barocss-ssr";
8
+ /** Decode the HTML character references an attribute value may contain. */
9
+ export declare function decodeHtmlEntities(s: string): string;
10
+ /**
11
+ * Class names from every `class` attribute in `html`, first-seen order, deduplicated. Handles double,
12
+ * single and unquoted values, any whitespace and character references; ignores comments and the
13
+ * contents of `<script>` and `<style>`.
14
+ */
15
+ export declare function extractClasses(html: string): string[];
16
+ /** What a build stylesheet already defines. */
17
+ export interface CssDefinitions {
18
+ /** Classes that lead a selector (the #210 rule: `.p-4`, `.md\:p-4` in @media, `.hover\:x:hover`). */
19
+ classes: Set<string>;
20
+ /** Custom properties declared in `:root` / `:host` blocks. */
21
+ vars: Set<string>;
22
+ /** `@property` names. */
23
+ properties: Set<string>;
24
+ /** `@keyframes` names. */
25
+ keyframes: Set<string>;
26
+ /** Whether the sheet has a top-level `@layer a, b;` order statement. */
27
+ layerStatement: boolean;
28
+ }
29
+ /**
30
+ * Parse CSS text (e.g. Tailwind or BaroCSS build output) into the names it defines. A small brace
31
+ * scanner that honours comments, strings and escapes; nested rules and @layer/@media/@supports groups
32
+ * are walked. Same leading-class rule as the browser's #210 `skipExisting`.
33
+ */
34
+ export declare function parseCssDefinitions(css: string): CssDefinitions;
35
+ /** `<style data-barocss-ssr>` around `css`; a `</style` inside the CSS is neutralised. */
36
+ export declare function ssrStyleTag(css: string, options?: {
37
+ nonce?: string;
38
+ }): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barocss/server",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/barocss/barocss.git",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "jsdom": "^26.1.0",
29
- "@barocss/kit": "0.5.0"
29
+ "@barocss/kit": "0.7.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "vite": "^7.1.3",