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
@@ -0,0 +1,382 @@
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
+ createPipeline,
13
+ createSyntheticSink,
14
+ normalizer,
15
+ runPasses,
16
+ syncClassesFromComputed
17
+ } from "./chunk-TTJEXWAC.js";
18
+ import {
19
+ init_esm_shims
20
+ } from "./chunk-U5GOONKV.js";
21
+
22
+ // src/index.ts
23
+ init_esm_shims();
24
+ import { dirname, join } from "path";
25
+ import { fileURLToPath } from "url";
26
+
27
+ // src/pipeline-run.ts
28
+ init_esm_shims();
29
+ function bytes(s) {
30
+ return Buffer.byteLength(s, "utf8");
31
+ }
32
+ function countClassTokens(code) {
33
+ let total = 0;
34
+ const re = /\b(?:className|class)\s*=\s*"([^"]*)"/g;
35
+ let m;
36
+ while ((m = re.exec(code)) !== null) {
37
+ total += m[1].split(/\s+/).filter((t) => t.length > 0).length;
38
+ }
39
+ return total;
40
+ }
41
+ function computeStats(code, out, nodesIn, nodesOut) {
42
+ const classesBefore = countClassTokens(code);
43
+ const classesAfter = countClassTokens(out);
44
+ return {
45
+ nodesRemoved: Math.max(0, nodesIn - nodesOut),
46
+ classesSaved: Math.max(0, classesBefore - classesAfter),
47
+ bytesSaved: bytes(code) - bytes(out)
48
+ };
49
+ }
50
+ function jsxKindOf(id) {
51
+ const clean = id.split("?", 1)[0] ?? id;
52
+ if (clean.endsWith(".tsx")) return "tsx";
53
+ if (clean.endsWith(".jsx")) return "jsx";
54
+ return null;
55
+ }
56
+ function htmlKindOf(id) {
57
+ const clean = (id.split("?", 1)[0] ?? id).toLowerCase();
58
+ if (clean.endsWith(".html") || clean.endsWith(".htm")) return "html";
59
+ return null;
60
+ }
61
+ function eolOf(doc) {
62
+ for (const src of doc.sources.values()) return src.eol;
63
+ return "\n";
64
+ }
65
+ function buildPasses(patterns) {
66
+ const byPhase = /* @__PURE__ */ new Map();
67
+ for (const p of patterns) {
68
+ const phase = p.category.split("/", 1)[0] ?? "flatten";
69
+ let bucket = byPhase.get(phase);
70
+ if (!bucket) {
71
+ bucket = [];
72
+ byPhase.set(phase, bucket);
73
+ }
74
+ bucket.push(p);
75
+ }
76
+ const passes = [];
77
+ for (const [phase, pats] of byPhase) {
78
+ passes.push({ phase, category: `${phase}/builtin`, patterns: pats });
79
+ }
80
+ return passes;
81
+ }
82
+ function preparePipeline(code, id, kind, resolver, patterns, safety, gate) {
83
+ const parsed = createJsxFrontend().parse(code, {
84
+ id,
85
+ kind,
86
+ resolver,
87
+ normalizer,
88
+ config: {},
89
+ onDiagnostic: () => {
90
+ }
91
+ });
92
+ const doc = parsed.doc;
93
+ for (const node of doc.nodes.values()) node.meta.safetyFloor = 3;
94
+ const ctx = {
95
+ doc,
96
+ safetyCeiling: safety,
97
+ normalizer,
98
+ // Real CSS-selector-safety index from the active resolver: a wrapper a combinator/structural
99
+ // selector depends on is flagged so the flatten guards refuse to flatten it. Tailwind (no
100
+ // complexSelectors) degrades to the null index — behaviour unchanged.
101
+ selectors: buildSelectorIndex(doc, resolver),
102
+ resolver,
103
+ gate
104
+ };
105
+ return { doc, ctx, passes: buildPasses(patterns) };
106
+ }
107
+ function finishPipeline(optimized, id, resolver) {
108
+ syncClassesFromComputed(optimized, resolver, normalizer);
109
+ const printed = createJsxBackend().print(
110
+ optimized,
111
+ { moduleId: id, ops: [], provenance: /* @__PURE__ */ new Map() },
112
+ {
113
+ normalizer,
114
+ resolver,
115
+ sink: createSyntheticSink(),
116
+ eol: eolOf(optimized),
117
+ onDiagnostic: () => {
118
+ }
119
+ }
120
+ );
121
+ return printed.code;
122
+ }
123
+ function runJsxPipeline(code, id, kind, resolver, patterns, safety) {
124
+ const { doc, ctx, passes } = preparePipeline(code, id, kind, resolver, patterns, safety, "provably-safe");
125
+ const nodesIn = doc.nodes.size;
126
+ const { doc: optimized } = runPasses(doc, passes, ctx);
127
+ const out = finishPipeline(optimized, id, resolver);
128
+ return { code: out, stats: computeStats(code, out, nodesIn, optimized.nodes.size) };
129
+ }
130
+ function prepareHtml(code, id, resolver, patterns, safety, gate) {
131
+ const parsed = createHtmlFrontend().parse(code, {
132
+ id,
133
+ kind: "html",
134
+ resolver,
135
+ normalizer,
136
+ config: {},
137
+ onDiagnostic: () => {
138
+ }
139
+ });
140
+ const doc = parsed.doc;
141
+ const ctx = {
142
+ doc,
143
+ safetyCeiling: safety,
144
+ normalizer,
145
+ selectors: buildSelectorIndex(doc, resolver),
146
+ resolver,
147
+ gate
148
+ };
149
+ return { doc, ctx, passes: buildPasses(patterns) };
150
+ }
151
+ function finishHtmlPipeline(optimized, id, resolver) {
152
+ syncClassesFromComputed(optimized, resolver, normalizer);
153
+ const printed = createHtmlBackend().print(
154
+ optimized,
155
+ { moduleId: id, ops: [], provenance: /* @__PURE__ */ new Map() },
156
+ {
157
+ normalizer,
158
+ resolver,
159
+ sink: createSyntheticSink(),
160
+ eol: eolOf(optimized),
161
+ onDiagnostic: () => {
162
+ }
163
+ }
164
+ );
165
+ return printed.code;
166
+ }
167
+ function runHtmlPipeline(code, id, resolver, patterns, safety) {
168
+ const { doc, ctx, passes } = prepareHtml(code, id, resolver, patterns, safety, "provably-safe");
169
+ const nodesIn = doc.nodes.size;
170
+ const { doc: optimized } = runPasses(doc, passes, ctx);
171
+ const out = finishHtmlPipeline(optimized, id, resolver);
172
+ return { code: out, stats: computeStats(code, out, nodesIn, optimized.nodes.size) };
173
+ }
174
+
175
+ // src/summary.ts
176
+ init_esm_shims();
177
+ function zeroStats() {
178
+ return { nodesRemoved: 0, classesSaved: 0, bytesSaved: 0 };
179
+ }
180
+ function emptyTotals() {
181
+ return { files: 0, nodesRemoved: 0, classesCompressed: 0, bytesSaved: 0 };
182
+ }
183
+ function resetTotals(t) {
184
+ t.files = 0;
185
+ t.nodesRemoved = 0;
186
+ t.classesCompressed = 0;
187
+ t.bytesSaved = 0;
188
+ }
189
+ function addStats(t, s, changed) {
190
+ if (!changed) return;
191
+ t.files += 1;
192
+ t.nodesRemoved += s.nodesRemoved;
193
+ t.classesCompressed += s.classesSaved;
194
+ t.bytesSaved += s.bytesSaved;
195
+ }
196
+ var BYTE_UNITS = ["KB", "MB", "GB", "TB"];
197
+ function formatBytes(n) {
198
+ const abs = Math.abs(n);
199
+ if (abs < 1024) return `${n} B`;
200
+ let value = n / 1024;
201
+ let unit = 0;
202
+ while (Math.abs(value) >= 1024 && unit < BYTE_UNITS.length - 1) {
203
+ value /= 1024;
204
+ unit += 1;
205
+ }
206
+ return `${value.toFixed(1)} ${BYTE_UNITS[unit]}`;
207
+ }
208
+ function formatCount(n) {
209
+ return Math.trunc(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
210
+ }
211
+ var LABEL_WIDTH = 20;
212
+ var RULE = ` ${"\u2500".repeat(32)}`;
213
+ function row(label, value) {
214
+ return ` ${label.padEnd(LABEL_WIDTH)}${value}`;
215
+ }
216
+ function renderSummary(totals) {
217
+ return [
218
+ "",
219
+ " \u25B2 domflax",
220
+ RULE,
221
+ row("files optimized", formatCount(totals.files)),
222
+ row("DOM nodes removed", formatCount(totals.nodesRemoved)),
223
+ row("classes compressed", formatCount(totals.classesCompressed)),
224
+ row("size saved", formatBytes(totals.bytesSaved)),
225
+ RULE,
226
+ ""
227
+ ].join("\n");
228
+ }
229
+ var TOTALS_KEY = /* @__PURE__ */ Symbol.for("domflax.buildTotals");
230
+ var PRINTED_KEY = /* @__PURE__ */ Symbol.for("domflax.summaryPrinted");
231
+ function accumulateOnCompilation(compilation, stats, changed) {
232
+ if (compilation === null || typeof compilation !== "object") return;
233
+ const bag = compilation;
234
+ let totals = bag[TOTALS_KEY];
235
+ if (!totals) {
236
+ totals = emptyTotals();
237
+ bag[TOTALS_KEY] = totals;
238
+ }
239
+ addStats(totals, stats, changed);
240
+ }
241
+ function printCompilationSummary(compilation) {
242
+ if (compilation === null || typeof compilation !== "object") return;
243
+ const bag = compilation;
244
+ if (bag[PRINTED_KEY]) return;
245
+ bag[PRINTED_KEY] = true;
246
+ const totals = bag[TOTALS_KEY];
247
+ if (totals && totals.files > 0) process.stdout.write(renderSummary(totals));
248
+ }
249
+
250
+ // src/index.ts
251
+ var DEFAULT_INCLUDE = [".jsx", ".tsx", ".html", ".htm"];
252
+ function resolveOptions(options) {
253
+ return {
254
+ provider: options.provider ?? "auto",
255
+ cssFiles: options.cssFiles ?? [],
256
+ dryRun: options.dryRun ?? false,
257
+ safety: options.safety ?? 2,
258
+ include: options.include ?? DEFAULT_INCLUDE
259
+ };
260
+ }
261
+ function isSupported(id, include) {
262
+ const clean = id.split("?", 1)[0] ?? id;
263
+ return include.some((ext) => clean.endsWith(ext));
264
+ }
265
+ function createResolver(resolved) {
266
+ if (resolved.provider === "custom") {
267
+ return createCssResolver([], { files: resolved.cssFiles });
268
+ }
269
+ return createTailwindResolver();
270
+ }
271
+ function createDomflax(options = {}) {
272
+ const resolved = resolveOptions(options);
273
+ const pipeline = createPipeline();
274
+ const patterns = builtinPatterns;
275
+ let cachedResolver = null;
276
+ const getResolver = () => cachedResolver ??= createResolver(resolved);
277
+ return {
278
+ options: resolved,
279
+ pipeline,
280
+ get resolver() {
281
+ return getResolver();
282
+ },
283
+ patterns,
284
+ transform(code, id) {
285
+ if (!isSupported(id, resolved.include)) return { code, map: null, stats: zeroStats() };
286
+ const kind = jsxKindOf(id);
287
+ if (kind !== null) {
288
+ const out = runJsxPipeline(code, id, kind, getResolver(), patterns, resolved.safety);
289
+ return { code: out.code, map: null, stats: out.stats };
290
+ }
291
+ if (htmlKindOf(id) !== null) {
292
+ const out = runHtmlPipeline(code, id, getResolver(), patterns, resolved.safety);
293
+ return { code: out.code, map: null, stats: out.stats };
294
+ }
295
+ return { code, map: null, stats: zeroStats() };
296
+ }
297
+ };
298
+ }
299
+ function vite(options = {}) {
300
+ const engine = createDomflax(options);
301
+ const totals = emptyTotals();
302
+ let printed = false;
303
+ const printSummary = () => {
304
+ if (printed) return;
305
+ printed = true;
306
+ if (totals.files > 0) process.stdout.write(renderSummary(totals));
307
+ };
308
+ return {
309
+ name: "domflax",
310
+ enforce: "pre",
311
+ buildStart() {
312
+ resetTotals(totals);
313
+ printed = false;
314
+ },
315
+ transform(code, id) {
316
+ if (!isSupported(id, engine.options.include)) return null;
317
+ const out = engine.transform(code, id);
318
+ const changed = out.code !== code;
319
+ addStats(totals, out.stats, changed);
320
+ return changed ? out : null;
321
+ },
322
+ buildEnd() {
323
+ printSummary();
324
+ },
325
+ closeBundle() {
326
+ printSummary();
327
+ }
328
+ };
329
+ }
330
+ var WEBPACK_JSX_TEST = /\.[jt]sx$/;
331
+ function webpackLoaderPath() {
332
+ const here = dirname(fileURLToPath(import.meta.url));
333
+ return join(here, "webpack-loader.cjs");
334
+ }
335
+ function tapWebpackSummary(compiler) {
336
+ const done = compiler.hooks?.done;
337
+ if (typeof done?.tap !== "function") return;
338
+ done.tap("domflax", (stats) => {
339
+ const compilation = stats?.compilation ?? stats;
340
+ printCompilationSummary(compilation);
341
+ });
342
+ }
343
+ function installWebpackSummary(compiler, host) {
344
+ if (typeof compiler.hooks?.done?.tap === "function") {
345
+ tapWebpackSummary(compiler);
346
+ return;
347
+ }
348
+ const plugins = host.plugins ??= [];
349
+ if (Array.isArray(plugins)) {
350
+ plugins.push({ apply: (real) => tapWebpackSummary(real) });
351
+ }
352
+ }
353
+ function webpack(options = {}) {
354
+ createDomflax(options);
355
+ return {
356
+ name: "domflax",
357
+ apply(compiler) {
358
+ const host = compiler.options ?? compiler;
359
+ const mod = host.module ??= {};
360
+ const rules = mod.rules ??= [];
361
+ const rule = {
362
+ test: WEBPACK_JSX_TEST,
363
+ enforce: "pre",
364
+ exclude: /node_modules/,
365
+ use: [{ loader: webpackLoaderPath(), options }]
366
+ };
367
+ rules.push(rule);
368
+ installWebpackSummary(compiler, host);
369
+ }
370
+ };
371
+ }
372
+ var domflax = { createDomflax, vite, webpack };
373
+ var src_default = domflax;
374
+
375
+ export {
376
+ accumulateOnCompilation,
377
+ createDomflax,
378
+ vite,
379
+ webpack,
380
+ src_default
381
+ };
382
+ //# sourceMappingURL=chunk-JBM3MJRM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/pipeline-run.ts","../src/summary.ts"],"sourcesContent":["/**\n * domflax — public meta package.\n *\n * Re-exports the entire `@domflax/core` public API (types + reference runtime) and the built-in\n * `@domflax/patterns` library, then layers thin, framework-agnostic build adapters on top\n * (`vite()` / `webpack()`) plus a programmatic `createDomflax()` factory.\n *\n * Each adapter runs the SAME single-file engine as {@link createDomflax} (JSX/TSX + HTML frontends +\n * lazy Tailwind/CSS resolver → core pass manager → reverse-emit → surgical backend). The adapters are\n * structurally typed against their bundlers — they never hard-depend on `vite` or `webpack`.\n *\n * `.jsx`/`.tsx` route to `@domflax/frontend-jsx` (Babel); `.html`/`.htm` route to\n * `@domflax/frontend-html` (parse5). Both emit via SURGICAL span edits over the original source.\n */\n\nimport { createPipeline } from '@domflax/core';\nimport type {\n EncodedSourceMap,\n Pattern,\n Pipeline,\n SafetyLevel,\n StyleResolver,\n} from '@domflax/core';\nimport { builtinPatterns } from '@domflax/patterns';\nimport { createTailwindResolver } from '@domflax/resolver-tailwind';\nimport { createCssResolver } from '@domflax/resolver-css';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { htmlKindOf, jsxKindOf, runHtmlPipeline, runJsxPipeline } from './pipeline-run';\nimport {\n addStats,\n emptyTotals,\n printCompilationSummary,\n renderSummary,\n resetTotals,\n zeroStats,\n type FileStatDelta,\n type Totals,\n} from './summary';\n\n// ── Re-export the public surface ──────────────────────────────────────────────────────────────\nexport * from '@domflax/core';\nexport * from '@domflax/patterns';\n\n/* ────────────────────────────────────────────────────────────────────────── *\n * Options\n * ────────────────────────────────────────────────────────────────────────── */\n\n/** How class names resolve to computed styles. */\nexport type DomflaxProvider = 'auto' | 'tailwind' | 'custom';\n\n/** Public adapter/factory options (mirrors the documented `domflax({...})` surface). */\nexport interface DomflaxOptions {\n /** Resolution strategy. Defaults to `'auto'`. */\n readonly provider?: DomflaxProvider;\n /** Stylesheets to parse when `provider` is `'custom'`. */\n readonly cssFiles?: readonly string[];\n /** Preview changes without rewriting source. */\n readonly dryRun?: boolean;\n /** Optimization aggressiveness handed to the pass manager (0 lint … 3 aggressive). */\n readonly safety?: SafetyLevel;\n /** File globs/extensions the adapters should consider. Defaults to jsx/tsx/html. */\n readonly include?: readonly string[];\n}\n\n/** Fully-resolved options with defaults applied. */\nexport interface ResolvedDomflaxOptions {\n readonly provider: DomflaxProvider;\n readonly cssFiles: readonly string[];\n readonly dryRun: boolean;\n readonly safety: SafetyLevel;\n readonly include: readonly string[];\n}\n\nconst DEFAULT_INCLUDE: readonly string[] = ['.jsx', '.tsx', '.html', '.htm'];\n\nfunction resolveOptions(options: DomflaxOptions): ResolvedDomflaxOptions {\n return {\n provider: options.provider ?? 'auto',\n cssFiles: options.cssFiles ?? [],\n dryRun: options.dryRun ?? false,\n safety: options.safety ?? 2,\n include: options.include ?? DEFAULT_INCLUDE,\n };\n}\n\n/** True when `id` is a file domflax knows how to transform. */\nfunction isSupported(id: string, include: readonly string[]): boolean {\n // Strip query suffixes bundlers append (e.g. `App.tsx?used`).\n const clean = id.split('?', 1)[0] ?? id;\n return include.some((ext) => clean.endsWith(ext));\n}\n\n/* ────────────────────────────────────────────────────────────────────────── *\n * Programmatic instance\n * ────────────────────────────────────────────────────────────────────────── */\n\n/** Result of a single-file transform. `map` is null until codegen lands. */\nexport interface DomflaxTransformResult {\n readonly code: string;\n readonly map: EncodedSourceMap | null;\n /**\n * Per-file optimization delta (nodes removed / classes saved / bytes saved). Zeroed for\n * unsupported or unchanged files. Consumed by the build adapters to accumulate the build-end\n * {@link renderSummary summary}.\n */\n readonly stats: FileStatDelta;\n}\n\n/**\n * A configured domflax engine. Holds the wired core {@link Pipeline}, the passthrough\n * {@link StyleResolver}, and the built-in {@link Pattern} set, and exposes a single-file\n * `transform`.\n */\nexport interface Domflax {\n readonly options: ResolvedDomflaxOptions;\n readonly pipeline: Pipeline;\n readonly resolver: StyleResolver;\n readonly patterns: readonly Pattern[];\n /**\n * Transform one file (SYNCHRONOUS, fully static, never launches a browser). For `.jsx`/`.tsx` this\n * runs the full pipeline (parse → resolve → flatten[provably-safe only] → reverse-emit → print);\n * every other (or unsupported) file is returned unchanged. Only provably layout-neutral flattens are\n * applied — domflax never changes rendering.\n */\n transform(code: string, id: string): DomflaxTransformResult;\n}\n\n/**\n * Build a configured domflax engine.\n *\n * Wires a real single-file pipeline: the JSX/TSX frontend + a Tailwind resolver feed the core pass\n * manager (running {@link builtinPatterns}), whose output is reverse-emitted back to class tokens\n * and re-printed by the JSX backend. Non-jsx/tsx files pass through unchanged.\n */\n/**\n * Build the {@link StyleResolver} for the chosen provider. The heavy engine each resolver wraps\n * (Tailwind v3 / postcss) is loaded LAZILY — at the moment this factory runs — and resolved from the\n * CONSUMER'S project, NOT from domflax's (possibly bundled) location. Both engines are OPTIONAL peer\n * dependencies of the published `domflax`: a Tailwind-only user never triggers a postcss load, and a\n * custom-CSS-only user never triggers a Tailwind load, because only the selected branch constructs.\n */\nfunction createResolver(resolved: ResolvedDomflaxOptions): StyleResolver {\n if (resolved.provider === 'custom') {\n return createCssResolver([], { files: resolved.cssFiles });\n }\n // 'auto' and 'tailwind' both resolve against the project's Tailwind engine.\n return createTailwindResolver();\n}\n\nexport function createDomflax(options: DomflaxOptions = {}): Domflax {\n const resolved = resolveOptions(options);\n const pipeline = createPipeline();\n const patterns = builtinPatterns;\n\n // Construct the resolver lazily so neither optional engine (Tailwind / postcss) is loaded until a\n // file is actually transformed (and only the engine for the selected provider is ever loaded).\n let cachedResolver: StyleResolver | null = null;\n const getResolver = (): StyleResolver => (cachedResolver ??= createResolver(resolved));\n\n return {\n options: resolved,\n pipeline,\n get resolver(): StyleResolver {\n return getResolver();\n },\n patterns,\n transform(code: string, id: string): DomflaxTransformResult {\n if (!isSupported(id, resolved.include)) return { code, map: null, stats: zeroStats() };\n const kind = jsxKindOf(id);\n if (kind !== null) {\n const out = runJsxPipeline(code, id, kind, getResolver(), patterns, resolved.safety);\n return { code: out.code, map: null, stats: out.stats };\n }\n // `.html`/`.htm` route to the parse5 HTML frontend/backend (surgical span edits).\n if (htmlKindOf(id) !== null) {\n const out = runHtmlPipeline(code, id, getResolver(), patterns, resolved.safety);\n return { code: out.code, map: null, stats: out.stats };\n }\n return { code, map: null, stats: zeroStats() };\n },\n };\n}\n\n/* ────────────────────────────────────────────────────────────────────────── *\n * Build adapters (framework-agnostic, structurally-typed shapes)\n * ────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Minimal Vite-plugin shape. Declared locally so this adapter does NOT depend on `vite`'s types\n * (an optional, type-only peer). Structurally compatible with Vite's `Plugin` for the hooks domflax\n * uses: `enforce: 'pre'` runs domflax before Vite's JSX→`createElement` transform, and `transform`\n * is Vite's per-file source hook. Returning `null` is Vite's \"no change\" signal.\n */\nexport interface DomflaxVitePlugin {\n readonly name: string;\n readonly enforce: 'pre';\n /** Vite's per-file source hook. Fully synchronous and browser-free. */\n transform(code: string, id: string): DomflaxTransformResult | null;\n /** Vite build-start hook — resets the per-build summary accumulator (watch/serve safe). */\n buildStart(): void;\n /** Vite build-end hook — prints the aggregate {@link renderSummary} once (if anything changed). */\n buildEnd(): void;\n /** Vite close-bundle hook — prints the summary as a backstop if `buildEnd` did not fire. */\n closeBundle(): void;\n}\n\n/**\n * Vite adapter. Returns a real Vite `Plugin` (`enforce: 'pre'`) whose `transform` runs the domflax\n * engine on `.jsx`/`.tsx` modules — strips any bundler query suffix (e.g. `App.tsx?used`) before\n * matching, returns `{ code, map }` when the source changed, and `null` (Vite's unchanged signal)\n * for unchanged sources and for any non-jsx/tsx module.\n *\n * @example\n * ```js\n * // vite.config.js\n * import domflax from 'domflax';\n * export default { plugins: [domflax.vite({ provider: 'tailwind' })] };\n * ```\n */\nexport function vite(options: DomflaxOptions = {}): DomflaxVitePlugin {\n const engine = createDomflax(options);\n\n // Aggregate across every `transform` call in this plugin instance. `buildStart` resets it so\n // watch/serve rebuilds each get their own summary; a `printed` latch guards the double-fire of\n // `buildEnd` + `closeBundle`.\n const totals: Totals = emptyTotals();\n let printed = false;\n\n const printSummary = (): void => {\n if (printed) return;\n printed = true;\n if (totals.files > 0) process.stdout.write(renderSummary(totals));\n };\n\n return {\n name: 'domflax',\n enforce: 'pre',\n buildStart(): void {\n resetTotals(totals);\n printed = false;\n },\n transform(code: string, id: string): DomflaxTransformResult | null {\n if (!isSupported(id, engine.options.include)) return null;\n const out = engine.transform(code, id);\n const changed = out.code !== code;\n addStats(totals, out.stats, changed);\n // Signal \"no change\" to Vite when the source round-tripped unchanged.\n return changed ? out : null;\n },\n buildEnd(): void {\n printSummary();\n },\n closeBundle(): void {\n printSummary();\n },\n };\n}\n\n/* ── webpack / Next.js ──────────────────────────────────────────────────────────────────────── */\n\n/** A `module.rule` `use` entry: an absolute loader path plus the options forwarded to it. */\ninterface DomflaxRuleUse {\n readonly loader: string;\n readonly options: DomflaxOptions;\n}\n\n/** The slice of a webpack `module.rule` domflax appends. */\ninterface DomflaxModuleRule {\n readonly test: RegExp;\n readonly enforce: 'pre';\n readonly exclude: RegExp;\n readonly use: readonly DomflaxRuleUse[];\n}\n\n/** Anything carrying a `module.rules` array — both a webpack `Compiler.options` and Next's bare config. */\ninterface DomflaxWebpackModuleHost {\n module?: { rules?: unknown[] };\n /** webpack's plugin list (present on both a real `Compiler.options` and Next's bare config). */\n plugins?: unknown[];\n}\n\n/** A tappable webpack hook (only the `tap` arm domflax uses). */\ninterface DomflaxWebpackHook {\n tap(name: string, fn: (arg: unknown) => void): void;\n}\n\n/**\n * Minimal webpack-compiler shape. Declared locally so this adapter does NOT depend on `webpack`'s\n * types. domflax only needs to push a rule onto the host's `module.rules`.\n *\n * `apply` accepts BOTH shapes: a real webpack `Compiler` (rules live under `compiler.options.module`)\n * AND the bare `config` object Next.js hands you from `webpack(config)` (rules live directly under\n * `config.module`). It duck-types `compiler.options ?? compiler` to find the right host.\n */\nexport interface DomflaxWebpackCompiler extends DomflaxWebpackModuleHost {\n options?: DomflaxWebpackModuleHost;\n /** Present only on a REAL webpack `Compiler` (not on Next's bare config). Used for the summary. */\n hooks?: { done?: DomflaxWebpackHook };\n}\n\n/**\n * Minimal webpack-plugin shape. `apply(compiler)` is the webpack plugin entry point.\n */\nexport interface DomflaxWebpackPlugin {\n readonly name: string;\n apply(compiler: DomflaxWebpackCompiler): void;\n}\n\n/** `.jsx`/`.tsx` modules only (combinator-free with the JSX frontend; `.js`/`.ts` are skipped). */\nconst WEBPACK_JSX_TEST = /\\.[jt]sx$/;\n\n/**\n * Absolute path to the bundled webpack loader (`./webpack-loader`). Resolved lazily against THIS\n * module's location so it works whether `domflax` is loaded as ESM (`dist/index.js`) or CJS\n * (`dist/index.cjs`) — both sit beside `dist/webpack-loader.cjs`. webpack requires loaders via\n * CommonJS, so we always point at the `.cjs` output.\n */\nfunction webpackLoaderPath(): string {\n const here = dirname(fileURLToPath(import.meta.url));\n return join(here, 'webpack-loader.cjs');\n}\n\n/**\n * webpack adapter (also the Next.js path). Returns a plugin whose `apply(compiler)` injects a\n * pre-enforced `module.rule` that invokes the domflax {@link ./webpack-loader loader} on every\n * `.jsx`/`.tsx` module. The loader runs the SAME lazy engine as {@link createDomflax} (no eager\n * Tailwind/postcss load).\n *\n * Next.js wiring (`next.config.js`) — Next exposes the underlying webpack config via `webpack(config)`:\n * ```js\n * // next.config.js\n * const domflax = require('domflax');\n * module.exports = {\n * webpack(config) {\n * domflax.webpack({ provider: 'tailwind' }).apply(config);\n * return config;\n * },\n * };\n * ```\n * `apply(compiler)` is intentionally duck-typed on `compiler.options.module.rules`, so it accepts\n * both a real webpack `Compiler` and the bare `config` object Next.js hands you.\n *\n * Caveat: this targets the webpack builder only. **Turbopack is not yet supported** — it does not\n * accept arbitrary webpack loaders, so the `next.config.js` wiring above is a no-op under\n * `next dev --turbopack`. Run domflax through the webpack builder until Turbopack exposes a loader API.\n */\n/**\n * Tap a REAL webpack `Compiler`'s `done` hook to print the build-end summary. The per-file stats were\n * stashed on the `compilation` by the loader (separate bundle) under a shared `Symbol.for` key; here\n * we read + print them once. No-op if `compiler` has no `done` hook (e.g. a bare config or a stub).\n */\nfunction tapWebpackSummary(compiler: DomflaxWebpackCompiler): void {\n const done = compiler.hooks?.done;\n if (typeof done?.tap !== 'function') return;\n done.tap('domflax', (stats: unknown) => {\n // `done` receives a `Stats` whose `.compilation` is the object the loader wrote to; some stubs\n // pass the compilation directly.\n const compilation = (stats as { compilation?: unknown } | null)?.compilation ?? stats;\n printCompilationSummary(compilation);\n });\n}\n\n/**\n * Wire the summary printer. On a real `Compiler` we tap `done` directly. On Next's bare config (no\n * `hooks`) we push a child plugin onto `config.plugins`; webpack later calls its `apply(compiler)`\n * with the real `Compiler`, at which point we tap `done`.\n */\nfunction installWebpackSummary(compiler: DomflaxWebpackCompiler, host: DomflaxWebpackModuleHost): void {\n if (typeof compiler.hooks?.done?.tap === 'function') {\n tapWebpackSummary(compiler);\n return;\n }\n const plugins = (host.plugins ??= []);\n if (Array.isArray(plugins)) {\n plugins.push({ apply: (real: DomflaxWebpackCompiler) => tapWebpackSummary(real) });\n }\n}\n\nexport function webpack(options: DomflaxOptions = {}): DomflaxWebpackPlugin {\n // Validate options eagerly (parity with the other adapters); the resolver stays lazy.\n createDomflax(options);\n return {\n name: 'domflax',\n apply(compiler: DomflaxWebpackCompiler): void {\n // Real webpack passes a `Compiler` (rules under `.options.module`); Next's `webpack(config)`\n // passes the bare config (rules under `.module`). Duck-type to the right host.\n const host: DomflaxWebpackModuleHost = compiler.options ?? compiler;\n const mod = (host.module ??= {});\n const rules = (mod.rules ??= []);\n const rule: DomflaxModuleRule = {\n test: WEBPACK_JSX_TEST,\n enforce: 'pre',\n exclude: /node_modules/,\n use: [{ loader: webpackLoaderPath(), options }],\n };\n rules.push(rule);\n // Print the aggregate summary at build end (loader ↔ plugin bridge over the compilation).\n installWebpackSummary(compiler, host);\n },\n };\n}\n\n/**\n * The default-export namespace. Exposes the build adapters and the programmatic factory as an OBJECT\n * so the documented `import domflax from 'domflax'; domflax.vite()` / `domflax.webpack()` works (and a\n * CommonJS `const domflax = require('domflax'); domflax.vite()` too). The named exports\n * (`createDomflax`, `vite`, `webpack`, …) remain available for direct import.\n */\nexport interface DomflaxDefault {\n createDomflax(options?: DomflaxOptions): Domflax;\n vite(options?: DomflaxOptions): DomflaxVitePlugin;\n webpack(options?: DomflaxOptions): DomflaxWebpackPlugin;\n}\n\n/** Default export: an object exposing `vite`, `webpack`, and the programmatic `createDomflax`. */\nconst domflax: DomflaxDefault = { createDomflax, vite, webpack };\nexport default domflax;\n","/**\n * domflax — the single-file JSX/TSX pipeline runner (parse → resolve → flatten → reverse-emit →\n * print), split out of `index.ts` so the meta package's barrel + adapters stay focused.\n *\n * {@link runJsxPipeline} is SYNC and fully static (gate `'provably-safe'`): it never changes rendering\n * and never launches a browser.\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';\n\nimport type { FileStatDelta } from './summary';\n\n/** Output of a pipeline run: the printed code plus the per-file optimization delta. */\nexport interface PipelineOutput {\n readonly code: string;\n readonly stats: FileStatDelta;\n}\n\n/** UTF-8 byte length (matches the CLI's `bytes()` — bytesSaved is measured in real bytes). */\nfunction bytes(s: string): number {\n return Buffer.byteLength(s, 'utf8');\n}\n\n/**\n * Rough class-token count (provider-independent, string-level) — identical to the CLI's\n * `countClassTokens`, so both surfaces report the same \"classes compressed\" figure.\n */\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\n/**\n * Compute the per-file stat delta the same way the CLI's `finish()` does: nodes from the IR\n * node-count delta, classes from the class-token delta, bytes from the UTF-8 byte-length delta.\n */\nfunction computeStats(code: string, out: string, nodesIn: number, nodesOut: number): FileStatDelta {\n const classesBefore = countClassTokens(code);\n const classesAfter = countClassTokens(out);\n return {\n nodesRemoved: Math.max(0, nodesIn - nodesOut),\n classesSaved: Math.max(0, classesBefore - classesAfter),\n bytesSaved: bytes(code) - bytes(out),\n };\n}\n\n/** `.tsx`/`.jsx` ⇒ the matching {@link FileKind}; anything else ⇒ null (no JSX frontend). */\nexport function jsxKindOf(id: string): FileKind | null {\n const clean = id.split('?', 1)[0] ?? id;\n if (clean.endsWith('.tsx')) return 'tsx';\n if (clean.endsWith('.jsx')) return 'jsx';\n return null;\n}\n\n/** `.html`/`.htm` ⇒ `'html'`; anything else ⇒ null (no HTML frontend). */\nexport function htmlKindOf(id: string): FileKind | null {\n const clean = (id.split('?', 1)[0] ?? id).toLowerCase();\n if (clean.endsWith('.html') || clean.endsWith('.htm')) return 'html';\n return null;\n}\n\n/** First registered source's EOL, defaulting to `\\n`. */\nfunction eolOf(doc: IRDocument): '\\n' | '\\r\\n' {\n for (const src of doc.sources.values()) return src.eol;\n return '\\n';\n}\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/** The parsed, authorized doc + the apply context + grouped passes, shared by sync + async runs. */\ninterface PreparedRun {\n readonly doc: IRDocument;\n readonly ctx: ApplyContext;\n readonly passes: readonly Pass[];\n}\n\n/** PARSE (JSX → IR, resolving classes onto `computed`) + AUTHORIZE + build the apply context. */\nfunction preparePipeline(\n code: string,\n id: string,\n kind: FileKind,\n resolver: StyleResolver,\n patterns: readonly Pattern[],\n safety: SafetyLevel,\n gate: FlattenGate,\n): PreparedRun {\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\n // AUTHORIZE — the JSX frontend defaults every node's safety floor to 0. The orchestrator opens the\n // floor to the max; the configured ceiling + each pattern's opacity predicates are the real gate.\n for (const node of doc.nodes.values()) node.meta.safetyFloor = 3;\n\n const ctx: ApplyContext = {\n doc,\n safetyCeiling: safety,\n normalizer,\n // Real CSS-selector-safety index from the active resolver: a wrapper a combinator/structural\n // selector depends on is flagged so the flatten guards refuse to flatten it. Tailwind (no\n // complexSelectors) degrades to the null index — behaviour unchanged.\n selectors: buildSelectorIndex(doc, resolver),\n resolver,\n gate,\n };\n return { doc, ctx, passes: buildPasses(patterns) };\n}\n\n/** REVERSE-EMIT optimized computed styles back into class tokens, then PRINT IR → JSX/TSX text. */\nfunction finishPipeline(optimized: IRDocument, id: string, resolver: StyleResolver): string {\n syncClassesFromComputed(optimized, resolver, normalizer);\n const printed = createJsxBackend().print(\n optimized,\n { moduleId: id, ops: [], provenance: new Map() },\n {\n normalizer,\n resolver,\n sink: createSyntheticSink(),\n eol: eolOf(optimized),\n onDiagnostic: () => {},\n },\n );\n return printed.code;\n}\n\n/** SYNC full pipeline (gate `'provably-safe'` — never changes rendering, never launches a browser). */\nexport function runJsxPipeline(\n code: string,\n id: string,\n kind: FileKind,\n resolver: StyleResolver,\n patterns: readonly Pattern[],\n safety: SafetyLevel,\n): PipelineOutput {\n const { doc, ctx, passes } = preparePipeline(code, id, kind, resolver, patterns, safety, 'provably-safe');\n const nodesIn = doc.nodes.size;\n const { doc: optimized } = runPasses(doc, passes, ctx);\n const out = finishPipeline(optimized, id, resolver);\n return { code: out, stats: computeStats(code, out, nodesIn, optimized.nodes.size) };\n}\n\n/* ───────────────────────── HTML pipeline (parse5 frontend/backend) ───────────────────────── */\n\n/**\n * PARSE (HTML → IR, resolving classes onto `computed`) + AUTHORIZE + build the apply context. Unlike\n * the JSX path, the HTML frontend sets per-node `safetyFloor` itself (opaque nodes → 0), so we must\n * NOT blanket-open every node to 3 (that would strip the opacity floors).\n */\nfunction prepareHtml(\n code: string,\n id: string,\n resolver: StyleResolver,\n patterns: readonly Pattern[],\n safety: SafetyLevel,\n gate: FlattenGate,\n): PreparedRun {\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 ctx: ApplyContext = {\n doc,\n safetyCeiling: safety,\n normalizer,\n selectors: buildSelectorIndex(doc, resolver),\n resolver,\n gate,\n };\n return { doc, ctx, passes: buildPasses(patterns) };\n}\n\n/** REVERSE-EMIT optimized computed styles back into class tokens, then PRINT IR → HTML text. */\nfunction finishHtmlPipeline(optimized: IRDocument, id: string, resolver: StyleResolver): string {\n syncClassesFromComputed(optimized, resolver, normalizer);\n const printed = createHtmlBackend().print(\n optimized,\n { moduleId: id, ops: [], provenance: new Map() },\n {\n normalizer,\n resolver,\n sink: createSyntheticSink(),\n eol: eolOf(optimized),\n onDiagnostic: () => {},\n },\n );\n return printed.code;\n}\n\n/** SYNC full HTML pipeline (gate `'provably-safe'` — surgical span edits over verbatim source). */\nexport function runHtmlPipeline(\n code: string,\n id: string,\n resolver: StyleResolver,\n patterns: readonly Pattern[],\n safety: SafetyLevel,\n): PipelineOutput {\n const { doc, ctx, passes } = prepareHtml(code, id, resolver, patterns, safety, 'provably-safe');\n const nodesIn = doc.nodes.size;\n const { doc: optimized } = runPasses(doc, passes, ctx);\n const out = finishHtmlPipeline(optimized, id, resolver);\n return { code: out, stats: computeStats(code, out, nodesIn, optimized.nodes.size) };\n}\n","/**\n * domflax — build-end optimization SUMMARY.\n *\n * A tiny, dependency-free formatter shared by the Vite and webpack/Next adapters. Each adapter\n * accumulates {@link FileStatDelta} numbers across the build into a {@link Totals}, then prints ONE\n * boxed {@link renderSummary} block at build end — so the user sees the aggregate payoff without any\n * per-file spam in between.\n *\n * ```\n * ▲ domflax\n * ────────────────────────────────\n * files optimized 42\n * DOM nodes removed 318\n * classes compressed 1,204\n * size saved 18.7 KB\n * ────────────────────────────────\n * ```\n */\n\n/** Per-file optimization delta (from a single {@link Domflax.transform}). */\nexport interface FileStatDelta {\n /** DOM/IR nodes removed by provably-safe flattens. */\n readonly nodesRemoved: number;\n /** Class tokens eliminated by semantic compression. */\n readonly classesSaved: number;\n /** Bytes saved = original byte length − output byte length (may be negative in edge cases). */\n readonly bytesSaved: number;\n}\n\n/** All-zero delta, for unsupported / unchanged files. */\nexport function zeroStats(): FileStatDelta {\n return { nodesRemoved: 0, classesSaved: 0, bytesSaved: 0 };\n}\n\n/** Aggregate accumulator across a whole build. `files` counts only files that actually changed. */\nexport interface Totals {\n files: number;\n nodesRemoved: number;\n classesCompressed: number;\n bytesSaved: number;\n}\n\n/** A fresh, zeroed {@link Totals}. */\nexport function emptyTotals(): Totals {\n return { files: 0, nodesRemoved: 0, classesCompressed: 0, bytesSaved: 0 };\n}\n\n/** Reset a {@link Totals} in place (used to clear per rebuild in watch/serve mode). */\nexport function resetTotals(t: Totals): void {\n t.files = 0;\n t.nodesRemoved = 0;\n t.classesCompressed = 0;\n t.bytesSaved = 0;\n}\n\n/** Fold one file's delta into the running totals. Only `changed` files count toward `files`. */\nexport function addStats(t: Totals, s: FileStatDelta, changed: boolean): void {\n if (!changed) return;\n t.files += 1;\n t.nodesRemoved += s.nodesRemoved;\n t.classesCompressed += s.classesSaved;\n t.bytesSaved += s.bytesSaved;\n}\n\n/* ────────────────────────────────────────────────────────────────────────── *\n * Formatting\n * ────────────────────────────────────────────────────────────────────────── */\n\nconst BYTE_UNITS = ['KB', 'MB', 'GB', 'TB'] as const;\n\n/** Human byte size: `< 1 KiB` stays `B`, otherwise KB/MB/GB/TB with one decimal (1024-based). */\nexport function formatBytes(n: number): string {\n const abs = Math.abs(n);\n if (abs < 1024) return `${n} B`;\n let value = n / 1024;\n let unit = 0;\n while (Math.abs(value) >= 1024 && unit < BYTE_UNITS.length - 1) {\n value /= 1024;\n unit += 1;\n }\n return `${value.toFixed(1)} ${BYTE_UNITS[unit]}`;\n}\n\n/** Integer with thousands separators, e.g. `1204` → `1,204` (locale-independent). */\nexport function formatCount(n: number): string {\n return Math.trunc(n)\n .toString()\n .replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n}\n\n/** Width of the label column (values align to this offset within the 3-space row indent). */\nconst LABEL_WIDTH = 20;\n/** The horizontal rule inside the box. */\nconst RULE = ` ${'─'.repeat(32)}`;\n\nfunction row(label: string, value: string): string {\n return ` ${label.padEnd(LABEL_WIDTH)}${value}`;\n}\n\n/**\n * Render the boxed build-end summary. Callers should only invoke this when `totals.files > 0`\n * (i.e. at least one file changed) so a no-op build stays silent.\n */\nexport function renderSummary(totals: Totals): string {\n return [\n '',\n ' ▲ domflax',\n RULE,\n row('files optimized', formatCount(totals.files)),\n row('DOM nodes removed', formatCount(totals.nodesRemoved)),\n row('classes compressed', formatCount(totals.classesCompressed)),\n row('size saved', formatBytes(totals.bytesSaved)),\n RULE,\n '',\n ].join('\\n');\n}\n\n/* ────────────────────────────────────────────────────────────────────────── *\n * webpack loader ↔ plugin bridge\n *\n * The webpack loader (`webpack-loader.cjs`) and the plugin (`index.cjs`) ship as SEPARATE bundles,\n * so a module-level accumulator would not be shared between them. Instead the loader stashes the\n * running {@link Totals} directly on the webpack `compilation` object under a GLOBAL-REGISTRY symbol\n * (`Symbol.for`, shared process-wide across both bundles); the plugin reads the same key from the\n * compilation in its `done` hook. A fresh compilation per (re)build gives per-build totals for free.\n * ────────────────────────────────────────────────────────────────────────── */\n\n/** Global-registry keys — identical string ⇒ identical symbol across the separately-bundled files. */\nconst TOTALS_KEY = Symbol.for('domflax.buildTotals');\nconst PRINTED_KEY = Symbol.for('domflax.summaryPrinted');\n\n/** Accumulate one file's delta onto a webpack `compilation` (called from the loader). Defensive. */\nexport function accumulateOnCompilation(compilation: unknown, stats: FileStatDelta, changed: boolean): void {\n if (compilation === null || typeof compilation !== 'object') return;\n const bag = compilation as Record<symbol, unknown>;\n let totals = bag[TOTALS_KEY] as Totals | undefined;\n if (!totals) {\n totals = emptyTotals();\n bag[TOTALS_KEY] = totals;\n }\n addStats(totals, stats, changed);\n}\n\n/**\n * Print the summary stashed on a `compilation` exactly once (called from the plugin's `done` hook).\n * Silent when nothing was stashed or nothing changed. The once-latch guards a double-tap.\n */\nexport function printCompilationSummary(compilation: unknown): void {\n if (compilation === null || typeof compilation !== 'object') return;\n const bag = compilation as Record<symbol, unknown>;\n if (bag[PRINTED_KEY]) return;\n bag[PRINTED_KEY] = true;\n const totals = bag[TOTALS_KEY] as Totals | undefined;\n if (totals && totals.files > 0) process.stdout.write(renderSummary(totals));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AA0BA,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;;;AC3B9B;AAuCA,SAAS,MAAM,GAAmB;AAChC,SAAO,OAAO,WAAW,GAAG,MAAM;AACpC;AAMA,SAAS,iBAAiB,MAAsB;AAC9C,MAAI,QAAQ;AACZ,QAAM,KAAK;AACX,MAAI;AACJ,UAAQ,IAAI,GAAG,KAAK,IAAI,OAAO,MAAM;AACnC,aAAS,EAAE,CAAC,EAAG,MAAM,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE;AAAA,EAC1D;AACA,SAAO;AACT;AAMA,SAAS,aAAa,MAAc,KAAa,SAAiB,UAAiC;AACjG,QAAM,gBAAgB,iBAAiB,IAAI;AAC3C,QAAM,eAAe,iBAAiB,GAAG;AACzC,SAAO;AAAA,IACL,cAAc,KAAK,IAAI,GAAG,UAAU,QAAQ;AAAA,IAC5C,cAAc,KAAK,IAAI,GAAG,gBAAgB,YAAY;AAAA,IACtD,YAAY,MAAM,IAAI,IAAI,MAAM,GAAG;AAAA,EACrC;AACF;AAGO,SAAS,UAAU,IAA6B;AACrD,QAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,EAAE,CAAC,KAAK;AACrC,MAAI,MAAM,SAAS,MAAM,EAAG,QAAO;AACnC,MAAI,MAAM,SAAS,MAAM,EAAG,QAAO;AACnC,SAAO;AACT;AAGO,SAAS,WAAW,IAA6B;AACtD,QAAM,SAAS,GAAG,MAAM,KAAK,CAAC,EAAE,CAAC,KAAK,IAAI,YAAY;AACtD,MAAI,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,MAAM,EAAG,QAAO;AAC9D,SAAO;AACT;AAGA,SAAS,MAAM,KAAgC;AAC7C,aAAW,OAAO,IAAI,QAAQ,OAAO,EAAG,QAAO,IAAI;AACnD,SAAO;AACT;AAGA,SAAS,YAAY,UAAsC;AACzD,QAAM,UAAU,oBAAI,IAA0B;AAC9C,aAAW,KAAK,UAAU;AACxB,UAAM,QAAS,EAAE,SAAS,MAAM,KAAK,CAAC,EAAE,CAAC,KAAK;AAC9C,QAAI,SAAS,QAAQ,IAAI,KAAK;AAC9B,QAAI,CAAC,QAAQ;AACX,eAAS,CAAC;AACV,cAAQ,IAAI,OAAO,MAAM;AAAA,IAC3B;AACA,WAAO,KAAK,CAAC;AAAA,EACf;AACA,QAAM,SAAiB,CAAC;AACxB,aAAW,CAAC,OAAO,IAAI,KAAK,SAAS;AACnC,WAAO,KAAK,EAAE,OAAO,UAAU,GAAG,KAAK,YAA4B,UAAU,KAAK,CAAC;AAAA,EACrF;AACA,SAAO;AACT;AAUA,SAAS,gBACP,MACA,IACA,MACA,UACA,UACA,QACA,MACa;AACb,QAAM,SAAS,kBAAkB,EAAE,MAAM,MAAM;AAAA,IAC7C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,CAAC;AAAA,IACT,cAAc,MAAM;AAAA,IAAC;AAAA,EACvB,CAAC;AACD,QAAM,MAAM,OAAO;AAInB,aAAW,QAAQ,IAAI,MAAM,OAAO,EAAG,MAAK,KAAK,cAAc;AAE/D,QAAM,MAAoB;AAAA,IACxB;AAAA,IACA,eAAe;AAAA,IACf;AAAA;AAAA;AAAA;AAAA,IAIA,WAAW,mBAAmB,KAAK,QAAQ;AAAA,IAC3C;AAAA,IACA;AAAA,EACF;AACA,SAAO,EAAE,KAAK,KAAK,QAAQ,YAAY,QAAQ,EAAE;AACnD;AAGA,SAAS,eAAe,WAAuB,IAAY,UAAiC;AAC1F,0BAAwB,WAAW,UAAU,UAAU;AACvD,QAAM,UAAU,iBAAiB,EAAE;AAAA,IACjC;AAAA,IACA,EAAE,UAAU,IAAI,KAAK,CAAC,GAAG,YAAY,oBAAI,IAAI,EAAE;AAAA,IAC/C;AAAA,MACE;AAAA,MACA;AAAA,MACA,MAAM,oBAAoB;AAAA,MAC1B,KAAK,MAAM,SAAS;AAAA,MACpB,cAAc,MAAM;AAAA,MAAC;AAAA,IACvB;AAAA,EACF;AACA,SAAO,QAAQ;AACjB;AAGO,SAAS,eACd,MACA,IACA,MACA,UACA,UACA,QACgB;AAChB,QAAM,EAAE,KAAK,KAAK,OAAO,IAAI,gBAAgB,MAAM,IAAI,MAAM,UAAU,UAAU,QAAQ,eAAe;AACxG,QAAM,UAAU,IAAI,MAAM;AAC1B,QAAM,EAAE,KAAK,UAAU,IAAI,UAAU,KAAK,QAAQ,GAAG;AACrD,QAAM,MAAM,eAAe,WAAW,IAAI,QAAQ;AAClD,SAAO,EAAE,MAAM,KAAK,OAAO,aAAa,MAAM,KAAK,SAAS,UAAU,MAAM,IAAI,EAAE;AACpF;AASA,SAAS,YACP,MACA,IACA,UACA,UACA,QACA,MACa;AACb,QAAM,SAAS,mBAAmB,EAAE,MAAM,MAAM;AAAA,IAC9C;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,QAAQ,CAAC;AAAA,IACT,cAAc,MAAM;AAAA,IAAC;AAAA,EACvB,CAAC;AACD,QAAM,MAAM,OAAO;AACnB,QAAM,MAAoB;AAAA,IACxB;AAAA,IACA,eAAe;AAAA,IACf;AAAA,IACA,WAAW,mBAAmB,KAAK,QAAQ;AAAA,IAC3C;AAAA,IACA;AAAA,EACF;AACA,SAAO,EAAE,KAAK,KAAK,QAAQ,YAAY,QAAQ,EAAE;AACnD;AAGA,SAAS,mBAAmB,WAAuB,IAAY,UAAiC;AAC9F,0BAAwB,WAAW,UAAU,UAAU;AACvD,QAAM,UAAU,kBAAkB,EAAE;AAAA,IAClC;AAAA,IACA,EAAE,UAAU,IAAI,KAAK,CAAC,GAAG,YAAY,oBAAI,IAAI,EAAE;AAAA,IAC/C;AAAA,MACE;AAAA,MACA;AAAA,MACA,MAAM,oBAAoB;AAAA,MAC1B,KAAK,MAAM,SAAS;AAAA,MACpB,cAAc,MAAM;AAAA,MAAC;AAAA,IACvB;AAAA,EACF;AACA,SAAO,QAAQ;AACjB;AAGO,SAAS,gBACd,MACA,IACA,UACA,UACA,QACgB;AAChB,QAAM,EAAE,KAAK,KAAK,OAAO,IAAI,YAAY,MAAM,IAAI,UAAU,UAAU,QAAQ,eAAe;AAC9F,QAAM,UAAU,IAAI,MAAM;AAC1B,QAAM,EAAE,KAAK,UAAU,IAAI,UAAU,KAAK,QAAQ,GAAG;AACrD,QAAM,MAAM,mBAAmB,WAAW,IAAI,QAAQ;AACtD,SAAO,EAAE,MAAM,KAAK,OAAO,aAAa,MAAM,KAAK,SAAS,UAAU,MAAM,IAAI,EAAE;AACpF;;;AC9PA;AA8BO,SAAS,YAA2B;AACzC,SAAO,EAAE,cAAc,GAAG,cAAc,GAAG,YAAY,EAAE;AAC3D;AAWO,SAAS,cAAsB;AACpC,SAAO,EAAE,OAAO,GAAG,cAAc,GAAG,mBAAmB,GAAG,YAAY,EAAE;AAC1E;AAGO,SAAS,YAAY,GAAiB;AAC3C,IAAE,QAAQ;AACV,IAAE,eAAe;AACjB,IAAE,oBAAoB;AACtB,IAAE,aAAa;AACjB;AAGO,SAAS,SAAS,GAAW,GAAkB,SAAwB;AAC5E,MAAI,CAAC,QAAS;AACd,IAAE,SAAS;AACX,IAAE,gBAAgB,EAAE;AACpB,IAAE,qBAAqB,EAAE;AACzB,IAAE,cAAc,EAAE;AACpB;AAMA,IAAM,aAAa,CAAC,MAAM,MAAM,MAAM,IAAI;AAGnC,SAAS,YAAY,GAAmB;AAC7C,QAAM,MAAM,KAAK,IAAI,CAAC;AACtB,MAAI,MAAM,KAAM,QAAO,GAAG,CAAC;AAC3B,MAAI,QAAQ,IAAI;AAChB,MAAI,OAAO;AACX,SAAO,KAAK,IAAI,KAAK,KAAK,QAAQ,OAAO,WAAW,SAAS,GAAG;AAC9D,aAAS;AACT,YAAQ;AAAA,EACV;AACA,SAAO,GAAG,MAAM,QAAQ,CAAC,CAAC,IAAI,WAAW,IAAI,CAAC;AAChD;AAGO,SAAS,YAAY,GAAmB;AAC7C,SAAO,KAAK,MAAM,CAAC,EAChB,SAAS,EACT,QAAQ,yBAAyB,GAAG;AACzC;AAGA,IAAM,cAAc;AAEpB,IAAM,OAAO,KAAK,SAAI,OAAO,EAAE,CAAC;AAEhC,SAAS,IAAI,OAAe,OAAuB;AACjD,SAAO,MAAM,MAAM,OAAO,WAAW,CAAC,GAAG,KAAK;AAChD;AAMO,SAAS,cAAc,QAAwB;AACpD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,IAAI,mBAAmB,YAAY,OAAO,KAAK,CAAC;AAAA,IAChD,IAAI,qBAAqB,YAAY,OAAO,YAAY,CAAC;AAAA,IACzD,IAAI,sBAAsB,YAAY,OAAO,iBAAiB,CAAC;AAAA,IAC/D,IAAI,cAAc,YAAY,OAAO,UAAU,CAAC;AAAA,IAChD;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAaA,IAAM,aAAa,uBAAO,IAAI,qBAAqB;AACnD,IAAM,cAAc,uBAAO,IAAI,wBAAwB;AAGhD,SAAS,wBAAwB,aAAsB,OAAsB,SAAwB;AAC1G,MAAI,gBAAgB,QAAQ,OAAO,gBAAgB,SAAU;AAC7D,QAAM,MAAM;AACZ,MAAI,SAAS,IAAI,UAAU;AAC3B,MAAI,CAAC,QAAQ;AACX,aAAS,YAAY;AACrB,QAAI,UAAU,IAAI;AAAA,EACpB;AACA,WAAS,QAAQ,OAAO,OAAO;AACjC;AAMO,SAAS,wBAAwB,aAA4B;AAClE,MAAI,gBAAgB,QAAQ,OAAO,gBAAgB,SAAU;AAC7D,QAAM,MAAM;AACZ,MAAI,IAAI,WAAW,EAAG;AACtB,MAAI,WAAW,IAAI;AACnB,QAAM,SAAS,IAAI,UAAU;AAC7B,MAAI,UAAU,OAAO,QAAQ,EAAG,SAAQ,OAAO,MAAM,cAAc,MAAM,CAAC;AAC5E;;;AF/EA,IAAM,kBAAqC,CAAC,QAAQ,QAAQ,SAAS,MAAM;AAE3E,SAAS,eAAe,SAAiD;AACvE,SAAO;AAAA,IACL,UAAU,QAAQ,YAAY;AAAA,IAC9B,UAAU,QAAQ,YAAY,CAAC;AAAA,IAC/B,QAAQ,QAAQ,UAAU;AAAA,IAC1B,QAAQ,QAAQ,UAAU;AAAA,IAC1B,SAAS,QAAQ,WAAW;AAAA,EAC9B;AACF;AAGA,SAAS,YAAY,IAAY,SAAqC;AAEpE,QAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,EAAE,CAAC,KAAK;AACrC,SAAO,QAAQ,KAAK,CAAC,QAAQ,MAAM,SAAS,GAAG,CAAC;AAClD;AAmDA,SAAS,eAAe,UAAiD;AACvE,MAAI,SAAS,aAAa,UAAU;AAClC,WAAO,kBAAkB,CAAC,GAAG,EAAE,OAAO,SAAS,SAAS,CAAC;AAAA,EAC3D;AAEA,SAAO,uBAAuB;AAChC;AAEO,SAAS,cAAc,UAA0B,CAAC,GAAY;AACnE,QAAM,WAAW,eAAe,OAAO;AACvC,QAAM,WAAW,eAAe;AAChC,QAAM,WAAW;AAIjB,MAAI,iBAAuC;AAC3C,QAAM,cAAc,MAAsB,mBAAmB,eAAe,QAAQ;AAEpF,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,IAAI,WAA0B;AAC5B,aAAO,YAAY;AAAA,IACrB;AAAA,IACA;AAAA,IACA,UAAU,MAAc,IAAoC;AAC1D,UAAI,CAAC,YAAY,IAAI,SAAS,OAAO,EAAG,QAAO,EAAE,MAAM,KAAK,MAAM,OAAO,UAAU,EAAE;AACrF,YAAM,OAAO,UAAU,EAAE;AACzB,UAAI,SAAS,MAAM;AACjB,cAAM,MAAM,eAAe,MAAM,IAAI,MAAM,YAAY,GAAG,UAAU,SAAS,MAAM;AACnF,eAAO,EAAE,MAAM,IAAI,MAAM,KAAK,MAAM,OAAO,IAAI,MAAM;AAAA,MACvD;AAEA,UAAI,WAAW,EAAE,MAAM,MAAM;AAC3B,cAAM,MAAM,gBAAgB,MAAM,IAAI,YAAY,GAAG,UAAU,SAAS,MAAM;AAC9E,eAAO,EAAE,MAAM,IAAI,MAAM,KAAK,MAAM,OAAO,IAAI,MAAM;AAAA,MACvD;AACA,aAAO,EAAE,MAAM,KAAK,MAAM,OAAO,UAAU,EAAE;AAAA,IAC/C;AAAA,EACF;AACF;AAsCO,SAAS,KAAK,UAA0B,CAAC,GAAsB;AACpE,QAAM,SAAS,cAAc,OAAO;AAKpC,QAAM,SAAiB,YAAY;AACnC,MAAI,UAAU;AAEd,QAAM,eAAe,MAAY;AAC/B,QAAI,QAAS;AACb,cAAU;AACV,QAAI,OAAO,QAAQ,EAAG,SAAQ,OAAO,MAAM,cAAc,MAAM,CAAC;AAAA,EAClE;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aAAmB;AACjB,kBAAY,MAAM;AAClB,gBAAU;AAAA,IACZ;AAAA,IACA,UAAU,MAAc,IAA2C;AACjE,UAAI,CAAC,YAAY,IAAI,OAAO,QAAQ,OAAO,EAAG,QAAO;AACrD,YAAM,MAAM,OAAO,UAAU,MAAM,EAAE;AACrC,YAAM,UAAU,IAAI,SAAS;AAC7B,eAAS,QAAQ,IAAI,OAAO,OAAO;AAEnC,aAAO,UAAU,MAAM;AAAA,IACzB;AAAA,IACA,WAAiB;AACf,mBAAa;AAAA,IACf;AAAA,IACA,cAAoB;AAClB,mBAAa;AAAA,IACf;AAAA,EACF;AACF;AAqDA,IAAM,mBAAmB;AAQzB,SAAS,oBAA4B;AACnC,QAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AACnD,SAAO,KAAK,MAAM,oBAAoB;AACxC;AA+BA,SAAS,kBAAkB,UAAwC;AACjE,QAAM,OAAO,SAAS,OAAO;AAC7B,MAAI,OAAO,MAAM,QAAQ,WAAY;AACrC,OAAK,IAAI,WAAW,CAAC,UAAmB;AAGtC,UAAM,cAAe,OAA4C,eAAe;AAChF,4BAAwB,WAAW;AAAA,EACrC,CAAC;AACH;AAOA,SAAS,sBAAsB,UAAkC,MAAsC;AACrG,MAAI,OAAO,SAAS,OAAO,MAAM,QAAQ,YAAY;AACnD,sBAAkB,QAAQ;AAC1B;AAAA,EACF;AACA,QAAM,UAAW,KAAK,YAAY,CAAC;AACnC,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,YAAQ,KAAK,EAAE,OAAO,CAAC,SAAiC,kBAAkB,IAAI,EAAE,CAAC;AAAA,EACnF;AACF;AAEO,SAAS,QAAQ,UAA0B,CAAC,GAAyB;AAE1E,gBAAc,OAAO;AACrB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,UAAwC;AAG5C,YAAM,OAAiC,SAAS,WAAW;AAC3D,YAAM,MAAO,KAAK,WAAW,CAAC;AAC9B,YAAM,QAAS,IAAI,UAAU,CAAC;AAC9B,YAAM,OAA0B;AAAA,QAC9B,MAAM;AAAA,QACN,SAAS;AAAA,QACT,SAAS;AAAA,QACT,KAAK,CAAC,EAAE,QAAQ,kBAAkB,GAAG,QAAQ,CAAC;AAAA,MAChD;AACA,YAAM,KAAK,IAAI;AAEf,4BAAsB,UAAU,IAAI;AAAA,IACtC;AAAA,EACF;AACF;AAeA,IAAM,UAA0B,EAAE,eAAe,MAAM,QAAQ;AAC/D,IAAO,cAAQ;","names":[]}