domflax 0.1.1 → 0.1.4

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 (46) hide show
  1. package/README.md +25 -8
  2. package/dist/{chunk-DNHOGPYV.js → chunk-3Z5ZWLXX.js} +407 -51
  3. package/dist/chunk-3Z5ZWLXX.js.map +1 -0
  4. package/dist/{chunk-DOQEBGWB.js → chunk-5FWENSD2.js} +63 -8
  5. package/dist/chunk-5FWENSD2.js.map +1 -0
  6. package/dist/chunk-EVENAJYI.js +336 -0
  7. package/dist/chunk-EVENAJYI.js.map +1 -0
  8. package/dist/{chunk-DWLB7FRR.js → chunk-H5KTGI3A.js} +153 -7
  9. package/dist/chunk-H5KTGI3A.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 +1033 -178
  13. package/dist/cli.cjs.map +1 -1
  14. package/dist/cli.js +285 -243
  15. package/dist/cli.js.map +1 -1
  16. package/dist/index.cjs +614 -68
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +34 -18
  19. package/dist/index.d.ts +34 -18
  20. package/dist/index.js +4 -4
  21. package/dist/{pattern-F5xBtIE-.d.cts → pattern-CP9_HpVK.d.cts} +1 -1
  22. package/dist/{pattern-CV607P87.d.ts → pattern-CYgsv-jO.d.ts} +1 -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/{resolve-ops-DIwEelH-.d.ts → resolve-ops-Ci7LgYHC.d.cts} +9 -0
  28. package/dist/{resolve-ops-DIwEelH-.d.cts → resolve-ops-Ci7LgYHC.d.ts} +9 -0
  29. package/dist/verify.d.cts +1 -1
  30. package/dist/verify.d.ts +1 -1
  31. package/dist/verify.js +1 -1
  32. package/dist/webpack-loader.cjs +614 -68
  33. package/dist/webpack-loader.cjs.map +1 -1
  34. package/dist/webpack-loader.d.cts +2 -2
  35. package/dist/webpack-loader.d.ts +2 -2
  36. package/dist/webpack-loader.js +4 -4
  37. package/dist/worker.cjs +5955 -0
  38. package/dist/worker.cjs.map +1 -0
  39. package/dist/worker.d.cts +2 -0
  40. package/dist/worker.d.ts +2 -0
  41. package/dist/worker.js +72 -0
  42. package/dist/worker.js.map +1 -0
  43. package/package.json +4 -2
  44. package/dist/chunk-DNHOGPYV.js.map +0 -1
  45. package/dist/chunk-DOQEBGWB.js.map +0 -1
  46. 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-EVENAJYI.js";
8
+ import "./chunk-3Z5ZWLXX.js";
9
+ import "./chunk-H5KTGI3A.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,
@@ -187,7 +190,9 @@ function parseInvocation(argv) {
187
190
  "no-interactive": { type: "boolean", default: false },
188
191
  yes: { type: "boolean", short: "y", default: false },
189
192
  safety: { type: "string" },
190
- "project-root": { type: "string" }
193
+ "project-root": { type: "string" },
194
+ "max-memory": { type: "string" },
195
+ concurrency: { type: "string" }
191
196
  }
192
197
  });
193
198
  const provider = values.provider ?? DEFAULT_PROVIDER;
@@ -206,7 +211,9 @@ function parseInvocation(argv) {
206
211
  interactive: values["no-interactive"] !== true && values.yes !== true,
207
212
  passes: null,
208
213
  safety: toSafety(values.safety),
209
- projectRoot: values["project-root"] ?? null
214
+ projectRoot: values["project-root"] ?? null,
215
+ maxMemory: toPositiveInt(values["max-memory"], "--max-memory"),
216
+ concurrency: toPositiveInt(values.concurrency, "--concurrency")
210
217
  };
211
218
  }
