domflax 0.1.2 → 0.2.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.
Files changed (49) hide show
  1. package/README.md +66 -31
  2. package/dist/chunk-EYQXQQQH.js +336 -0
  3. package/dist/chunk-EYQXQQQH.js.map +1 -0
  4. package/dist/{chunk-DNHOGPYV.js → chunk-FPT4EJ6Q.js} +1100 -1551
  5. package/dist/chunk-FPT4EJ6Q.js.map +1 -0
  6. package/dist/chunk-JBM3MJRM.js +382 -0
  7. package/dist/chunk-JBM3MJRM.js.map +1 -0
  8. package/dist/{chunk-DWLB7FRR.js → chunk-TTJEXWAC.js} +322 -9
  9. package/dist/chunk-TTJEXWAC.js.map +1 -0
  10. package/dist/{chunk-6WVVF6AD.js → chunk-U5GOONKV.js} +5 -2
  11. package/dist/{chunk-6WVVF6AD.js.map → chunk-U5GOONKV.js.map} +1 -1
  12. package/dist/cli.cjs +3010 -2789
  13. package/dist/cli.cjs.map +1 -1
  14. package/dist/cli.js +268 -232
  15. package/dist/cli.js.map +1 -1
  16. package/dist/index.cjs +1684 -1649
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +255 -498
  19. package/dist/index.d.ts +255 -498
  20. package/dist/index.js +17 -37
  21. package/dist/{pattern-F5xBtIE-.d.cts → pattern-DotR_dHs.d.cts} +1 -1
  22. package/dist/pattern-kit.cjs +60 -1
  23. package/dist/pattern-kit.cjs.map +1 -1
  24. package/dist/pattern-kit.d.cts +2 -2
  25. package/dist/pattern-kit.d.ts +2 -2
  26. package/dist/pattern-kit.js +2 -2
  27. package/dist/{pattern-CV607P87.d.ts → pattern-urm5uuwj.d.ts} +1 -1
  28. package/dist/{resolve-ops-DIwEelH-.d.ts → resolve-ops-D8aQina5.d.cts} +20 -0
  29. package/dist/{resolve-ops-DIwEelH-.d.cts → resolve-ops-D8aQina5.d.ts} +20 -0
  30. package/dist/verify.d.cts +1 -1
  31. package/dist/verify.d.ts +1 -1
  32. package/dist/verify.js +1 -1
  33. package/dist/webpack-loader.cjs +1615 -1633
  34. package/dist/webpack-loader.cjs.map +1 -1
  35. package/dist/webpack-loader.d.cts +8 -2
  36. package/dist/webpack-loader.d.ts +8 -2
  37. package/dist/webpack-loader.js +8 -5
  38. package/dist/webpack-loader.js.map +1 -1
  39. package/dist/worker.cjs +5337 -0
  40. package/dist/worker.cjs.map +1 -0
  41. package/dist/worker.d.cts +2 -0
  42. package/dist/worker.d.ts +2 -0
  43. package/dist/worker.js +72 -0
  44. package/dist/worker.js.map +1 -0
  45. package/package.json +4 -2
  46. package/dist/chunk-DNHOGPYV.js.map +0 -1
  47. package/dist/chunk-DOQEBGWB.js +0 -188
  48. package/dist/chunk-DOQEBGWB.js.map +0 -1
  49. package/dist/chunk-DWLB7FRR.js.map +0 -1
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  > Compile-time DOM flattener and semantic CSS compressor — fewer DOM nodes, smaller class sets, **identical rendered UI**.
4
4
 
5
- `domflax` analyzes your JSX at build time and rewrites it to a smaller equivalent:
5
+ `domflax` analyzes your JSX **and HTML** at build time and rewrites it to a smaller equivalent:
6
6
 
7
- 1. **Compress** — collapses verbose class sets into their shortest equivalents (`px-4 py-4 mt-2 mb-2` → `p-4 my-2`, `h-10 w-10` → `size-10`).
7
+ 1. **Compress** — a general engine rewrites each element's classes to the **shortest set that produces the same computed style** (`px-4 py-4 mt-2 mb-2` → `p-4 my-2`, `h-10 w-10` → `size-10`). One algorithm across **Tailwind v3, Tailwind v4, and custom CSS** — no per-utility patterns.
8
8
  2. **Flatten** — removes wrapper elements that are *provably inert* (they add no layout and paint nothing).
9
9
 
10
10
  Matching happens on **computed styles**, not raw class names — so the rules work across Tailwind, custom CSS, and (later) other providers, and a Tailwind class and an equivalent custom class compress the same way.
