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/dist/cli.js CHANGED
@@ -1,23 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- builtinPatterns,
4
- createCssResolver,
5
- createJsxBackend,
6
- createJsxFrontend,
7
- createTailwindResolver
8
- } from "./chunk-DNHOGPYV.js";
9
- import {
10
- buildSelectorIndex,
11
- createSyntheticSink,
12
- normalizer,
13
- runPasses,
14
- syncClassesFromComputed
15
- } from "./chunk-DWLB7FRR.js";
3
+ createTransform,
4
+ destinationFor,
5
+ isGitClean,
6
+ planWrites
7
+ } from "./chunk-EYQXQQQH.js";
8
+ import "./chunk-FPT4EJ6Q.js";
9
+ import "./chunk-TTJEXWAC.js";
16
10
  import {
17
11
  __commonJS,
12
+ __dirname,
18
13
  __toESM,
19
14
  init_esm_shims
20
- } from "./chunk-6WVVF6AD.js";
15
+ } from "./chunk-U5GOONKV.js";
21
16
 
22
17
  // ../../node_modules/sisteransi/src/index.js
23
18
  var require_src = __commonJS({
@@ -172,6 +167,14 @@ function toSafety(raw) {
172
167
  if (n === 0 || n === 1 || n === 2 || n === 3) return n;
173
168
  throw new Error(`domflax: invalid --safety "${raw}" (expected 0, 1, 2 or 3)`);
174
169
  }
170
+ function toPositiveInt(raw, flag) {
171
+ if (raw === void 0) return null;
172
+ const n = Number(raw);
173
+ if (!Number.isInteger(n) || n < 1) {
174
+ throw new Error(`domflax: invalid ${flag} "${raw}" (expected a positive integer)`);
175
+ }
176
+ return n;
177
+ }
175
178
  function parseInvocation(argv) {
176
179
  const { values, positionals } = parseArgs({
177
180
  args: argv,
@@ -182,12 +185,15 @@ function parseInvocation(argv) {
182
185
  css: { type: "string", multiple: true },
183
186
  "dry-run": { type: "boolean", default: false },
184
187
  report: { type: "boolean", default: false },
188
+ details: { type: "boolean", default: false },
185
189
  "dangerously-overwrite-source": { type: "boolean", default: false },
186
190
  "no-git-check": { type: "boolean", default: false },
187
191
  "no-interactive": { type: "boolean", default: false },
188
192
  yes: { type: "boolean", short: "y", default: false },
189
193
  safety: { type: "string" },
190
- "project-root": { type: "string" }
194
+ "project-root": { type: "string" },
195
+ "max-memory": { type: "string" },
196
+ concurrency: { type: "string" }
191
197
  }
192
198
  });
193
199
  const provider = values.provider ?? DEFAULT_PROVIDER;
@@ -201,12 +207,15 @@ function parseInvocation(argv) {
201
207
  css: values.css ?? [],
202
208
  dryRun: values["dry-run"] === true,
203
209
  report: values.report === true,
210
+ details: values.details === true,
204
211
  dangerouslyOverwriteSource: values["dangerously-overwrite-source"] === true,
205
212
  noGitCheck: values["no-git-check"] === true,
206
213
  interactive: values["no-interactive"] !== true && values.yes !== true,
207
214
  passes: null,
208
215
  safety: toSafety(values.safety),
209
- projectRoot: values["project-root"] ?? null
216
+ projectRoot: values["project-root"] ?? null,
217
+ maxMemory: toPositiveInt(values["max-memory"], "--max-memory"),
218
+ concurrency: toPositiveInt(values.concurrency, "--concurrency")
210
219
  };
211
220
  }
212
221
  function shouldPrompt(options, isTty) {
@@ -215,7 +224,7 @@ function shouldPrompt(options, isTty) {
215
224
  var USAGE = [
216
225
  "Usage: domflax [paths...] [options]",
217
226
  "",
218
- "Optimizes .jsx/.tsx files (flatten redundant wrappers + compress class sets).",
227
+ "Optimizes .jsx/.tsx/.html files (flatten redundant wrappers + compress class sets).",
219
228
  "Source is READ-ONLY by default \u2014 output goes to --out or ./domflax-out.",
220
229
  "",
221
230
  "Arguments:",
@@ -227,221 +236,225 @@ var USAGE = [
227
236
  " --css <file...> stylesheets feeding the custom-CSS provider",
228
237
  " --dry-run print per-file diffs; write nothing",
229
238
  " --report print a summary of what changed",
239
+ " --details print per-file optimization stats (nodes/classes/bytes)",
230
240
  " --dangerously-overwrite-source overwrite source in place (needs a clean git tree)",
231
241
  " --no-git-check skip the clean-git-tree gate",
232
242
  " --safety <0|1|2|3> optimization aggressiveness (default: 2)",
243
+ " --max-memory <MB> memory budget; caps pool RAM AND parallelism (default: ~70% free RAM)",
244
+ " --concurrency <N> max parallel workers (still clamped by --max-memory)",
233
245
  " --yes, --no-interactive never launch the wizard (CI-safe)",
234
246
  "",
247
+ "Many files are processed across CPU cores by a memory-bounded worker pool; small jobs run inline.",
235
248
  "With no paths in an interactive terminal, a guided wizard launches."
236
249
  ].join("\n");
237
250
 
238
- // ../cli/src/safety.ts
251
+ // ../cli/src/pool.ts
239
252
  init_esm_shims();
240
- import { execFileSync } from "child_process";
253
+ import { existsSync } from "fs";
254
+ import * as os from "os";
241
255
  import * as path from "path";
242
- var DISPOSABLE_DIRS = /* @__PURE__ */ new Set(["dist", "build", "out", ".next"]);
243
- function isDisposablePath(file) {
244
- return path.resolve(file).split(path.sep).some((seg) => DISPOSABLE_DIRS.has(seg));
256
+ import { fileURLToPath } from "url";
257
+ import { Worker } from "worker_threads";
258
+ var PER_WORKER_MB = 160;
259
+ var MIN_OLD_GEN_MB = 64;
260
+ function emptyTotals() {
261
+ return { files: 0, changed: 0, nodesRemoved: 0, classesSaved: 0, bytesSaved: 0 };
245
262
  }
246
- function isGitClean(cwd) {
247
- try {
248
- const out = execFileSync("git", ["status", "--porcelain"], {
249
- cwd,
250
- encoding: "utf8",
251
- stdio: ["ignore", "pipe", "ignore"]
252
- });
253
- return out.trim().length === 0;
254
- } catch {
255
- return false;
256
- }
257
- }
258
- function planWrites(options, gitClean) {
259
- if (options.dangerouslyOverwriteSource) {
260
- if (!options.noGitCheck && !gitClean) {
261
- return {
262
- ok: false,
263
- error: "refusing --dangerously-overwrite-source: git working tree is not clean. Commit or stash first, or pass --no-git-check to override."
264
- };
265
- }
266
- return { ok: true, value: { mode: "overwrite-source", outDir: null } };
267
- }
268
- const outDir = path.resolve(options.out ?? "domflax-out");
269
- return { ok: true, value: { mode: "out-dir", outDir } };
270
- }
271
- function destinationFor(file, inputRoot, plan) {
272
- const absFile = path.resolve(file);
273
- if (plan.mode === "overwrite-source") {
274
- return { ok: true, value: absFile };
275
- }
276
- const outDir = plan.outDir;
277
- const rel = path.relative(inputRoot, absFile);
278
- const safeRel = rel === "" || rel.startsWith("..") || path.isAbsolute(rel) ? path.basename(absFile) : rel;
279
- const dest = path.join(outDir, safeRel);
280
- if (path.resolve(dest) === absFile && !isDisposablePath(absFile)) {
281
- return {
282
- ok: false,
283
- 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).`
284
- };
285
- }
286
- return { ok: true, value: dest };
263
+ function addStats(totals, stats, changed) {
264
+ totals.files += 1;
265
+ if (changed) totals.changed += 1;
266
+ totals.nodesRemoved += stats.nodesRemoved;
267
+ totals.classesSaved += stats.classesSaved;
268
+ totals.bytesSaved += stats.bytesSaved;
287
269
  }
288
-
289
- // ../cli/src/transform.ts
290
- init_esm_shims();
291
- function buildResolver(provider, css, projectRoot) {
292
- if (provider === "custom") {
293
- return createCssResolver([], { files: css, projectRoot });
294
- }
295
- return createTailwindResolver({ projectRoot });
270
+ function clamp(n, lo, hi) {
271
+ return Math.max(lo, Math.min(hi, n));
296
272
  }
297
- function buildPasses(patterns) {
298
- const byPhase = /* @__PURE__ */ new Map();
299
- for (const p2 of patterns) {
300
- const phase = p2.category.split("/", 1)[0] ?? "flatten";
301
- let bucket = byPhase.get(phase);
302
- if (!bucket) {
303
- bucket = [];
304
- byPhase.set(phase, bucket);
305
- }
306
- bucket.push(p2);
307
- }
308
- const passes = [];
309
- for (const [phase, pats] of byPhase) {
310
- passes.push({ phase, category: `${phase}/builtin`, patterns: pats });
311
- }
312
- return passes;
273
+ function computeWorkerCount(options) {
274
+ const cpus2 = Math.max(1, os.cpus().length);
275
+ const freeMB = Math.floor(os.freemem() / (1024 * 1024));
276
+ const budgetMB = Math.max(PER_WORKER_MB, options.maxMemory ?? Math.floor(freeMB * 0.7));
277
+ const byMemory = Math.max(1, Math.floor(budgetMB / PER_WORKER_MB));
278
+ const target = options.concurrency ?? Math.max(1, cpus2 - 1);
279
+ const workers = clamp(Math.min(target, byMemory), 1, byMemory);
280
+ const perWorkerCapMB = Math.max(MIN_OLD_GEN_MB, Math.floor(budgetMB / workers));
281
+ return { workers, budgetMB, perWorkerCapMB };
313
282
  }
314
- function selectPatterns(names) {
315
- if (names === null) return builtinPatterns;
316
- const set = new Set(names);
317
- return builtinPatterns.filter((p2) => set.has(p2.name));
283
+ function inlineThreshold(workers) {
284
+ return Math.max(4, 2 * workers);
318
285
  }
319
- function jsxKindOf(id) {
320
- const clean = id.split("?", 1)[0] ?? id;
321
- const lower = clean.toLowerCase();
322
- if (lower.endsWith(".tsx")) return "tsx";
323
- if (lower.endsWith(".jsx")) return "jsx";
324
- return null;
286
+ function shouldUsePool(fileCount, plan) {
287
+ return plan.workers > 1 && fileCount > inlineThreshold(plan.workers);
325
288
  }
326
- function countClassTokens(code) {
327
- let total = 0;
328
- const re = /\b(?:className|class)\s*=\s*"([^"]*)"/g;
329
- let m2;
330
- while ((m2 = re.exec(code)) !== null) {
331
- total += m2[1].split(/\s+/).filter((t) => t.length > 0).length;
289
+ function moduleDir() {
290
+ try {
291
+ return path.dirname(fileURLToPath(import.meta.url));
292
+ } catch {
293
+ return typeof __dirname !== "undefined" ? __dirname : process.cwd();
332
294
  }
333
- return total;
334
295
  }
335
- function bytes(s) {
336
- return Buffer.byteLength(s, "utf8");
337
- }
338
- function passthroughResult(code) {
339
- return {
340
- code,
341
- changed: false,
342
- passthrough: true,
343
- stats: {
344
- nodesIn: 0,
345
- nodesOut: 0,
346
- nodesRemoved: 0,
347
- classesBefore: 0,
348
- classesAfter: 0,
349
- classesSaved: 0,
350
- bytesBefore: bytes(code),
351
- bytesAfter: bytes(code),
352
- bytesSaved: 0
353
- }
354
- };
296
+ function resolveWorkerPath() {
297
+ const dir = moduleDir();
298
+ const candidates = [
299
+ path.join(dir, "worker.cjs"),
300
+ path.join(dir, "worker.js"),
301
+ path.join(dir, "..", "dist", "worker.cjs"),
302
+ path.join(dir, "..", "dist", "worker.js")
303
+ ];
304
+ for (const c of candidates) {
305
+ if (existsSync(c)) return c;
306
+ }
307
+ return candidates[0];
355
308
  }
356
- function createTransform(options) {
357
- const projectRoot = options.projectRoot ?? process.cwd();
358
- const resolver = buildResolver(options.provider, options.css, projectRoot);
359
- const patterns = selectPatterns(options.passes);
360
- function prepare(code, id, kind, gate) {
361
- const parsed = createJsxFrontend().parse(code, {
362
- id,
363
- kind,
364
- resolver,
365
- normalizer,
366
- config: {},
367
- onDiagnostic: () => {
309
+ function runPool(files, init, plan, onWrote) {
310
+ const workerPath = resolveWorkerPath();
311
+ const totals = emptyTotals();
312
+ const wrote = [];
313
+ const changedFiles = [];
314
+ const errors = [];
315
+ let failures = 0;
316
+ const budgetBytes = plan.budgetMB * 1024 * 1024;
317
+ let nextIndex = 0;
318
+ let completed = 0;
319
+ const total = files.length;
320
+ let respawns = 0;
321
+ const maxRespawns = total + plan.workers + 8;
322
+ return new Promise((resolve3) => {
323
+ const handles = /* @__PURE__ */ new Set();
324
+ const finishIfDone = () => {
325
+ if (completed < total) return;
326
+ for (const h2 of handles) {
327
+ if (!h2.dead) {
328
+ try {
329
+ h2.worker.postMessage({ type: "stop" });
330
+ } catch {
331
+ }
332
+ void h2.worker.terminate();
333
+ }
368
334
  }
369
- });
370
- const doc = parsed.doc;
371
- const nodesIn = doc.nodes.size;
372
- for (const node of doc.nodes.values()) node.meta.safetyFloor = 3;
373
- const ctx = {
374
- doc,
375
- safetyCeiling: options.safety,
376
- normalizer,
377
- // Real CSS-selector-safety index from the active resolver (custom-CSS reports combinator /
378
- // structural-pseudo coupling; Tailwind has none → null index, behaviour unchanged).
379
- selectors: buildSelectorIndex(doc, resolver),
380
- resolver,
381
- gate
335
+ handles.clear();
336
+ resolve3({ totals, failures, wrote, changedFiles, errors });
382
337
  };
383
- return { doc, ctx, passes: buildPasses(patterns), nodesIn };
384
- }
385
- function finish(code, optimized, id, nodesIn) {
386
- syncClassesFromComputed(optimized, resolver, normalizer);
387
- const printed = createJsxBackend().print(
388
- optimized,
389
- { moduleId: id, ops: [], provenance: /* @__PURE__ */ new Map() },
390
- { normalizer, resolver, sink: createSyntheticSink(), eol: "\n", onDiagnostic: () => {
391
- } }
392
- );
393
- const out = printed.code;
394
- const nodesOut = optimized.nodes.size;
395
- const classesBefore = countClassTokens(code);
396
- const classesAfter = countClassTokens(out);
397
- return {
398
- code: out,
399
- changed: out !== code,
400
- passthrough: false,
401
- stats: {
402
- nodesIn,
403
- nodesOut,
404
- nodesRemoved: Math.max(0, nodesIn - nodesOut),
405
- classesBefore,
406
- classesAfter,
407
- classesSaved: Math.max(0, classesBefore - classesAfter),
408
- bytesBefore: bytes(code),
409
- bytesAfter: bytes(out),
410
- bytesSaved: bytes(code) - bytes(out)
338
+ const recordFailure = (file, error) => {
339
+ failures += 1;
340
+ completed += 1;
341
+ errors.push({ path: file, error });
342
+ };
343
+ const dispatch = (h2) => {
344
+ if (h2.dead) return;
345
+ if (nextIndex >= total) {
346
+ h2.current = null;
347
+ try {
348
+ h2.worker.postMessage({ type: "stop" });
349
+ } catch {
350
+ }
351
+ return;
352
+ }
353
+ if (process.memoryUsage().rss > budgetBytes) {
354
+ setTimeout(() => dispatch(h2), 25);
355
+ return;
356
+ }
357
+ const file = files[nextIndex++];
358
+ h2.current = file;
359
+ try {
360
+ h2.worker.postMessage({ type: "file", path: file });
361
+ } catch {
362
+ onWorkerDown(h2);
411
363
  }
412
364
  };
413
- }
414
- return {
415
- resolver,
416
- transformFile(code, id) {
417
- const kind = jsxKindOf(id);
418
- if (kind === null) return passthroughResult(code);
419
- const { doc, ctx, passes, nodesIn } = prepare(code, id, kind, "provably-safe");
420
- const { doc: optimized } = runPasses(doc, passes, ctx);
421
- return finish(code, optimized, id, nodesIn);
422
- }
423
- };
365
+ const onMessage = (h2, msg) => {
366
+ if (msg.type === "ready") {
367
+ h2.ready = true;
368
+ dispatch(h2);
369
+ return;
370
+ }
371
+ h2.current = null;
372
+ if (msg.ok) {
373
+ addStats(totals, msg.stats, msg.changed);
374
+ if (msg.wrote) {
375
+ wrote.push(msg.wrote);
376
+ changedFiles.push({ dest: msg.wrote, stats: msg.stats });
377
+ onWrote?.(msg.wrote);
378
+ }
379
+ } else {
380
+ failures += 1;
381
+ errors.push({ path: msg.path, error: msg.error });
382
+ }
383
+ completed += 1;
384
+ if (completed >= total) {
385
+ finishIfDone();
386
+ return;
387
+ }
388
+ dispatch(h2);
389
+ };
390
+ const drainRemaining = (reason) => {
391
+ while (nextIndex < total) recordFailure(files[nextIndex++], reason);
392
+ finishIfDone();
393
+ };
394
+ const onWorkerDown = (h2) => {
395
+ if (h2.dead) return;
396
+ h2.dead = true;
397
+ handles.delete(h2);
398
+ const lost = h2.current;
399
+ h2.current = null;
400
+ if (lost !== null) recordFailure(lost, "worker crashed while processing this file");
401
+ void h2.worker.terminate();
402
+ if (completed >= total) {
403
+ finishIfDone();
404
+ return;
405
+ }
406
+ if (nextIndex >= total) {
407
+ if (handles.size === 0) finishIfDone();
408
+ return;
409
+ }
410
+ if (respawns < maxRespawns) {
411
+ respawns += 1;
412
+ spawn();
413
+ } else if (handles.size === 0) {
414
+ drainRemaining("worker pool exhausted its respawn budget (memory cap too small?)");
415
+ }
416
+ };
417
+ const spawn = () => {
418
+ let worker;
419
+ try {
420
+ worker = new Worker(workerPath, {
421
+ workerData: init,
422
+ resourceLimits: { maxOldGenerationSizeMb: plan.perWorkerCapMB }
423
+ });
424
+ } catch {
425
+ if (nextIndex < total) recordFailure(files[nextIndex++], "failed to spawn worker");
426
+ if (completed >= total) finishIfDone();
427
+ else if (handles.size === 0 && nextIndex < total) spawn();
428
+ return;
429
+ }
430
+ const h2 = { worker, current: null, ready: false, dead: false };
431
+ handles.add(h2);
432
+ worker.on("message", (m2) => onMessage(h2, m2));
433
+ worker.on("error", () => onWorkerDown(h2));
434
+ worker.on("exit", (code) => {
435
+ if (code !== 0) onWorkerDown(h2);
436
+ });
437
+ };
438
+ const initial = Math.min(plan.workers, total);
439
+ for (let i = 0; i < initial; i++) spawn();
440
+ if (initial === 0) finishIfDone();
441
+ });
424
442
  }
425
443
 
426
444
  // ../cli/src/walk.ts
427
445
  init_esm_shims();
428
446
  import * as fs from "fs";
429
447
  import * as path2 from "path";
430
- var SUPPORTED_EXTS = [".jsx", ".tsx"];
431
- var HTML_EXTS = [".html", ".htm"];
448
+ var SUPPORTED_EXTS = [".jsx", ".tsx", ".html", ".htm"];
432
449
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "domflax-out"]);
433
450
  function isSupported(file) {
434
451
  const lower = file.toLowerCase();
435
452
  return SUPPORTED_EXTS.some((ext) => lower.endsWith(ext));
436
453
  }
437
- function isHtml(file) {
438
- const lower = file.toLowerCase();
439
- return HTML_EXTS.some((ext) => lower.endsWith(ext));
440
- }
441
454
  function hasGlobMagic(p2) {
442
455
  return /[*?[\]{}]/.test(p2);
443
456
  }
444
- function walkDir(dir, out, counts) {
457
+ function walkDir(dir, out) {
445
458
  let entries;
446
459
  try {
447
460
  entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -452,10 +465,9 @@ function walkDir(dir, out, counts) {
452
465
  const full = path2.join(dir, entry.name);
453
466
  if (entry.isDirectory()) {
454
467
  if (SKIP_DIRS.has(entry.name)) continue;
455
- walkDir(full, out, counts);
468
+ walkDir(full, out);
456
469
  } else if (entry.isFile()) {
457
470
  if (isSupported(entry.name)) out.push(full);
458
- else if (isHtml(entry.name)) counts.html += 1;
459
471
  }
460
472
  }
461
473
  }
@@ -466,7 +478,6 @@ function globSyncMaybe() {
466
478
  function discoverInputs(paths) {
467
479
  const files = [];
468
480
  const warnings = [];
469
- const counts = { html: 0 };
470
481
  const seen = /* @__PURE__ */ new Set();
471
482
  const push = (f) => {
472
483
  const abs = path2.resolve(f);
@@ -490,12 +501,11 @@ function discoverInputs(paths) {
490
501
  stat = null;
491
502
  }
492
503
  if (stat?.isDirectory()) {
493
- walkDir(path2.resolve(p2), files, counts);
504
+ walkDir(path2.resolve(p2), files);
494
505
  continue;
495
506
  }
496
507
  if (stat?.isFile()) {
497
508
  if (isSupported(p2)) push(p2);
498
- else if (isHtml(p2)) counts.html += 1;
499
509
  else warnings.push(`unsupported file type, skipped: ${p2}`);
500
510
  continue;
501
511
  }
@@ -507,8 +517,7 @@ function discoverInputs(paths) {
507
517
  }
508
518
  const matched = glob(p2);
509
519
  const supported = matched.filter(isSupported);
510
- counts.html += matched.filter(isHtml).length;
511
- if (supported.length === 0) warnings.push(`no .jsx/.tsx files matched: ${p2}`);
520
+ if (supported.length === 0) warnings.push(`no .jsx/.tsx/.html files matched: ${p2}`);
512
521
  for (const m2 of supported) push(m2);
513
522
  continue;
514
523
  }
@@ -523,11 +532,6 @@ function discoverInputs(paths) {
523
532
  deduped.push(abs);
524
533
  }
525
534
  }
526
- if (deduped.length === 0 && counts.html > 0) {
527
- warnings.push(
528
- `found ${counts.html} .html file${counts.html === 1 ? "" : "s"} but HTML optimization isn't supported yet (domflax currently optimizes .jsx/.tsx source; HTML is on the roadmap: https://github.com/Krishnesh-Mishra/domflax#roadmap).`
529
- );
530
- }
531
535
  return { files: deduped, inputRoot, warnings };
532
536
  }
533
537
 
@@ -1305,12 +1309,8 @@ async function runWizard(base) {
1305
1309
  }
1306
1310
 
1307
1311
  // ../cli/src/index.ts
1308
- function addStats(totals, stats, changed) {
1309
- totals.files += 1;
1310
- if (changed) totals.changed += 1;
1311
- totals.nodesRemoved += stats.nodesRemoved;
1312
- totals.classesSaved += stats.classesSaved;
1313
- totals.bytesSaved += stats.bytesSaved;
1312
+ function fileDetail(stats) {
1313
+ return `${stats.nodesRemoved} nodes, ${stats.classesSaved} classes, ${stats.bytesSaved} bytes`;
1314
1314
  }
1315
1315
  function printReport(totals) {
1316
1316
  console.log("");
@@ -1321,23 +1321,8 @@ function printReport(totals) {
1321
1321
  console.log(` classes saved : ${totals.classesSaved}`);
1322
1322
  console.log(` bytes saved : ${totals.bytesSaved}`);
1323
1323
  }
1324
- async function execute(options) {
1325
- const { files, inputRoot, warnings } = discoverInputs(options.paths);
1326
- for (const w2 of warnings) console.error(`domflax: ${w2}`);
1327
- if (files.length === 0) {
1328
- console.error("domflax: no .jsx/.tsx files found for the given paths");
1329
- return { exitCode: 1 };
1330
- }
1331
- const projectRoot = options.projectRoot ?? process.cwd();
1332
- const gitClean = options.dangerouslyOverwriteSource && !options.noGitCheck ? isGitClean(projectRoot) : true;
1333
- const planned = planWrites(options, gitClean);
1334
- if (!planned.ok) {
1335
- console.error(`domflax: ${planned.error}`);
1336
- return { exitCode: 1 };
1337
- }
1338
- const plan = planned.value;
1324
+ function runInline(files, options, inputRoot, plan, totals) {
1339
1325
  const transform = createTransform(options);
1340
- const totals = { files: 0, changed: 0, nodesRemoved: 0, classesSaved: 0, bytesSaved: 0 };
1341
1326
  let failures = 0;
1342
1327
  for (const file of files) {
1343
1328
  let code;
@@ -1352,8 +1337,13 @@ async function execute(options) {
1352
1337
  addStats(totals, result.stats, result.changed);
1353
1338
  if (options.dryRun) {
1354
1339
  const rel = path4.relative(inputRoot, file) || path4.basename(file);
1355
- if (result.changed) console.log(unifiedDiff(code, result.code, rel));
1356
- else if (!options.report) console.log(` (unchanged) ${rel}`);
1340
+ if (result.changed) {
1341
+ console.log(
1342
+ options.details ? `domflax: ${rel} \u2014 ${fileDetail(result.stats)}` : unifiedDiff(code, result.code, rel)
1343
+ );
1344
+ } else if (!options.report && !options.details) {
1345
+ console.log(` (unchanged) ${rel}`);
1346
+ }
1357
1347
  continue;
1358
1348
  }
1359
1349
  if (!result.changed) continue;
@@ -1366,12 +1356,58 @@ async function execute(options) {
1366
1356
  try {
1367
1357
  mkdirSync(path4.dirname(target.value), { recursive: true });
1368
1358
  writeFileSync(target.value, result.code, "utf8");
1369
- console.log(`domflax: wrote ${path4.relative(process.cwd(), target.value) || target.value}`);
1359
+ const rel = path4.relative(process.cwd(), target.value) || target.value;
1360
+ console.log(options.details ? `domflax: wrote ${rel} \u2014 ${fileDetail(result.stats)}` : `domflax: wrote ${rel}`);
1370
1361
  } catch (err) {
1371
1362
  console.error(`domflax: cannot write ${target.value}: ${String(err?.message ?? err)}`);
1372
1363
  failures += 1;
1373
1364
  }
1374
1365
  }
1366
+ return failures;
1367
+ }
1368
+ async function execute(options) {
1369
+ const { files, inputRoot, warnings } = discoverInputs(options.paths);
1370
+ for (const w2 of warnings) console.error(`domflax: ${w2}`);
1371
+ if (files.length === 0) {
1372
+ console.error("domflax: no .jsx/.tsx files found for the given paths");
1373
+ return { exitCode: 1 };
1374
+ }
1375
+ const projectRoot = options.projectRoot ?? process.cwd();
1376
+ const gitClean = options.dangerouslyOverwriteSource && !options.noGitCheck ? isGitClean(projectRoot) : true;
1377
+ const planned = planWrites(options, gitClean);
1378
+ if (!planned.ok) {
1379
+ console.error(`domflax: ${planned.error}`);
1380
+ return { exitCode: 1 };
1381
+ }
1382
+ const plan = planned.value;
1383
+ const poolPlan = computeWorkerCount(options);
1384
+ const usePool = !options.dryRun && shouldUsePool(files.length, poolPlan);
1385
+ const totals = emptyTotals();
1386
+ let failures = 0;
1387
+ if (usePool) {
1388
+ const outcome = await runPool(
1389
+ files,
1390
+ { options, inputRoot, plan },
1391
+ poolPlan
1392
+ // Per-file "wrote" lines are collected and printed in deterministic (sorted) order below.
1393
+ );
1394
+ Object.assign(totals, outcome.totals);
1395
+ failures = outcome.failures;
1396
+ for (const { path: p2, error } of outcome.errors) {
1397
+ console.error(`domflax: failed ${path4.relative(process.cwd(), p2) || p2}: ${error}`);
1398
+ }
1399
+ if (options.details) {
1400
+ for (const { dest, stats } of [...outcome.changedFiles].sort((a, b3) => a.dest.localeCompare(b3.dest))) {
1401
+ console.log(`domflax: wrote ${path4.relative(process.cwd(), dest) || dest} \u2014 ${fileDetail(stats)}`);
1402
+ }
1403
+ } else {
1404
+ for (const dest of [...outcome.wrote].sort()) {
1405
+ console.log(`domflax: wrote ${path4.relative(process.cwd(), dest) || dest}`);
1406
+ }
1407
+ }
1408
+ } else {
1409
+ failures += runInline(files, options, inputRoot, plan, totals);
1410
+ }
1375
1411
  if (options.dryRun) {
1376
1412
  console.log("\ndomflax: dry run \u2014 no files were written.");
1377
1413
  } else if (totals.changed === 0) {