212
219
  function shouldPrompt(options, isTty) {
@@ -230,200 +237,203 @@ var USAGE = [
230
237
  " --dangerously-overwrite-source overwrite source in place (needs a clean git tree)",
231
238
  " --no-git-check skip the clean-git-tree gate",
232
239
  " --safety <0|1|2|3> optimization aggressiveness (default: 2)",
240
+ " --max-memory <MB> memory budget; caps pool RAM AND parallelism (default: ~70% free RAM)",
241
+ " --concurrency <N> max parallel workers (still clamped by --max-memory)",
233
242
  " --yes, --no-interactive never launch the wizard (CI-safe)",
234
243
  "",
244
+ "Many files are processed across CPU cores by a memory-bounded worker pool; small jobs run inline.",
235
245
  "With no paths in an interactive terminal, a guided wizard launches."
236
246
  ].join("\n");
237
247
 
238
- // ../cli/src/safety.ts
248
+ // ../cli/src/pool.ts
239
249
  init_esm_shims();
240
- import { execFileSync } from "child_process";
250
+ import { existsSync } from "fs";
251
+ import * as os from "os";
241
252
  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));
253
+ import { fileURLToPath } from "url";
254
+ import { Worker } from "worker_threads";
255
+ var PER_WORKER_MB = 160;
256
+ var MIN_OLD_GEN_MB = 64;
257
+ function emptyTotals() {
258
+ return { files: 0, changed: 0, nodesRemoved: 0, classesSaved: 0, bytesSaved: 0 };
245
259
  }
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 };
260
+ function addStats(totals, stats, changed) {
261
+ totals.files += 1;
262
+ if (changed) totals.changed += 1;
263
+ totals.nodesRemoved += stats.nodesRemoved;
264
+ totals.classesSaved += stats.classesSaved;
265
+ totals.bytesSaved += stats.bytesSaved;
287
266
  }
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 });
267
+ function clamp(n, lo, hi) {
268
+ return Math.max(lo, Math.min(hi, n));
296
269
  }
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;
270
+ function computeWorkerCount(options) {
271
+ const cpus2 = Math.max(1, os.cpus().length);
272
+ const freeMB = Math.floor(os.freemem() / (1024 * 1024));
273
+ const budgetMB = Math.max(PER_WORKER_MB, options.maxMemory ?? Math.floor(freeMB * 0.7));
274
+ const byMemory = Math.max(1, Math.floor(budgetMB / PER_WORKER_MB));
275
+ const target = options.concurrency ?? Math.max(1, cpus2 - 1);
276
+ const workers = clamp(Math.min(target, byMemory), 1, byMemory);
277
+ const perWorkerCapMB = Math.max(MIN_OLD_GEN_MB, Math.floor(budgetMB / workers));
278
+ return { workers, budgetMB, perWorkerCapMB };
313
279
  }
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));
280
+ function inlineThreshold(workers) {
281
+ return Math.max(4, 2 * workers);
318
282
  }
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;
283
+ function shouldUsePool(fileCount, plan) {
284
+ return plan.workers > 1 && fileCount > inlineThreshold(plan.workers);
325
285
  }
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;
286
+ function moduleDir() {
287
+ try {
288
+ return path.dirname(fileURLToPath(import.meta.url));
289
+ } catch {
290
+ return typeof __dirname !== "undefined" ? __dirname : process.cwd();
332
291
  }
333
- return total;
334
292
  }
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
- };
293
+ function resolveWorkerPath() {
294
+ const dir = moduleDir();
295
+ const candidates = [
296
+ path.join(dir, "worker.cjs"),
297
+ path.join(dir, "worker.js"),
298
+ path.join(dir, "..", "dist", "worker.cjs"),
299
+ path.join(dir, "..", "dist", "worker.js")
300
+ ];
301
+ for (const c of candidates) {
302
+ if (existsSync(c)) return c;
303
+ }
304
+ return candidates[0];
355
305
  }
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: () => {
306
+ function runPool(files, init, plan, onWrote) {
307
+ const workerPath = resolveWorkerPath();
308
+ const totals = emptyTotals();
309
+ const wrote = [];
310
+ const errors = [];
311
+ let failures = 0;
312
+ const budgetBytes = plan.budgetMB * 1024 * 1024;
313
+ let nextIndex = 0;
314
+ let completed = 0;
315
+ const total = files.length;
316
+ let respawns = 0;
317
+ const maxRespawns = total + plan.workers + 8;
318
+ return new Promise((resolve3) => {
319
+ const handles = /* @__PURE__ */ new Set();
320
+ const finishIfDone = () => {
321
+ if (completed < total) return;
322
+ for (const h2 of handles) {
323
+ if (!h2.dead) {
324
+ try {
325
+ h2.worker.postMessage({ type: "stop" });
326
+ } catch {
327
+ }
328
+ void h2.worker.terminate();
329
+ }
368
330
  }
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
331
+ handles.clear();
332
+ resolve3({ totals, failures, wrote, errors });
382
333
  };
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)
334
+ const recordFailure = (file, error) => {
335
+ failures += 1;
336
+ completed += 1;
337
+ errors.push({ path: file, error });
338
+ };
339
+ const dispatch = (h2) => {
340
+ if (h2.dead) return;
341
+ if (nextIndex >= total) {
342
+ h2.current = null;
343
+ try {
344
+ h2.worker.postMessage({ type: "stop" });
345
+ } catch {
346
+ }
347
+ return;
348
+ }
349
+ if (process.memoryUsage().rss > budgetBytes) {
350
+ setTimeout(() => dispatch(h2), 25);
351
+ return;
352
+ }
353
+ const file = files[nextIndex++];
354
+ h2.current = file;
355
+ try {
356
+ h2.worker.postMessage({ type: "file", path: file });
357
+ } catch {
358
+ onWorkerDown(h2);
411
359
  }
412
360
  };
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
- };
424
- }
425
- function builtinPatternNames() {
426
- return builtinPatterns.map((p2) => p2.name);
361
+ const onMessage = (h2, msg) => {
362
+ if (msg.type === "ready") {
363
+ h2.ready = true;
364
+ dispatch(h2);
365
+ return;
366
+ }
367
+ h2.current = null;
368
+ if (msg.ok) {
369
+ addStats(totals, msg.stats, msg.changed);
370
+ if (msg.wrote) {
371
+ wrote.push(msg.wrote);
372
+ onWrote?.(msg.wrote);
373
+ }
374
+ } else {
375
+ failures += 1;
376
+ errors.push({ path: msg.path, error: msg.error });
377
+ }
378
+ completed += 1;
379
+ if (completed >= total) {
380
+ finishIfDone();
381
+ return;
382
+ }
383
+ dispatch(h2);
384
+ };
385
+ const drainRemaining = (reason) => {
386
+ while (nextIndex < total) recordFailure(files[nextIndex++], reason);
387
+ finishIfDone();
388
+ };
389
+ const onWorkerDown = (h2) => {
390
+ if (h2.dead) return;
391
+ h2.dead = true;
392
+ handles.delete(h2);
393
+ const lost = h2.current;
394
+ h2.current = null;
395
+ if (lost !== null) recordFailure(lost, "worker crashed while processing this file");
396
+ void h2.worker.terminate();
397
+ if (completed >= total) {
398
+ finishIfDone();
399
+ return;
400
+ }
401
+ if (nextIndex >= total) {
402
+ if (handles.size === 0) finishIfDone();
403
+ return;
404
+ }
405
+ if (respawns < maxRespawns) {
406
+ respawns += 1;
407
+ spawn();
408
+ } else if (handles.size === 0) {
409
+ drainRemaining("worker pool exhausted its respawn budget (memory cap too small?)");
410
+ }
411
+ };
412
+ const spawn = () => {
413
+ let worker;
414
+ try {
415
+ worker = new Worker(workerPath, {
416
+ workerData: init,
417
+ resourceLimits: { maxOldGenerationSizeMb: plan.perWorkerCapMB }
418
+ });
419
+ } catch {
420
+ if (nextIndex < total) recordFailure(files[nextIndex++], "failed to spawn worker");
421
+ if (completed >= total) finishIfDone();
422
+ else if (handles.size === 0 && nextIndex < total) spawn();
423
+ return;
424
+ }
425
+ const h2 = { worker, current: null, ready: false, dead: false };
426
+ handles.add(h2);
427
+ worker.on("message", (m2) => onMessage(h2, m2));
428
+ worker.on("error", () => onWorkerDown(h2));
429
+ worker.on("exit", (code) => {
430
+ if (code !== 0) onWorkerDown(h2);
431
+ });
432
+ };
433
+ const initial = Math.min(plan.workers, total);
434
+ for (let i = 0; i < initial; i++) spawn();
435
+ if (initial === 0) finishIfDone();
436
+ });
427
437
  }