@@ -24,10 +24,10 @@ It rewrites only the **static shape** of your markup. Dynamic class lists (`clas
24
24
  **Safety model — conservative by default, no browser involved.**
25
25
 
26
26
  - **Compression is always safe.** It only re-serializes an element's *own* class list, so a `ref`, an event handler, a `{dynamic}` child, or `dangerouslySetInnerHTML` never blocks it — only a *dynamic* className (or a class a CSS selector depends on) is left alone.
27
- - **Flattening is conservative.** A wrapper is removed only when removal is *provably* render-neutral — it establishes no layout context and has no style to reproduce on its child. It never drops a style it can't reproduce, and never touches a wrapper a CSS selector depends on (`.list > .item h3`).
27
+ - **Flattening is conservative.** A wrapper is removed only when removal is *provably* render-neutral — it establishes no layout context and has no style to reproduce on its child. A `flex`/`grid` **centering** wrapper is removed only when its parent is statically `display:grid` (so `place-self:center` is provably equivalent — Chromium-verified); a flex/block/unknown parent leaves it preserved. It never drops a style it can't reproduce, and never touches a wrapper a CSS selector depends on (`.list > .item h3`).
28
28
  - domflax runs as a **purely static** source transform. It never launches a browser, so builds stay fast and deterministic.
29
29
 
30
- > **Status: v0.1.2.** Works end-to-end on real `.jsx`/`.tsx` — in component-return position **and inside `.map()` / expressions (list rows)** — via Vite, Next.js (webpack), and the CLI, with Tailwind and custom-CSS providers. 22 patterns. Wrappers that establish a layout context (e.g. `flex`/`grid` centering) are **conservatively preserved** proving those render-identical needs context a static pass can't see; recovering them safely is on the Roadmap. APIs may change before 1.0.
30
+ > **Status: v0.2.0.** Optimizes real `.jsx`/`.tsx` **and `.html`** — component-return, inside `.map()`/expressions, and whole static-HTML sites — via Vite, Next.js (webpack), and the CLI. **Compression is one general engine** that emits the shortest class set reproducing each element's computed style, uniformly across **Tailwind v3, Tailwind v4, and custom CSS** (it re-resolves the result and verifies it's identical before emitting). **Flattening** is a lean set of provably-safe structural patterns (inert wrappers + grid-parent centering). **Static-onlynever launches a browser** during a build; a Tailwind project it can't resolve is left untouched, never broken. The CLI batches large sites across CPU cores (`--max-memory`, never OOM) and auto-detects each HTML page's own `<link>` stylesheets; the Vite/Next plugins print a build-end optimization summary. APIs may change before 1.0.
31
31
 
32
32
  ## Install
33
33
 
@@ -67,9 +67,21 @@ module.exports = {
67
67
 
68
68
  > domflax runs as a **source transform** on your `.jsx`/`.tsx` files via the bundler — it never touches a framework's shipped `index.html`. Use `next build` (webpack); **Turbopack is not supported yet** (it doesn't accept arbitrary webpack loaders).
69
69
 
70
+ At the end of the build both plugins print a one-box summary of what domflax did:
71
+
72
+ ```
73
+ ▲ domflax
74
+ ────────────────────────────────
75
+ files optimized 42
76
+ DOM nodes removed 318
77
+ classes compressed 1,204
78
+ size saved 18.7 KB
79
+ ────────────────────────────────
80
+ ```
81
+
70
82
  ### Tailwind (auto-detected)
71
83
 
72
- When `tailwindcss` is present, `provider: 'auto'` resolves classes through the real Tailwind engine and emits the shortest equivalent Tailwind classes back. `tailwindcss` is an optional peer, loaded from your project only when used.
84
+ When `tailwindcss` is present, `provider: 'auto'` resolves classes through your project's real Tailwind engine — **Tailwind v3 and v4 are both supported** — and emits the shortest equivalent classes back. `tailwindcss` is an optional peer, loaded from your project only when used. A Tailwind version domflax can't resolve is left untouched (never broken).
73
85
 
74
86
  ### Custom CSS files
75
87
 
@@ -96,32 +108,55 @@ npx domflax ./src --out ./domflax-out
96
108
  | `<path>` | Folder (auto-scanned) or glob of files. |
97
109
  | `--out <dir>` | Write optimized output here (mirrors input structure). |
98
110
  | `--provider <name>` | `auto` (default), `tailwind`, or `custom`. |
99
- | `--css <files...>` | Stylesheets when `--provider custom`. |
111
+ | `--css <files...>` | **Global** stylesheets (`--provider custom`); each `.html` page's own `<link>` imports are auto-detected on top. |
112
+ | `--max-memory <MB>` | Cap total RAM — and thus worker parallelism. Default ≈ 70% of free RAM; low values run slower but never OOM. |
113
+ | `--concurrency <N>` | Cap worker count (memory always wins). |
100
114
  | `--dry-run` | Preview changes, write nothing. |
115
+ | `--details` | Print per-file optimization stats (nodes / classes / bytes). |
101
116
  | `--dangerously-overwrite-source` | Allow in-place source rewrite (needs clean git). |
102
117
 
118
+ ### HTML & static sites
119
+
120
+ domflax optimizes `.html`/`.htm` too (parse5), so you can run it over a **built static site** (`dist/`):
121
+
122
+ ```bash
123
+ npx domflax ./dist --provider custom --out ./dist-optimized
124
+ ```
125
+
126
+ - **Per-page CSS, automatically.** Each HTML file resolves against the stylesheets *it* links (`<link rel="stylesheet">`, relative + local) plus any global `--css` — so you usually don't select CSS at all, and selector-safety is accurate per page.
127
+ - **Centering actually flattens here.** In HTML the parent is statically known, so a `grid`-parent centering wrapper is provably removable (Chromium-verified) — real node removal, not just compression.
128
+ - **Big sites, safely parallel.** Large batches run across CPU cores with a memory-bounded worker pool: `--max-memory` caps RAM (and parallelism); a bad or huge file fails just that file (reported), never crashing or OOM-ing the run.
129
+ - **Byte-for-byte outside edits** — doctype, comments, whitespace, scripts, and attribute order are preserved; only changed `class` values and unwrapped tags are touched.
130
+
103
131
  ## Writing a pattern
104
132
 
105
- Patterns are how domflax knows what's safe to rewrite. Each is a **single declarative file** the definition and its tests live in one `definePattern` call, with no separate test file and no manual registration:
133
+ Compression is a general engine — there are **no per-utility compress patterns**. Patterns are for **flattening**: each is a single declarative file whose definition and tests live in one `definePattern` call, auto-discovered, with no manual registration:
106
134
 
107
135
  ```ts
108
- import { definePattern } from 'domflax/pattern-kit'
136
+ import { definePattern, not, hasDynamicClasses } from 'domflax/pattern-kit'
109
137
 
110
138
  export default definePattern({
111
- name: 'padding-shorthand',
112
- category: 'compress/padding-shorthand',
113
- safety: 1,
114
- doc: { summary: 'Equal/paired padding longhands collapse to the shortest shorthand.' },
115
- // a compress recipe rewrites only the element's own class list (declines with null otherwise)
116
- rewrite: { rewriteClasses: (computed) => foldPadding(computed) },
139
+ name: 'display-contents-wrapper',
140
+ category: 'flatten/wrapper/display-contents-wrapper',
141
+ safety: 2,
142
+ doc: { summary: 'A display:contents wrapper generates no box unwrap it into its sole child.' },
143
+ match: {
144
+ tag: 'div',
145
+ style: { display: 'contents' },
146
+ onlyChild: 'element',
147
+ paintsNothing: true,
148
+ where: [not(hasDynamicClasses)],
149
+ },
150
+ rewrite: { flattenInto: 'child' },
117
151
  test: {
118
- cases: [{ before: '<div className="px-4 py-4">{x}</div>', after: '<div className="p-4">{x}</div>' }],
119
- noMatch: ['<div className="pt-2 pr-4 pb-8 pl-4">box</div>'],
152
+ cases: [{ before: '<div className="contents"><a className="text-blue-500">L</a></div>',
153
+ after: '<a className="text-blue-500">L</a>' }],
154
+ noMatch: ['<div className="contents" ref={r}><a>L</a></div>'],
120
155
  },
121
156
  })
122
157
  ```
123
158
 
124
- Drop the file under `src/library/**` as `*.pattern.ts` and it's **auto-discovered**. The generic harness runs every pattern's `test` cases through the *real* transform, plus an automatic invariant suite (purity, opacity-barrier safety, id-preservation, fixpoint termination) so a new pattern is wired, tested, and proven sound with zero boilerplate. Flatten patterns auto-receive the opacity + selector-safety guards; compress patterns are gated only on dynamic / selector-bound classes.
159
+ Drop the file under `src/library/**` as `*.pattern.ts` and it's **auto-discovered**. The generic harness runs its `test` cases through the *real* transform, plus an automatic invariant suite (purity, opacity-barrier safety, id-preservation, fixpoint termination). Every `flatten/*` pattern auto-receives the opacity + selector-safety guards, and the conservative safety gate only commits a removal it can *prove* is render-neutral so a pattern can never produce unsafe output.
125
160
 
126
161
  ## Advanced entry points
127
162
 
@@ -134,23 +169,23 @@ The transform itself is static and never launches a browser. `domflax/verify` is
134
169
 
135
170
  ## Examples
136
171
 
137
- Runnable examples live in [`examples/`](./examples): `vite-react-tailwind`, `vite-custom-css` (custom provider + selector-safety), and `next-tailwind`.
172
+ Runnable examples live in [`examples/`](./examples): `vite-react-tailwind`, `vite-custom-css` (custom provider + selector-safety), `next-tailwind`, and `static-html` (CLI optimizing a plain `.html` page with per-page `<link>` CSS auto-detection).
138
173
 
139
174
  ## Roadmap
140
175
 
141
- - [x] Monorepo + single bundled package
142
- - [x] Core engine (IR, pass manager, surgical full-module codegen)
143
- - [x] Declarative `definePattern({ …, test })` + auto-discovery; 22 flatten/compress patterns
144
- - [x] Real Tailwind engine + custom-CSS resolvers
145
- - [x] CSS selector-safety + residual-skip (don't break `div div h1`; never drop un-reproducible styles)
146
- - [x] Compression across dynamic content (refs / handlers / `{expr}` children)
147
- - [x] Optimize JSX inside `.map()` / expressions (list rows)
148
- - [x] Vite + Next.js (webpack) adapters + CLI (folders, wizard, output-safety)
149
- - [x] Standalone equivalence verifier (Playwright, opt-in)
150
- - [ ] Context-aware (or opt-in-verified) flatten for `flex`/`grid` centering wrappers
151
- - [ ] HTML frontend (plain `.html` / Astro static)
152
- - [ ] `domflax/runtime` — optimize dynamic HTML strings before `innerHTML`
153
- - [ ] More providers; `templatize` (plain-HTML cloneNode)
176
+ Done so far: monorepo + single bundled package · core IR/pass engine with surgical codegen · declarative `definePattern` + auto-discovery · the general compress **engine** (Tailwind v3 + v4 + custom CSS) · Tailwind v4 support + fail-safe · selector-safety & residual-skip · compression across dynamic content and inside `.map()` rows · Vite + Next.js adapters with a build-end summary · HTML frontend with per-page `<link>` CSS auto-detection · grid-parent centering flatten (Chromium-verified) · memory-bounded parallel CLI.
177
+
178
+ Where it's going each release adds the engine capability that unlocks its next validated pattern batch (full details in [`docs/ROADMAP.md`](./docs/ROADMAP.md)):
179
+
180
+ | Version | Theme | Patterns |
181
+ | --- | --- | --- |
182
+ | **0.3.0** | **Reach** `cn()`/template-literal static extraction, arbitrary-value + variant-aware compression, deeper static layout reasoning (margin-collapse, item sizing) | ~25 |
183
+ | **0.4.0** | **Verified tier** — opt-in render-verified flattening for static HTML (real pages, pixel-identical or rejected); unlocks animation-wrapper class-transfer, multi-child unwraps, merges | ~60 |
184
+ | **0.5.0** | **More frontends** — Astro static + Vue SFC frontends, Turbopack, Bootstrap/other providers for the compress engine | ~100 |
185
+ | **0.6.0** | **Perf & ecosystem** incremental/watch caching, `domflax/runtime`, `templatize` (cloneNode), community `domflax-pattern-*` packages | ~140 |
186
+ | **1.0.0** | **Stable** frozen API, semver guarantees, docs site, published benchmarks | **200+** |
187
+
188
+ Every pattern that ships must *uniquely fire on real code* and be proven render-neutral (statically or via the verified tier) — the count grows from new capability surface, never from padding.
154
189
 
155
190
  ## License
156
191
 
@@ -0,0 +1,336 @@
1
+ import {
2
+ builtinPatterns,
3
+ createCssResolver,
4
+ createHtmlBackend,
5
+ createHtmlFrontend,
6
+ createJsxBackend,
7
+ createJsxFrontend,
8
+ createTailwindResolver
9
+ } from "./chunk-FPT4EJ6Q.js";
10
+ import {
11
+ buildSelectorIndex,
12
+ createSyntheticSink,
13
+ normalizer,
14
+ runPasses,
15
+ syncClassesFromComputed
16
+ } from "./chunk-TTJEXWAC.js";
17
+ import {
18
+ init_esm_shims
19
+ } from "./chunk-U5GOONKV.js";
20
+
21
+ // ../cli/src/safety.ts
22
+ init_esm_shims();
23
+ import { execFileSync } from "child_process";
24
+ import * as path from "path";
25
+ var DISPOSABLE_DIRS = /* @__PURE__ */ new Set(["dist", "build", "out", ".next"]);
26
+ function isDisposablePath(file) {
27
+ return path.resolve(file).split(path.sep).some((seg) => DISPOSABLE_DIRS.has(seg));
28
+ }
29
+ function isGitClean(cwd) {
30
+ try {
31
+ const out = execFileSync("git", ["status", "--porcelain"], {
32
+ cwd,
33
+ encoding: "utf8",
34
+ stdio: ["ignore", "pipe", "ignore"]
35
+ });
36
+ return out.trim().length === 0;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ function planWrites(options, gitClean) {
42
+ if (options.dangerouslyOverwriteSource) {
43
+ if (!options.noGitCheck && !gitClean) {
44
+ return {
45
+ ok: false,
46
+ error: "refusing --dangerously-overwrite-source: git working tree is not clean. Commit or stash first, or pass --no-git-check to override."
47
+ };
48
+ }
49
+ return { ok: true, value: { mode: "overwrite-source", outDir: null } };
50
+ }
51
+ const outDir = path.resolve(options.out ?? "domflax-out");
52
+ return { ok: true, value: { mode: "out-dir", outDir } };
53
+ }
54
+ function destinationFor(file, inputRoot, plan) {
55
+ const absFile = path.resolve(file);
56
+ if (plan.mode === "overwrite-source") {
57
+ return { ok: true, value: absFile };
58
+ }
59
+ const outDir = plan.outDir;
60
+ const rel = path.relative(inputRoot, absFile);
61
+ const safeRel = rel === "" || rel.startsWith("..") || path.isAbsolute(rel) ? path.basename(absFile) : rel;
62
+ const dest = path.join(outDir, safeRel);
63
+ if (path.resolve(dest) === absFile && !isDisposablePath(absFile)) {
64
+ return {
65
+ ok: false,
66
+ error: `refusing to overwrite source file ${absFile}: the output path resolves onto the source. Choose a different --out, or pass --dangerously-overwrite-source (with a clean git tree).`
67
+ };
68
+ }
69
+ return { ok: true, value: dest };
70
+ }
71
+
72
+ // ../cli/src/transform.ts
73
+ init_esm_shims();
74
+ import * as path3 from "path";
75
+
76
+ // ../cli/src/html-css.ts
77
+ init_esm_shims();
78
+ import { createHash } from "crypto";
79
+ import { existsSync } from "fs";
80
+ import * as path2 from "path";
81
+ function isRemoteHref(href) {
82
+ const h = href.trim();
83
+ return h.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(h);
84
+ }
85
+ function attrValue(tag, name) {
86
+ const re = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'>]+))`, "i");
87
+ const m = re.exec(tag);
88
+ if (!m) return null;
89
+ return (m[1] ?? m[2] ?? m[3] ?? "").trim();
90
+ }
91
+ function extractLinkHrefs(html) {
92
+ const out = [];
93
+ const linkRe = /<link\b[^>]*>/gi;
94
+ let m;
95
+ while ((m = linkRe.exec(html)) !== null) {
96
+ const tag = m[0];
97
+ const rel = attrValue(tag, "rel");
98
+ if (rel === null || !/(?:^|\s)stylesheet(?:\s|$)/i.test(rel)) continue;
99
+ const href = attrValue(tag, "href");
100
+ if (href) out.push(href);
101
+ }
102
+ return out;
103
+ }
104
+ function extractInlineStyles(html) {
105
+ const out = [];
106
+ const styleRe = /<style\b([^>]*)>([\s\S]*?)<\/style>/gi;
107
+ let m;
108
+ while ((m = styleRe.exec(html)) !== null) {
109
+ const type = attrValue(`<style ${m[1] ?? ""}>`, "type")?.toLowerCase();
110
+ if (type && type !== "text/css" && type !== "css") continue;
111
+ const css = m[2] ?? "";
112
+ if (css.trim().length > 0) out.push(css);
113
+ }
114
+ return out;
115
+ }
116
+ function extractHtmlStylesheets(htmlCode, htmlAbsPath) {
117
+ const dir = path2.dirname(path2.resolve(htmlAbsPath));
118
+ const files = [];
119
+ const seen = /* @__PURE__ */ new Set();
120
+ for (const rawHref of extractLinkHrefs(htmlCode)) {
121
+ const href = (rawHref.split(/[?#]/, 1)[0] ?? "").trim();
122
+ if (!href || isRemoteHref(href)) continue;
123
+ const abs = path2.resolve(dir, href);
124
+ if (seen.has(abs)) continue;
125
+ seen.add(abs);
126
+ if (existsSync(abs)) files.push(abs);
127
+ }
128
+ return { files, inline: extractInlineStyles(htmlCode) };
129
+ }
130
+ function cssSetKey(sortedResolvedPaths, inline) {
131
+ const h = createHash("sha1");
132
+ h.update(sortedResolvedPaths.join("\n"));
133
+ h.update("\0inline\0");
134
+ for (const block of inline) {
135
+ h.update(block);
136
+ h.update("\0");
137
+ }
138
+ return h.digest("hex");
139
+ }
140
+
141
+ // ../cli/src/transform.ts
142
+ function buildResolver(provider, css, projectRoot) {
143
+ if (provider === "custom") {
144
+ return createCssResolver([], { files: css, projectRoot });
145
+ }
146
+ return createTailwindResolver({ projectRoot });
147
+ }
148
+ function buildPasses(patterns) {
149
+ const byPhase = /* @__PURE__ */ new Map();
150
+ for (const p of patterns) {
151
+ const phase = p.category.split("/", 1)[0] ?? "flatten";
152
+ let bucket = byPhase.get(phase);
153
+ if (!bucket) {
154
+ bucket = [];
155
+ byPhase.set(phase, bucket);
156
+ }
157
+ bucket.push(p);
158
+ }
159
+ const passes = [];
160
+ for (const [phase, pats] of byPhase) {
161
+ passes.push({ phase, category: `${phase}/builtin`, patterns: pats });
162
+ }
163
+ return passes;
164
+ }
165
+ function selectPatterns(names) {
166
+ if (names === null) return builtinPatterns;
167
+ const set = new Set(names);
168
+ return builtinPatterns.filter((p) => set.has(p.name));
169
+ }
170
+ function jsxKindOf(id) {
171
+ const clean = id.split("?", 1)[0] ?? id;
172
+ const lower = clean.toLowerCase();
173
+ if (lower.endsWith(".tsx")) return "tsx";
174
+ if (lower.endsWith(".jsx")) return "jsx";
175
+ return null;
176
+ }
177
+ function htmlKindOf(id) {
178
+ const lower = (id.split("?", 1)[0] ?? id).toLowerCase();
179
+ if (lower.endsWith(".html") || lower.endsWith(".htm")) return "html";
180
+ return null;
181
+ }
182
+ function countClassTokens(code) {
183
+ let total = 0;
184
+ const re = /\b(?:className|class)\s*=\s*"([^"]*)"/g;
185
+ let m;
186
+ while ((m = re.exec(code)) !== null) {
187
+ total += m[1].split(/\s+/).filter((t) => t.length > 0).length;
188
+ }
189
+ return total;
190
+ }
191
+ function bytes(s) {
192
+ return Buffer.byteLength(s, "utf8");
193
+ }
194
+ function passthroughResult(code) {
195
+ return {
196
+ code,
197
+ changed: false,
198
+ passthrough: true,
199
+ stats: {
200
+ nodesIn: 0,
201
+ nodesOut: 0,
202
+ nodesRemoved: 0,
203
+ classesBefore: 0,
204
+ classesAfter: 0,
205
+ classesSaved: 0,
206
+ bytesBefore: bytes(code),
207
+ bytesAfter: bytes(code),
208
+ bytesSaved: 0
209
+ }
210
+ };
211
+ }
212
+ function createTransform(options) {
213
+ const projectRoot = options.projectRoot ?? process.cwd();
214
+ const globalResolver = buildResolver(options.provider, options.css, projectRoot);
215
+ const patterns = selectPatterns(options.passes);
216
+ const resolverCache = /* @__PURE__ */ new Map();
217
+ function resolverFor(code, id) {
218
+ if (options.provider !== "custom" || htmlKindOf(id) === null) return globalResolver;
219
+ const { files: localFiles, inline } = extractHtmlStylesheets(code, id);
220
+ if (localFiles.length === 0 && inline.length === 0) return globalResolver;
221
+ const globalPaths = options.css.map((p) => path3.resolve(p));
222
+ const sortedPaths = [.../* @__PURE__ */ new Set([...globalPaths, ...localFiles])].sort();
223
+ const key = cssSetKey(sortedPaths, inline);
224
+ let resolver = resolverCache.get(key);
225
+ if (!resolver) {
226
+ const inlineFiles = inline.map((css, i) => ({ id: `${id}#inline-${i}`, css }));
227
+ resolver = createCssResolver(inlineFiles, { files: sortedPaths, projectRoot });
228
+ resolverCache.set(key, resolver);
229
+ }
230
+ return resolver;
231
+ }
232
+ function prepare(code, id, kind, gate, resolver) {
233
+ const parsed = createJsxFrontend().parse(code, {
234
+ id,
235
+ kind,
236
+ resolver,
237
+ normalizer,
238
+ config: {},
239
+ onDiagnostic: () => {
240
+ }
241
+ });
242
+ const doc = parsed.doc;
243
+ const nodesIn = doc.nodes.size;
244
+ for (const node of doc.nodes.values()) node.meta.safetyFloor = 3;
245
+ const ctx = {
246
+ doc,
247
+ safetyCeiling: options.safety,
248
+ normalizer,
249
+ // Real CSS-selector-safety index from the active resolver (custom-CSS reports combinator /
250
+ // structural-pseudo coupling; Tailwind has none → null index, behaviour unchanged).
251
+ selectors: buildSelectorIndex(doc, resolver),
252
+ resolver,
253
+ gate
254
+ };
255
+ return { doc, ctx, passes: buildPasses(patterns), nodesIn };
256
+ }
257
+ function prepareHtml(code, id, gate, resolver) {
258
+ const parsed = createHtmlFrontend().parse(code, {
259
+ id,
260
+ kind: "html",
261
+ resolver,
262
+ normalizer,
263
+ config: {},
264
+ onDiagnostic: () => {
265
+ }
266
+ });
267
+ const doc = parsed.doc;
268
+ const nodesIn = doc.nodes.size;
269
+ const ctx = {
270
+ doc,
271
+ safetyCeiling: options.safety,
272
+ normalizer,
273
+ selectors: buildSelectorIndex(doc, resolver),
274
+ resolver,
275
+ gate
276
+ };
277
+ return { doc, ctx, passes: buildPasses(patterns), nodesIn };
278
+ }
279
+ function finish(code, optimized, id, nodesIn, resolver, backend = "jsx") {
280
+ syncClassesFromComputed(optimized, resolver, normalizer);
281
+ const print = backend === "html" ? createHtmlBackend().print : createJsxBackend().print;
282
+ const printed = print(
283
+ optimized,
284
+ { moduleId: id, ops: [], provenance: /* @__PURE__ */ new Map() },
285
+ { normalizer, resolver, sink: createSyntheticSink(), eol: "\n", onDiagnostic: () => {
286
+ } }
287
+ );
288
+ const out = printed.code;
289
+ const nodesOut = optimized.nodes.size;
290
+ const classesBefore = countClassTokens(code);
291
+ const classesAfter = countClassTokens(out);
292
+ return {
293
+ code: out,
294
+ changed: out !== code,
295
+ passthrough: false,
296
+ stats: {
297
+ nodesIn,
298
+ nodesOut,
299
+ nodesRemoved: Math.max(0, nodesIn - nodesOut),
300
+ classesBefore,
301
+ classesAfter,
302
+ classesSaved: Math.max(0, classesBefore - classesAfter),
303
+ bytesBefore: bytes(code),
304
+ bytesAfter: bytes(out),
305
+ bytesSaved: bytes(code) - bytes(out)
306
+ }
307
+ };
308
+ }
309
+ return {
310
+ resolver: globalResolver,
311
+ transformFile(code, id) {
312
+ const kind = jsxKindOf(id);
313
+ if (kind !== null) {
314
+ const resolver = resolverFor(code, id);
315
+ const { doc, ctx, passes, nodesIn } = prepare(code, id, kind, "provably-safe", resolver);
316
+ const { doc: optimized } = runPasses(doc, passes, ctx);
317
+ return finish(code, optimized, id, nodesIn, resolver);
318
+ }
319
+ if (htmlKindOf(id) !== null) {
320
+ const resolver = resolverFor(code, id);
321
+ const { doc, ctx, passes, nodesIn } = prepareHtml(code, id, "provably-safe", resolver);
322
+ const { doc: optimized } = runPasses(doc, passes, ctx);
323
+ return finish(code, optimized, id, nodesIn, resolver, "html");
324
+ }
325
+ return passthroughResult(code);
326
+ }
327
+ };
328
+ }
329
+
330
+ export {
331
+ isGitClean,
332
+ planWrites,
333
+ destinationFor,
334
+ createTransform
335
+ };
336
+ //# sourceMappingURL=chunk-EYQXQQQH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../cli/src/safety.ts","../../cli/src/transform.ts","../../cli/src/html-css.ts"],"sourcesContent":["/**\n * @domflax/cli — OUTPUT SAFETY (DESIGN-DECISIONS Q16, ARCHITECTURE §16.10).\n *\n * Source is READ-ONLY by default. Writes land in `--out`/`./domflax-out` (mirroring structure), or in\n * place ONLY inside disposable build dirs (dist/build/out/.next). Overwriting real source in place\n * requires `--dangerously-overwrite-source` AND a clean git tree (skippable with `--no-git-check`).\n * `--dry-run` writes nothing.\n */\n\nimport { execFileSync } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport type { CliOptions } from './options';\n\n/** Disposable build directories where in-place overwrite is always safe (they are regenerated). */\nconst DISPOSABLE_DIRS: ReadonlySet<string> = new Set(['dist', 'build', 'out', '.next']);\n\n/** True when any path segment is a disposable build dir, so the file is a regenerable artifact. */\nexport function isDisposablePath(file: string): boolean {\n return path\n .resolve(file)\n .split(path.sep)\n .some((seg) => DISPOSABLE_DIRS.has(seg));\n}\n\nexport type WriteMode = 'out-dir' | 'overwrite-source';\n\n/** Invocation-level write plan shared by every file. */\nexport interface WritePlan {\n readonly mode: WriteMode;\n /** Resolved absolute output dir for `out-dir` mode; `null` when overwriting source in place. */\n readonly outDir: string | null;\n}\n\nexport type Result<T> = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: string };\n\n/** Run `git status --porcelain`; clean ⇒ true. A non-repo / missing git ⇒ false (fail safe). */\nexport function isGitClean(cwd: string): boolean {\n try {\n const out = execFileSync('git', ['status', '--porcelain'], {\n cwd,\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n return out.trim().length === 0;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the invocation-level {@link WritePlan}. In-place source overwrite is refused unless the\n * danger flag is set and (the git tree is clean OR the git check is waived).\n */\nexport function planWrites(options: CliOptions, gitClean: boolean): Result<WritePlan> {\n if (options.dangerouslyOverwriteSource) {\n if (!options.noGitCheck && !gitClean) {\n return {\n ok: false,\n error:\n 'refusing --dangerously-overwrite-source: git working tree is not clean. ' +\n 'Commit or stash first, or pass --no-git-check to override.',\n };\n }\n return { ok: true, value: { mode: 'overwrite-source', outDir: null } };\n }\n const outDir = path.resolve(options.out ?? 'domflax-out');\n return { ok: true, value: { mode: 'out-dir', outDir } };\n}\n\n/**\n * Compute the destination path for one source file under a {@link WritePlan}. Refuses when an\n * `out-dir` destination resolves onto the source file itself and that file is NOT a disposable build\n * artifact — that would be an unsanctioned in-place source overwrite (the Q16 guard).\n */\nexport function destinationFor(file: string, inputRoot: string, plan: WritePlan): Result<string> {\n const absFile = path.resolve(file);\n\n if (plan.mode === 'overwrite-source') {\n // Already gated by planWrites (danger flag + clean-git / waiver).\n return { ok: true, value: absFile };\n }\n\n const outDir = plan.outDir!;\n const rel = path.relative(inputRoot, absFile);\n // Inputs outside the mirror root collapse to their basename so we never escape outDir with `..`.\n const safeRel = rel === '' || rel.startsWith('..') || path.isAbsolute(rel) ? path.basename(absFile) : rel;\n const dest = path.join(outDir, safeRel);\n\n if (path.resolve(dest) === absFile && !isDisposablePath(absFile)) {\n return {\n ok: false,\n error:\n `refusing to overwrite source file ${absFile}: the output path resolves onto the source. ` +\n 'Choose a different --out, or pass --dangerously-overwrite-source (with a clean git tree).',\n };\n }\n return { ok: true, value: dest };\n}\n","/**\n * @domflax/cli — the single-file transform engine.\n *\n * Built directly from the LOWER packages (core + frontend-jsx + resolver-tailwind/resolver-css +\n * patterns + pattern-kit). It deliberately does NOT import the `domflax` meta package: domflax's bin\n * imports `@domflax/cli`, so importing domflax here would form a dependency cycle. The pipeline\n * mirrors domflax's own: parse (JSX→IR, resolving each element's static classes through the chosen\n * resolver) → runPasses(builtinPatterns) → reverse-emit computed styles back to class tokens → print.\n *\n * `.jsx`/`.tsx` route to `@domflax/frontend-jsx` (Babel); `.html`/`.htm` route to\n * `@domflax/frontend-html` (parse5). Every other file passes through unchanged.\n */\n\nimport {\n buildSelectorIndex,\n createSyntheticSink,\n runPasses,\n syncClassesFromComputed,\n} from '@domflax/core';\nimport type {\n ApplyContext,\n FileKind,\n FlattenGate,\n IRDocument,\n Pass,\n PassCategory,\n PassPhase,\n Pattern,\n SafetyLevel,\n StyleResolver,\n} from '@domflax/core';\nimport { createHtmlBackend, createHtmlFrontend } from '@domflax/frontend-html';\nimport { createJsxBackend, createJsxFrontend } from '@domflax/frontend-jsx';\nimport { normalizer } from '@domflax/pattern-kit';\nimport { builtinPatterns } from '@domflax/patterns';\nimport { createCssResolver } from '@domflax/resolver-css';\nimport { createTailwindResolver } from '@domflax/resolver-tailwind';\n\nimport * as path from 'node:path';\n\nimport { cssSetKey, extractHtmlStylesheets } from './html-css';\nimport type { CliOptions, ProviderOption } from './options';\n\n/* ───────────────────────── per-file result + stats ───────────────────────── */\n\nexport interface FileStats {\n readonly nodesIn: number;\n readonly nodesOut: number;\n readonly nodesRemoved: number;\n readonly classesBefore: number;\n readonly classesAfter: number;\n readonly classesSaved: number;\n readonly bytesBefore: number;\n readonly bytesAfter: number;\n readonly bytesSaved: number;\n}\n\nexport interface FileResult {\n readonly code: string;\n readonly changed: boolean;\n readonly passthrough: boolean;\n readonly stats: FileStats;\n}\n\n/** A configured transform — holds the resolver (and its cached engine) across files. */\nexport interface Transform {\n readonly resolver: StyleResolver;\n /**\n * SYNC transform — fully static (gate `'provably-safe'`); never changes rendering and never launches\n * a browser. Only provably layout-neutral flattens are applied.\n */\n transformFile(code: string, id: string): FileResult;\n}\n\n/* ───────────────────────── resolver wiring ───────────────────────── */\n\n/**\n * Build the {@link StyleResolver} for the chosen provider. The heavy engine each wraps (Tailwind v3 /\n * postcss) is loaded LAZILY at construction and resolved from the user's PROJECT via the factories'\n * `projectRoot` option — never from where the CLI bundle happens to live.\n */\nexport function buildResolver(provider: ProviderOption, css: readonly string[], projectRoot: string): StyleResolver {\n if (provider === 'custom') {\n return createCssResolver([], { files: css, projectRoot });\n }\n // 'auto' and 'tailwind' both resolve against the project's Tailwind engine.\n return createTailwindResolver({ projectRoot });\n}\n\n/* ───────────────────────── pass assembly ───────────────────────── */\n\n/** Group the flat pattern list into one {@link Pass} per {@link PassPhase} (derived from category). */\nfunction buildPasses(patterns: readonly Pattern[]): Pass[] {\n const byPhase = new Map<PassPhase, Pattern[]>();\n for (const p of patterns) {\n const phase = (p.category.split('/', 1)[0] ?? 'flatten') as PassPhase;\n let bucket = byPhase.get(phase);\n if (!bucket) {\n bucket = [];\n byPhase.set(phase, bucket);\n }\n bucket.push(p);\n }\n const passes: Pass[] = [];\n for (const [phase, pats] of byPhase) {\n passes.push({ phase, category: `${phase}/builtin` as PassCategory, patterns: pats });\n }\n return passes;\n}\n\n/** Select the active patterns: every built-in unless the caller narrowed by name (the wizard does). */\nfunction selectPatterns(names: readonly string[] | null): readonly Pattern[] {\n if (names === null) return builtinPatterns;\n const set = new Set(names);\n return builtinPatterns.filter((p) => set.has(p.name));\n}\n\n/* ───────────────────────── file kind + token counting ───────────────────────── */\n\n/** `.tsx`/`.jsx` ⇒ the matching {@link FileKind}; anything else ⇒ null (no JSX frontend). */\nfunction jsxKindOf(id: string): FileKind | null {\n const clean = id.split('?', 1)[0] ?? id;\n const lower = clean.toLowerCase();\n if (lower.endsWith('.tsx')) return 'tsx';\n if (lower.endsWith('.jsx')) return 'jsx';\n return null;\n}\n\n/** `.html`/`.htm` ⇒ `'html'`; anything else ⇒ null (no HTML frontend). */\nfunction htmlKindOf(id: string): FileKind | null {\n const lower = (id.split('?', 1)[0] ?? id).toLowerCase();\n if (lower.endsWith('.html') || lower.endsWith('.htm')) return 'html';\n return null;\n}\n\n/** Rough class-token count for the `--report` summary (provider-independent, string-level). */\nfunction countClassTokens(code: string): number {\n let total = 0;\n const re = /\\b(?:className|class)\\s*=\\s*\"([^\"]*)\"/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(code)) !== null) {\n total += m[1]!.split(/\\s+/).filter((t) => t.length > 0).length;\n }\n return total;\n}\n\nfunction bytes(s: string): number {\n return Buffer.byteLength(s, 'utf8');\n}\n\nfunction passthroughResult(code: string): FileResult {\n return {\n code,\n changed: false,\n passthrough: true,\n stats: {\n nodesIn: 0,\n nodesOut: 0,\n nodesRemoved: 0,\n classesBefore: 0,\n classesAfter: 0,\n classesSaved: 0,\n bytesBefore: bytes(code),\n bytesAfter: bytes(code),\n bytesSaved: 0,\n },\n };\n}\n\n/* ───────────────────────── the transform ───────────────────────── */\n\n/**\n * Construct a transform for the given options. The resolver (and its engine) is built once and reused\n * across every file. With `provider: 'tailwind'|'auto'`, if Tailwind cannot be resolved from the\n * project the resolver degrades to resolving nothing — transforms then pass through unchanged.\n */\n/** Parsed + authorized doc and the apply context, shared by the sync + async transform paths. */\ninterface PreparedFile {\n readonly doc: IRDocument;\n readonly ctx: ApplyContext;\n readonly passes: readonly Pass[];\n readonly nodesIn: number;\n}\n\nexport function createTransform(options: CliOptions): Transform {\n const projectRoot = options.projectRoot ?? process.cwd();\n const globalResolver = buildResolver(options.provider, options.css, projectRoot);\n const patterns = selectPatterns(options.passes);\n\n // FEATURE A — per-file resolver cache, keyed by the exact CSS set (sorted paths + inline hash), so\n // pages that share stylesheet imports reuse one resolver (and its parsed engine).\n const resolverCache = new Map<string, StyleResolver>();\n\n /**\n * Choose the resolver for one file. `.jsx`/`.tsx` and every non-custom provider use the single\n * GLOBAL resolver. An `.html`/`.htm` file under the CUSTOM provider resolves against the GLOBAL set\n * (`options.css`, applied to every file) PLUS its own `<link>` imports and inline `<style>` blocks.\n */\n function resolverFor(code: string, id: string): StyleResolver {\n if (options.provider !== 'custom' || htmlKindOf(id) === null) return globalResolver;\n const { files: localFiles, inline } = extractHtmlStylesheets(code, id);\n if (localFiles.length === 0 && inline.length === 0) return globalResolver;\n\n // Canonicalize: global stylesheets first (base cascade), then the file's own imports. The cache\n // key — and the resolver's own file order — are the sorted, resolved paths, so identical import\n // sets always hit the same cache entry regardless of source ordering.\n const globalPaths = options.css.map((p) => path.resolve(p));\n const sortedPaths = [...new Set([...globalPaths, ...localFiles])].sort();\n const key = cssSetKey(sortedPaths, inline);\n\n let resolver = resolverCache.get(key);\n if (!resolver) {\n const inlineFiles = inline.map((css, i) => ({ id: `${id}#inline-${i}`, css }));\n resolver = createCssResolver(inlineFiles, { files: sortedPaths, projectRoot });\n resolverCache.set(key, resolver);\n }\n return resolver;\n }\n\n /** PARSE (JSX → IR, classes onto `computed`) + AUTHORIZE + build the apply context for `gate`. */\n function prepare(\n code: string,\n id: string,\n kind: FileKind,\n gate: FlattenGate,\n resolver: StyleResolver,\n ): PreparedFile {\n const parsed = createJsxFrontend().parse(code, {\n id,\n kind,\n resolver,\n normalizer,\n config: {},\n onDiagnostic: () => {},\n });\n const doc = parsed.doc;\n const nodesIn = doc.nodes.size;\n for (const node of doc.nodes.values()) node.meta.safetyFloor = 3;\n const ctx: ApplyContext = {\n doc,\n safetyCeiling: options.safety as SafetyLevel,\n normalizer,\n // Real CSS-selector-safety index from the active resolver (custom-CSS reports combinator /\n // structural-pseudo coupling; Tailwind has none → null index, behaviour unchanged).\n selectors: buildSelectorIndex(doc, resolver),\n resolver,\n gate,\n };\n return { doc, ctx, passes: buildPasses(patterns), nodesIn };\n }\n\n /**\n * PARSE (HTML → IR, classes onto `computed`) for `.html`/`.htm`. The HTML frontend sets per-node\n * safety floors itself (opaque nodes → 0), so — unlike the JSX prepare — we must NOT blanket-open\n * every node to floor 3.\n */\n function prepareHtml(code: string, id: string, gate: FlattenGate, resolver: StyleResolver): PreparedFile {\n const parsed = createHtmlFrontend().parse(code, {\n id,\n kind: 'html',\n resolver,\n normalizer,\n config: {},\n onDiagnostic: () => {},\n });\n const doc = parsed.doc;\n const nodesIn = doc.nodes.size;\n const ctx: ApplyContext = {\n doc,\n safetyCeiling: options.safety as SafetyLevel,\n normalizer,\n selectors: buildSelectorIndex(doc, resolver),\n resolver,\n gate,\n };\n return { doc, ctx, passes: buildPasses(patterns), nodesIn };\n }\n\n /** REVERSE-EMIT + PRINT the optimized doc, then assemble the per-file result + stats. */\n function finish(\n code: string,\n optimized: IRDocument,\n id: string,\n nodesIn: number,\n resolver: StyleResolver,\n backend: 'jsx' | 'html' = 'jsx',\n ): FileResult {\n syncClassesFromComputed(optimized, resolver, normalizer);\n const print = backend === 'html' ? createHtmlBackend().print : createJsxBackend().print;\n const printed = print(\n optimized,\n { moduleId: id, ops: [], provenance: new Map() },\n { normalizer, resolver, sink: createSyntheticSink(), eol: '\\n', onDiagnostic: () => {} },\n );\n const out = printed.code;\n const nodesOut = optimized.nodes.size;\n const classesBefore = countClassTokens(code);\n const classesAfter = countClassTokens(out);\n return {\n code: out,\n changed: out !== code,\n passthrough: false,\n stats: {\n nodesIn,\n nodesOut,\n nodesRemoved: Math.max(0, nodesIn - nodesOut),\n classesBefore,\n classesAfter,\n classesSaved: Math.max(0, classesBefore - classesAfter),\n bytesBefore: bytes(code),\n bytesAfter: bytes(out),\n bytesSaved: bytes(code) - bytes(out),\n },\n };\n }\n\n return {\n resolver: globalResolver,\n transformFile(code: string, id: string): FileResult {\n const kind = jsxKindOf(id);\n if (kind !== null) {\n const resolver = resolverFor(code, id);\n const { doc, ctx, passes, nodesIn } = prepare(code, id, kind, 'provably-safe', resolver);\n const { doc: optimized } = runPasses(doc, passes, ctx);\n return finish(code, optimized, id, nodesIn, resolver);\n }\n if (htmlKindOf(id) !== null) {\n const resolver = resolverFor(code, id);\n const { doc, ctx, passes, nodesIn } = prepareHtml(code, id, 'provably-safe', resolver);\n const { doc: optimized } = runPasses(doc, passes, ctx);\n return finish(code, optimized, id, nodesIn, resolver, 'html');\n }\n return passthroughResult(code);\n },\n };\n}\n\n/** The names of every built-in pattern, for the wizard's multiselect. */\nexport function builtinPatternNames(): readonly string[] {\n return builtinPatterns.map((p) => p.name);\n}\n","/**\n * @domflax/cli — per-file CSS discovery for HTML inputs (FEATURE A).\n *\n * Each HTML page links its OWN stylesheets, so resolving every file against one hand-picked global\n * `--css` set is wrong for HTML. {@link extractHtmlStylesheets} lifts a file's own local stylesheets\n * (`<link rel=\"stylesheet\" href>`, resolved RELATIVE to the html file's directory) plus its inline\n * `<style>` blocks straight out of the markup. Remote (`http:`/`https:`/`data:`/protocol-relative)\n * and missing hrefs are skipped, so only real, on-disk stylesheets feed the resolver.\n *\n * The scan is a lightweight, dependency-free `<link>`/`<style>` sweep (no parse5): it runs per file and\n * inside every worker, so keeping it self-contained avoids pulling the HTML frontend into the hot path.\n * {@link cssSetKey} keys the transform's resolver cache so pages sharing imports reuse one resolver.\n */\n\nimport { createHash } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport * as path from 'node:path';\n\n/** The stylesheets a single HTML file pulls in beyond the global set. */\nexport interface HtmlStylesheets {\n /** Absolute paths of local `<link rel=\"stylesheet\">` targets that exist on disk. */\n readonly files: readonly string[];\n /** Verbatim contents of the file's inline `<style>` blocks (source order). */\n readonly inline: readonly string[];\n}\n\n/** True for any href we must NOT treat as a local file: it has a URI scheme or is protocol-relative. */\nfunction isRemoteHref(href: string): boolean {\n const h = href.trim();\n // A leading scheme (`http:`, `https:`, `data:`, `file:`…) or `//host` protocol-relative form.\n return h.startsWith('//') || /^[a-z][a-z0-9+.-]*:/i.test(h);\n}\n\n/**\n * Read one attribute's value out of a start-tag string. Handles double-, single-, and unquoted forms;\n * returns `null` when the attribute is absent (an empty value returns `''`).\n */\nfunction attrValue(tag: string, name: string): string | null {\n const re = new RegExp(`\\\\b${name}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s\"'>]+))`, 'i');\n const m = re.exec(tag);\n if (!m) return null;\n return (m[1] ?? m[2] ?? m[3] ?? '').trim();\n}\n\n/** Collect the `href` of every `<link>` whose `rel` includes the `stylesheet` keyword. */\nfunction extractLinkHrefs(html: string): string[] {\n const out: string[] = [];\n const linkRe = /<link\\b[^>]*>/gi;\n let m: RegExpExecArray | null;\n while ((m = linkRe.exec(html)) !== null) {\n const tag = m[0];\n const rel = attrValue(tag, 'rel');\n if (rel === null || !/(?:^|\\s)stylesheet(?:\\s|$)/i.test(rel)) continue;\n const href = attrValue(tag, 'href');\n if (href) out.push(href);\n }\n return out;\n}\n\n/** Collect the text of every inline `<style>` block (skipping any explicitly non-CSS `type`). */\nfunction extractInlineStyles(html: string): string[] {\n const out: string[] = [];\n const styleRe = /<style\\b([^>]*)>([\\s\\S]*?)<\\/style>/gi;\n let m: RegExpExecArray | null;\n while ((m = styleRe.exec(html)) !== null) {\n const type = attrValue(`<style ${m[1] ?? ''}>`, 'type')?.toLowerCase();\n // Only plain CSS counts — a typed island like text/scss or text/template is not a stylesheet.\n if (type && type !== 'text/css' && type !== 'css') continue;\n const css = m[2] ?? '';\n if (css.trim().length > 0) out.push(css);\n }\n return out;\n}\n\n/**\n * Extract an HTML file's own local stylesheets + inline styles. `href`s are resolved RELATIVE to the\n * html file's directory; remote/protocol-relative/`data:` hrefs and files that don't exist on disk are\n * dropped. Query/hash suffixes (`style.css?v=2`) are stripped before resolution.\n */\nexport function extractHtmlStylesheets(htmlCode: string, htmlAbsPath: string): HtmlStylesheets {\n const dir = path.dirname(path.resolve(htmlAbsPath));\n const files: string[] = [];\n const seen = new Set<string>();\n for (const rawHref of extractLinkHrefs(htmlCode)) {\n const href = (rawHref.split(/[?#]/, 1)[0] ?? '').trim();\n if (!href || isRemoteHref(href)) continue;\n const abs = path.resolve(dir, href);\n if (seen.has(abs)) continue;\n seen.add(abs);\n if (existsSync(abs)) files.push(abs);\n }\n return { files, inline: extractInlineStyles(htmlCode) };\n}\n\n/**\n * A stable cache key for a CSS set: the sorted, resolved stylesheet paths plus a hash of the inline\n * blocks. Two HTML files that pull in the same imports (and inline the same styles) produce the same\n * key, so the transform reuses one resolver for both — hundreds of pages stay fast.\n */\nexport function cssSetKey(sortedResolvedPaths: readonly string[], inline: readonly string[]): string {\n const h = createHash('sha1');\n h.update(sortedResolvedPaths.join('\\n'));\n h.update('\u0000inline\u0000');\n for (const block of inline) {\n h.update(block);\n h.update('\u0000');\n }\n return h.digest('hex');\n}\n"],"mappings":"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","names":["path","path"]}