428
438
 
429
439
  // ../cli/src/walk.ts
@@ -451,8 +461,8 @@ function walkDir(dir, out) {
451
461
  if (entry.isDirectory()) {
452
462
  if (SKIP_DIRS.has(entry.name)) continue;
453
463
  walkDir(full, out);
454
- } else if (entry.isFile() && isSupported(entry.name)) {
455
- out.push(full);
464
+ } else if (entry.isFile()) {
465
+ if (isSupported(entry.name)) out.push(full);
456
466
  }
457
467
  }
458
468
  }
@@ -490,7 +500,8 @@ function discoverInputs(paths) {
490
500
  continue;
491
501
  }
492
502
  if (stat?.isFile()) {
493
- push(p2);
503
+ if (isSupported(p2)) push(p2);
504
+ else warnings.push(`unsupported file type, skipped: ${p2}`);
494
505
  continue;
495
506
  }
496
507
  if (hasGlobMagic(p2)) {
@@ -499,9 +510,10 @@ function discoverInputs(paths) {
499
510
  warnings.push(`glob not supported on this Node version, skipped: ${p2}`);
500
511
  continue;
501
512
  }
502
- const matches = glob(p2).filter(isSupported);
503
- if (matches.length === 0) warnings.push(`no files matched: ${p2}`);
504
- for (const m2 of matches) push(m2);
513
+ const matched = glob(p2);
514
+ const supported = matched.filter(isSupported);
515
+ if (supported.length === 0) warnings.push(`no .jsx/.tsx/.html files matched: ${p2}`);
516
+ for (const m2 of supported) push(m2);
505
517
  continue;
506
518
  }
507
519
  warnings.push(`no such file or directory: ${p2}`);
@@ -1115,15 +1127,17 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
1115
1127
  ]);
1116
1128
  var COMMON_INPUT_DIRS = ["src", "app", "components", "pages", "lib", "ui", "public"];
1117
1129
  var CSS_FILE_CAP = 200;
1130
+ var DEFAULT_CSS_DEPTH = 10;
1118
1131
  function toRelative(root, abs) {
1119
1132
  const rel = path3.relative(root, abs);
1120
1133
  return rel.split(path3.sep).join("/");
1121
1134
  }
1122
- function detectCssFiles(root) {
1123
- const found = [];
1135
+ function detectCssFiles(root, scanRoots = [], maxDepth = DEFAULT_CSS_DEPTH) {
1136
+ const base = path3.resolve(root);
1137
+ const found = /* @__PURE__ */ new Map();
1124
1138
  let capped = false;
1125
- const walk = (dir) => {
1126
- if (capped) return;
1139
+ const walk = (dir, depth) => {
1140
+ if (capped || depth > maxDepth) return;
1127
1141
  let entries;
1128
1142
  try {
1129
1143
  entries = fs2.readdirSync(dir, { withFileTypes: true });
@@ -1134,23 +1148,31 @@ function detectCssFiles(root) {
1134
1148
  const full = path3.join(dir, entry.name);
1135
1149
  if (entry.isDirectory()) {
1136
1150
  if (SKIP_DIRS2.has(entry.name)) continue;
1137
- walk(full);
1151
+ walk(full, depth + 1);
1138
1152
  if (capped) return;
1139
1153
  } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".css")) {
1140
- found.push(toRelative(root, full));
1141
- if (found.length >= CSS_FILE_CAP) {
1142
- capped = true;
1143
- return;
1154
+ const abs = path3.resolve(full);
1155
+ if (!found.has(abs)) {
1156
+ found.set(abs, toRelative(base, abs));
1157
+ if (found.size >= CSS_FILE_CAP) {
1158
+ capped = true;
1159
+ return;
1160
+ }
1144
1161
  }
1145
1162
  }
1146
1163
  }
1147
1164
  };
1148
- walk(path3.resolve(root));
1149
- found.sort((a, b3) => a.localeCompare(b3));
1165
+ walk(base, 0);
1166
+ for (const r2 of scanRoots) {
1167
+ if (capped) break;
1168
+ const abs = path3.resolve(r2);
1169
+ if (abs !== base) walk(abs, 0);
1170
+ }
1171
+ const list = [...found.values()].sort((a, b3) => a.localeCompare(b3));
1150
1172
  if (capped) {
1151
1173
  console.error(`domflax: more than ${CSS_FILE_CAP} CSS files found; showing the first ${CSS_FILE_CAP}.`);
1152
1174
  }
1153
- return found;
1175
+ return list;
1154
1176
  }
1155
1177
  function detectInputDirs(root) {
1156
1178
  const resolved = path3.resolve(root);
@@ -1232,15 +1254,6 @@ async function runWizard(base) {
1232
1254
  } else if (outputMode === "overwrite") {
1233
1255
  dangerouslyOverwriteSource = true;
1234
1256
  }
1235
- const allPasses = builtinPatternNames();
1236
- const passSelection = await fe({
1237
- message: "Which optimization passes should run?",
1238
- options: allPasses.map((name) => ({ value: name, label: name })),
1239
- initialValues: [...allPasses],
1240
- required: true
1241
- });
1242
- if (cancelled(passSelection)) return done();
1243
- const passes = passSelection;
1244
1257
  const provider = await ve({
1245
1258
  message: "How should class names resolve to styles?",
1246
1259
  options: [
@@ -1253,7 +1266,7 @@ async function runWizard(base) {
1253
1266
  if (cancelled(provider)) return done();
1254
1267
  let css = base.css;
1255
1268
  if (provider === "custom") {
1256
- const detectedCss = detectCssFiles(root);
1269
+ const detectedCss = detectCssFiles(root, [inputPath]);
1257
1270
  if (detectedCss.length > 0) {
1258
1271
  const picked = await fe({
1259
1272
  message: "Which CSS files should resolve your classes? (all detected files are pre-selected)",
@@ -1281,7 +1294,7 @@ async function runWizard(base) {
1281
1294
  css,
1282
1295
  dryRun,
1283
1296
  dangerouslyOverwriteSource,
1284
- passes: passes.length === allPasses.length ? null : passes,
1297
+ passes: null,
1285
1298
  safety: base.safety ?? DEFAULT_SAFETY
1286
1299
  };
1287
1300
  function done() {
@@ -1291,13 +1304,6 @@ async function runWizard(base) {
1291
1304
  }
1292
1305
 
1293
1306
  // ../cli/src/index.ts
1294
- function addStats(totals, stats, changed) {
1295
- totals.files += 1;
1296
- if (changed) totals.changed += 1;
1297
- totals.nodesRemoved += stats.nodesRemoved;
1298
- totals.classesSaved += stats.classesSaved;
1299
- totals.bytesSaved += stats.bytesSaved;
1300
- }
1301
1307
  function printReport(totals) {
1302
1308
  console.log("");
1303
1309
  console.log("domflax report");
@@ -1307,23 +1313,8 @@ function printReport(totals) {
1307
1313
  console.log(` classes saved : ${totals.classesSaved}`);
1308
1314
  console.log(` bytes saved : ${totals.bytesSaved}`);
1309
1315
  }
1310
- async function execute(options) {
1311
- const { files, inputRoot, warnings } = discoverInputs(options.paths);
1312
- for (const w2 of warnings) console.error(`domflax: ${w2}`);
1313
- if (files.length === 0) {
1314
- console.error("domflax: no .jsx/.tsx/.html files found for the given paths");
1315
- return { exitCode: 1 };
1316
- }
1317
- const projectRoot = options.projectRoot ?? process.cwd();
1318
- const gitClean = options.dangerouslyOverwriteSource && !options.noGitCheck ? isGitClean(projectRoot) : true;
1319
- const planned = planWrites(options, gitClean);
1320
- if (!planned.ok) {
1321
- console.error(`domflax: ${planned.error}`);
1322
- return { exitCode: 1 };
1323
- }
1324
- const plan = planned.value;
1316
+ function runInline(files, options, inputRoot, plan, totals) {
1325
1317
  const transform = createTransform(options);
1326
- const totals = { files: 0, changed: 0, nodesRemoved: 0, classesSaved: 0, bytesSaved: 0 };
1327
1318
  let failures = 0;
1328
1319
  for (const file of files) {
1329
1320
  let code;
@@ -1358,8 +1349,59 @@ async function execute(options) {
1358
1349
  failures += 1;
1359
1350
  }
1360
1351
  }
1352
+ return failures;
1353
+ }
1354
+ async function execute(options) {
1355
+ const { files, inputRoot, warnings } = discoverInputs(options.paths);
1356
+ for (const w2 of warnings) console.error(`domflax: ${w2}`);
1357
+ if (files.length === 0) {
1358
+ console.error("domflax: no .jsx/.tsx files found for the given paths");
1359
+ return { exitCode: 1 };
1360
+ }
1361
+ const projectRoot = options.projectRoot ?? process.cwd();
1362
+ const gitClean = options.dangerouslyOverwriteSource && !options.noGitCheck ? isGitClean(projectRoot) : true;
1363
+ const planned = planWrites(options, gitClean);
1364
+ if (!planned.ok) {
1365
+ console.error(`domflax: ${planned.error}`);
1366
+ return { exitCode: 1 };
1367
+ }
1368
+ const plan = planned.value;
1369
+ const poolPlan = computeWorkerCount(options);
1370
+ const usePool = !options.dryRun && shouldUsePool(files.length, poolPlan);
1371
+ const totals = emptyTotals();
1372
+ let failures = 0;
1373
+ if (usePool) {
1374
+ const outcome = await runPool(
1375
+ files,
1376
+ { options, inputRoot, plan },
1377
+ poolPlan
1378
+ // Per-file "wrote" lines are collected and printed in deterministic (sorted) order below.
1379
+ );
1380
+ Object.assign(totals, outcome.totals);
1381
+ failures = outcome.failures;
1382
+ for (const { path: p2, error } of outcome.errors) {
1383
+ console.error(`domflax: failed ${path4.relative(process.cwd(), p2) || p2}: ${error}`);
1384
+ }
1385
+ for (const dest of [...outcome.wrote].sort()) {
1386
+ console.log(`domflax: wrote ${path4.relative(process.cwd(), dest) || dest}`);
1387
+ }
1388
+ } else {
1389
+ failures += runInline(files, options, inputRoot, plan, totals);
1390
+ }
1391
+ if (options.dryRun) {
1392
+ console.log("\ndomflax: dry run \u2014 no files were written.");
1393
+ } else if (totals.changed === 0) {
1394
+ console.log(
1395
+ `
1396
+ domflax: processed ${totals.files} file${totals.files === 1 ? "" : "s"} \u2014 nothing to optimize (0 changed).`
1397
+ );
1398
+ } else {
1399
+ console.log(
1400
+ `
1401
+ domflax: optimized ${totals.changed} of ${totals.files} file${totals.files === 1 ? "" : "s"} (${totals.nodesRemoved} nodes removed, ${totals.classesSaved} classes saved, ${totals.bytesSaved} bytes saved).`
1402
+ );
1403
+ }
1361
1404
  if (options.report) printReport(totals);
1362
- if (options.dryRun) console.log("\ndomflax: dry run \u2014 no files were written.");
1363
1405
  return { exitCode: failures > 0 ? 1 : 0 };
1364
1406
  }
1365
1407
  async function main(argv = process.argv.slice(2)) {