@codefast/cli 0.3.16-canary.2 → 0.4.0-canary.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 (41) hide show
  1. package/README.md +254 -86
  2. package/dist/arrange/analyze.mjs +2 -2
  3. package/dist/arrange/command.mjs +51 -27
  4. package/dist/arrange/domain/analyze-service.mjs +2 -2
  5. package/dist/arrange/domain/ast/collectors-cn.mjs +1 -1
  6. package/dist/arrange/domain/ast/collectors-tv.mjs +2 -2
  7. package/dist/arrange/domain/ast/helpers.mjs +2 -2
  8. package/dist/arrange/domain/ast/simplify-targets.mjs +109 -0
  9. package/dist/arrange/domain/ast/targets.mjs +25 -10
  10. package/dist/arrange/domain/grouping-service.mjs +1 -1
  11. package/dist/arrange/domain/grouping.mjs +5 -5
  12. package/dist/arrange/domain/imports.mjs +50 -2
  13. package/dist/arrange/domain/token-classifier.mjs +12 -12
  14. package/dist/arrange/output.mjs +11 -3
  15. package/dist/arrange/process-file.mjs +1 -1
  16. package/dist/arrange/scan-target.mjs +10 -1
  17. package/dist/arrange/simplify-process-file.mjs +35 -0
  18. package/dist/arrange/simplify-sync.mjs +32 -0
  19. package/dist/arrange/suggest.mjs +1 -1
  20. package/dist/arrange/sync.mjs +1 -1
  21. package/dist/arrange/workspace.mjs +1 -1
  22. package/dist/cli.mjs +1 -1
  23. package/dist/core/cli/result-handle.mjs +1 -1
  24. package/dist/core/config/schema.mjs +31 -10
  25. package/dist/core/source-text-edit.mjs +1 -1
  26. package/dist/core/workspace/resolver.mjs +3 -3
  27. package/dist/mirror/cli-result.mjs +2 -1
  28. package/dist/mirror/cli-schema.mjs +2 -1
  29. package/dist/mirror/command.mjs +12 -11
  30. package/dist/mirror/domain/exports.mjs +16 -19
  31. package/dist/mirror/output.mjs +3 -0
  32. package/dist/mirror/supplement-exports.mjs +114 -0
  33. package/dist/mirror/sync-reporter.mjs +5 -2
  34. package/dist/mirror/sync-workspace-package.mjs +35 -29
  35. package/dist/mirror/sync.mjs +2 -1
  36. package/dist/mirror/write-exports.mjs +3 -2
  37. package/dist/tag/command.mjs +6 -6
  38. package/dist/tag/output.mjs +1 -1
  39. package/dist/tag/sync.mjs +1 -1
  40. package/dist/tag/target-runner.mjs +1 -1
  41. package/package.json +10 -10
@@ -1,8 +1,8 @@
1
- import "../constants.mjs";
2
- import { tokenizeClassString } from "../tailwind-token.mjs";
3
1
  import { forEachDomainChild, isDomainArrayLiteralExpression, isDomainCallExpression, isDomainObjectLiteralExpression, isDomainPropertyAssignment, isDomainSpreadElement, isDomainTailwindClassLiteral } from "./ast-node.mjs";
2
+ import "../constants.mjs";
4
3
  import { CN_APPLY_LITERAL_WALK_OPTS, collectUnconditionalTailwindLiteralsFromCnArguments, forEachStringLiteralInClassExpression, isUnsafeLiteralForCnStyleApplySplit } from "./collectors-cn.mjs";
5
4
  import { buildKnownCnTvBindings, isCnOrTvIdentifier, propertyAssignmentNameText } from "./helpers.mjs";
5
+ import { tokenizeClassString } from "../tailwind-token.mjs";
6
6
  //#region src/arrange/domain/ast/collectors-tv.ts
7
7
  /**
8
8
  * @since 0.3.16-canary.0
@@ -1,5 +1,5 @@
1
- import { EMPTY_CN_TV_BINDINGS } from "../constants.mjs";
2
1
  import { isDomainIdentifier, isDomainImportDeclaration, isDomainNamedImports, isDomainNamespaceImport, isDomainPropertyAccessExpression, isDomainStringLiteral, lineOfSourcePosition } from "./ast-node.mjs";
2
+ import { EMPTY_CN_TV_BINDINGS } from "../constants.mjs";
3
3
  import { applyEditsDescending, indentOfLineContaining } from "../../../core/source-text-edit.mjs";
4
4
  //#region src/arrange/domain/ast/helpers.ts
5
5
  /**
@@ -21,7 +21,7 @@ const KNOWN_CN_TV_MODULES = new Set([
21
21
  function moduleLooksLikeCnTvReexport(moduleSpecifier) {
22
22
  const norm = moduleSpecifier.replace(/\\/g, "/");
23
23
  if (/(?:^|[./])utils(?:\/|$)/.test(norm)) return true;
24
- if (/\/utils\//.test(norm) || /\/utils$/.test(norm)) return true;
24
+ if (/\/utils\//.test(norm) || norm.endsWith("/utils")) return true;
25
25
  if (/(?:^|\/)cn\.tsx?$/.test(norm)) return true;
26
26
  return false;
27
27
  }
@@ -0,0 +1,109 @@
1
+ import { forEachDomainChild, isDomainArrayLiteralExpression, isDomainCallExpression, isDomainJsxAttribute, isDomainJsxExpression, isDomainObjectLiteralExpression, isDomainPropertyAssignment, isDomainSpreadElement, isDomainTailwindClassLiteral } from "./ast-node.mjs";
2
+ import "../constants.mjs";
3
+ import { indentOfLineContaining } from "../../../core/source-text-edit.mjs";
4
+ import { buildKnownCnTvBindings, isCnOrTvIdentifier } from "./helpers.mjs";
5
+ import { escapeTsStringLiteralContent } from "../source-text-formatters.mjs";
6
+ //#region src/arrange/domain/ast/simplify-targets.ts
7
+ function toFlatString(text) {
8
+ return `"${escapeTsStringLiteralContent(text.trim())}"`;
9
+ }
10
+ function joinLiterals(args) {
11
+ return args.filter(isDomainTailwindClassLiteral).map((lit) => lit.text).join(" ");
12
+ }
13
+ function isAllStaticLiterals(args) {
14
+ return args.length > 0 && args.every(isDomainTailwindClassLiteral);
15
+ }
16
+ /**
17
+ * For a cn() call that has both static and dynamic args:
18
+ * - Merge ALL static string literals (regardless of position) into one string.
19
+ * - Place the merged string first.
20
+ * - Append all dynamic args in their original relative order.
21
+ *
22
+ * Returns `null` when nothing changes (0 statics, or already 1 static at arg[0]).
23
+ */
24
+ function buildMixedCnReplacement(call, sourceText) {
25
+ const args = [...call.arguments];
26
+ if (args.length === 0) return null;
27
+ const staticTexts = [];
28
+ const dynamicSrcs = [];
29
+ for (const arg of args) if (isDomainTailwindClassLiteral(arg)) staticTexts.push(arg.text);
30
+ else dynamicSrcs.push(sourceText.slice(arg.pos, arg.end));
31
+ if (staticTexts.length === 0) return null;
32
+ const firstArg = args[0];
33
+ if (staticTexts.length === 1 && firstArg !== void 0 && isDomainTailwindClassLiteral(firstArg)) return null;
34
+ const flatStatic = staticTexts.join(" ").trim();
35
+ const baseIndent = indentOfLineContaining(sourceText, call.pos);
36
+ const argIndent = `${baseIndent} `;
37
+ return `cn(\n${[`${argIndent}"${escapeTsStringLiteralContent(flatStatic)}",`, ...dynamicSrcs.map((src) => `${argIndent}${src},`)].join("\n")}\n${baseIndent})`;
38
+ }
39
+ function collectTvArrayEdits(obj, results, depth) {
40
+ if (depth > 12) return;
41
+ for (const prop of obj.properties) {
42
+ if (!isDomainPropertyAssignment(prop)) continue;
43
+ const init = prop.initializer;
44
+ if (isDomainArrayLiteralExpression(init)) {
45
+ const elements = [...init.elements];
46
+ if (!elements.some(isDomainSpreadElement) && elements.length > 0 && isAllStaticLiterals(elements)) results.push({
47
+ start: init.pos,
48
+ end: init.end,
49
+ replacement: toFlatString(joinLiterals(elements)),
50
+ label: "tv-array"
51
+ });
52
+ } else if (isDomainObjectLiteralExpression(init)) collectTvArrayEdits(init, results, depth + 1);
53
+ }
54
+ }
55
+ /**
56
+ * Collect all simplify edits for a source file:
57
+ * - Arrays of pure string literals inside tv() slots → flat string
58
+ * - cn() calls whose every argument is a static string literal → flat string (unwrap cn)
59
+ * - cn() calls with mixed static + dynamic args → merge adjacent statics into one string
60
+ * - JSX className={cn(...all-static...)} → className="flat string"
61
+ *
62
+ * @since 0.3.16-canary.0
63
+ */
64
+ function collectSimplifyTargets(sourceFile) {
65
+ const sourceText = sourceFile.text;
66
+ const results = [];
67
+ const knownBindings = buildKnownCnTvBindings(sourceFile);
68
+ const seenCnPos = /* @__PURE__ */ new Set();
69
+ const visitNode = (node) => {
70
+ if (isDomainCallExpression(node)) {
71
+ if (isCnOrTvIdentifier(node.expression, "tv", knownBindings)) {
72
+ const arg0 = node.arguments[0];
73
+ if (arg0 && isDomainObjectLiteralExpression(arg0)) collectTvArrayEdits(arg0, results, 0);
74
+ } else if (isCnOrTvIdentifier(node.expression, "cn", knownBindings) && !seenCnPos.has(node.pos)) {
75
+ seenCnPos.add(node.pos);
76
+ const args = [...node.arguments];
77
+ if (isAllStaticLiterals(args)) {
78
+ const flat = joinLiterals(args);
79
+ const parent = node.parent;
80
+ if (parent && isDomainJsxExpression(parent) && parent.parent && isDomainJsxAttribute(parent.parent)) results.push({
81
+ start: parent.pos,
82
+ end: parent.end,
83
+ replacement: toFlatString(flat),
84
+ label: "jsx-cn"
85
+ });
86
+ else results.push({
87
+ start: node.pos,
88
+ end: node.end,
89
+ replacement: toFlatString(flat),
90
+ label: "cn-static"
91
+ });
92
+ } else {
93
+ const replacement = buildMixedCnReplacement(node, sourceText);
94
+ if (replacement !== null) results.push({
95
+ start: node.pos,
96
+ end: node.end,
97
+ replacement,
98
+ label: "cn-merge"
99
+ });
100
+ }
101
+ }
102
+ }
103
+ forEachDomainChild(node, visitNode);
104
+ };
105
+ for (const stmt of sourceFile.statements) visitNode(stmt);
106
+ return results;
107
+ }
108
+ //#endregion
109
+ export { collectSimplifyTargets };
@@ -1,12 +1,12 @@
1
+ import { forEachDomainChild, isDomainArrayLiteralExpression, isDomainJsxAttribute, isDomainPropertyAssignment, isDomainTailwindClassLiteral } from "./ast-node.mjs";
1
2
  import "../constants.mjs";
2
- import { tokenizeClassString } from "../tailwind-token.mjs";
3
- import { forEachDomainChild, isDomainArrayLiteralExpression, isDomainJsxAttribute, isDomainTailwindClassLiteral } from "./ast-node.mjs";
4
3
  import { isUnsafeLiteralForCnStyleApplySplit } from "./collectors-cn.mjs";
5
4
  import { jsxClassNameStaticLiteral } from "./collectors-jsx.mjs";
6
5
  import { endAfterOptionalCommaFollowingInSource, indentOfLineContaining, textPrefixFromLineStartToPosition } from "../../../core/source-text-edit.mjs";
6
+ import { tokenizeClassString } from "../tailwind-token.mjs";
7
7
  import { collectGroupableStringNodes, slotClassString } from "./collectors-tv.mjs";
8
- import { areCnTailwindPartitionsEquivalent, suggestCnGroups, summarizeGroupBucketLabels } from "../grouping.mjs";
9
8
  import { escapeTsStringLiteralContent, formatArray, formatArrayElementsAsSiblingLines, formatJsxCnAttributeValue } from "../source-text-formatters.mjs";
9
+ import { areCnTailwindPartitionsEquivalent, suggestCnGroups, summarizeGroupBucketLabels } from "../grouping.mjs";
10
10
  //#region src/arrange/domain/ast/targets.ts
11
11
  /**
12
12
  * @since 0.3.16-canary.0
@@ -62,7 +62,7 @@ function formatCnCallReplacement(stringNode, sourceText, withClassName) {
62
62
  for (const dynamicArgumentSource of dynamicArgTexts) allArgs.push(`${argIndent}${dynamicArgumentSource}`);
63
63
  if (withClassName) allArgs.push(`${argIndent}className`);
64
64
  const commaAfterLastArg = allArgs.length > 1;
65
- return `cn(\n${allArgs.map((argLine, lineIndex) => lineIndex < allArgs.length - 1 || commaAfterLastArg ? `${argLine},` : `${argLine}`).join("\n")}\n${baseIndent})`;
65
+ return `cn(\n${allArgs.map((argLine, lineIndex) => lineIndex < allArgs.length - 1 || commaAfterLastArg ? `${argLine},` : argLine).join("\n")}\n${baseIndent})`;
66
66
  }
67
67
  /**
68
68
  * @since 0.3.16-canary.0
@@ -88,14 +88,29 @@ function planGroupEditForTarget(target, textAfterUnwrap, withClassName) {
88
88
  if (areCnTailwindPartitionsEquivalent(target.item.nodes.map((classLiteral) => classLiteral.text), groups)) return;
89
89
  if (!target.item.cnCall) {
90
90
  const anchorClassLiteral = target.item.primaryClassLiteral;
91
- const parentArray = target.item.nodes.length > 1 && anchorClassLiteral.parent !== null && isDomainArrayLiteralExpression(anchorClassLiteral.parent) ? anchorClassLiteral.parent : null;
92
- const start = parentArray ? parentArray.pos : anchorClassLiteral.pos;
93
- const end = parentArray ? parentArray.end : endAfterOptionalCommaFollowingInSource(textAfterUnwrap, anchorClassLiteral.end);
94
- const baseIndent = indentOfLineContaining(textAfterUnwrap, start);
91
+ const parentNode = anchorClassLiteral.parent;
92
+ const parentArray = target.item.nodes.length > 1 && parentNode !== null && isDomainArrayLiteralExpression(parentNode) ? parentNode : null;
93
+ const bareStringProperty = !parentArray && parentNode !== null && isDomainPropertyAssignment(parentNode);
94
+ if (parentArray || bareStringProperty) {
95
+ const start = parentArray ? parentArray.pos : anchorClassLiteral.pos;
96
+ const end = parentArray ? parentArray.end : anchorClassLiteral.end;
97
+ const baseIndent = indentOfLineContaining(textAfterUnwrap, start);
98
+ return {
99
+ start,
100
+ end,
101
+ replacement: formatArray(groups).split("\n").map((line, lineIndex) => lineIndex === 0 ? line : `${baseIndent}${line}`).join("\n"),
102
+ bucketSummary: summarizeGroupBucketLabels(groups),
103
+ jsxCn: false,
104
+ lineSf: target.item.sf,
105
+ reportNode: anchorClassLiteral,
106
+ label: target.item.isTvContext ? "tv" : "cn"
107
+ };
108
+ }
109
+ const start = anchorClassLiteral.pos;
95
110
  return {
96
111
  start,
97
- end,
98
- replacement: parentArray ? formatArray(groups).split("\n").map((line, lineIndex) => lineIndex === 0 ? line : `${baseIndent}${line}`).join("\n") : formatArrayElementsAsSiblingLines(groups, textPrefixFromLineStartToPosition(textAfterUnwrap, start)),
112
+ end: endAfterOptionalCommaFollowingInSource(textAfterUnwrap, anchorClassLiteral.end),
113
+ replacement: formatArrayElementsAsSiblingLines(groups, textPrefixFromLineStartToPosition(textAfterUnwrap, start)),
99
114
  bucketSummary: summarizeGroupBucketLabels(groups),
100
115
  jsxCn: false,
101
116
  lineSf: target.item.sf,
@@ -42,7 +42,7 @@ function tryBuildGroupFileWorkPlan(input) {
42
42
  if (domainSfGrouped.text !== unwrap.textAfterUnwrap) throw new Error("Domain invariant: domainSfGrouped.text must match unwrap phase textAfterUnwrap");
43
43
  const groupTargets = collectGroupTargets(domainSfGrouped, filePath);
44
44
  if (unwrap.cnInTvCalls.length === 0 && groupTargets.length === 0) return null;
45
- const sortedTargets = [...groupTargets].sort((leftTarget, rightTarget) => targetReplaceStart(rightTarget) - targetReplaceStart(leftTarget));
45
+ const sortedTargets = [...groupTargets].toSorted((leftTarget, rightTarget) => targetReplaceStart(rightTarget) - targetReplaceStart(leftTarget));
46
46
  const plannedGroupEdits = [];
47
47
  for (const groupTarget of sortedTargets) {
48
48
  const plan = planGroupEditForTarget(groupTarget, unwrap.textAfterUnwrap, withClassName);
@@ -72,7 +72,7 @@ function compareClassifiedTailwindTokensForCnGrouping(left, right, firstVariantK
72
72
  function areCnTailwindPartitionsEquivalent(staticLiteralTexts, suggestedGroups) {
73
73
  const partitionSignatures = (chunks) => chunks.map((chunk) => {
74
74
  const classTokens = tokenizeClassString(chunk);
75
- return classTokens.length === 0 ? "" : [...classTokens].sort(compareClassTokensCanonically).join(" ");
75
+ return classTokens.length === 0 ? "" : [...classTokens].toSorted(compareClassTokensCanonically).join(" ");
76
76
  }).filter((s) => s.length > 0).sort(compareClassTokensCanonically);
77
77
  const staticPartitionSigs = partitionSignatures(staticLiteralTexts);
78
78
  const suggestedPartitionSigs = partitionSignatures(suggestedGroups);
@@ -230,7 +230,7 @@ function mergeEaseTimingIntoFollowingAnimatedState(groups) {
230
230
  const chunkJ = result[j];
231
231
  if (chunkJ === void 0) throw new Error("invariant: mergeEase following chunk missing");
232
232
  if (dominantBucketOfGroup(chunkJ) !== "state") continue;
233
- if (!tokenizeClassString(chunkJ).some((classToken) => /^animate/.test(stripVariants(classToken)))) continue;
233
+ if (!tokenizeClassString(chunkJ).some((classToken) => stripVariants(classToken).startsWith("animate"))) continue;
234
234
  target = j;
235
235
  break;
236
236
  }
@@ -248,7 +248,7 @@ function mergeEaseTimingIntoFollowingAnimatedState(groups) {
248
248
  function chunkIsOnlyEaseTimingMotion(groupStr) {
249
249
  const classTokens = tokenizeClassString(groupStr);
250
250
  if (classTokens.length === 0) return false;
251
- return classTokens.every((classToken) => classifyToken(classToken) === "motion" && /^ease-/.test(stripVariants(classToken)));
251
+ return classTokens.every((classToken) => classifyToken(classToken) === "motion" && stripVariants(classToken).startsWith("ease-"));
252
252
  }
253
253
  /**
254
254
  * @since 0.3.16-canary.0
@@ -328,9 +328,9 @@ function suggestCnGroups(classString) {
328
328
  function summarizeGroupBucketLabels(groups) {
329
329
  return groups.map((g) => {
330
330
  const uniq = new Set(tokenizeClassString(g).map(classifyToken));
331
- if (uniq.size !== 1) return `mixed:${[...uniq].sort(compareClassTokensCanonically).join("+")}`;
331
+ if (uniq.size !== 1) return `mixed:${[...uniq].toSorted(compareClassTokensCanonically).join("+")}`;
332
332
  const onlyBucket = [...uniq][0];
333
- if (onlyBucket === void 0) return `mixed:${[...uniq].sort(compareClassTokensCanonically).join("+")}`;
333
+ if (onlyBucket === void 0) return `mixed:${[...uniq].toSorted(compareClassTokensCanonically).join("+")}`;
334
334
  return onlyBucket;
335
335
  });
336
336
  }
@@ -1,4 +1,5 @@
1
- import { isDomainImportDeclaration, isDomainNamedImports, isDomainStringLiteral } from "./ast/ast-node.mjs";
1
+ import { forEachDomainChild, isDomainIdentifier, isDomainImportDeclaration, isDomainNamedImports, isDomainStringLiteral } from "./ast/ast-node.mjs";
2
+ import { endAfterOptionalCommaFollowingInSource } from "../../core/source-text-edit.mjs";
2
3
  //#region src/arrange/domain/imports.ts
3
4
  function sourceFileImportsCn(sourceFile) {
4
5
  for (const statement of sourceFile.statements) {
@@ -25,6 +26,53 @@ function findImportDeclarationFromModule(sourceFile, moduleSpecifier) {
25
26
  if (isDomainStringLiteral(spec) && spec.text === moduleSpecifier) return statement;
26
27
  }
27
28
  }
29
+ function cnIsUsedInBody(node) {
30
+ if (isDomainIdentifier(node) && node.text === "cn") return true;
31
+ let found = false;
32
+ forEachDomainChild(node, (child) => {
33
+ if (!found) found = cnIsUsedInBody(child);
34
+ });
35
+ return found;
36
+ }
37
+ function removeCnSpecifier(sourceText, specifier, allSpecifiers) {
38
+ const idx = allSpecifiers.indexOf(specifier);
39
+ if (idx === 0) {
40
+ const next = allSpecifiers[1];
41
+ if (!next) return sourceText;
42
+ return sourceText.slice(0, specifier.pos) + sourceText.slice(next.pos);
43
+ }
44
+ const prev = allSpecifiers[idx - 1];
45
+ if (!prev) return sourceText;
46
+ const endWithTrailingComma = endAfterOptionalCommaFollowingInSource(sourceText, specifier.end);
47
+ return sourceText.slice(0, prev.end) + sourceText.slice(endWithTrailingComma);
48
+ }
49
+ /**
50
+ * After simplify rewrites, drop the `cn` import specifier when `cn` is no
51
+ * longer referenced anywhere outside the import declarations.
52
+ *
53
+ * @since 0.3.16-canary.0
54
+ */
55
+ function dropCnImportIfUnused(sourceFile) {
56
+ const sourceText = sourceFile.text;
57
+ for (const stmt of sourceFile.statements) {
58
+ if (isDomainImportDeclaration(stmt)) continue;
59
+ if (cnIsUsedInBody(stmt)) return sourceText;
60
+ }
61
+ for (const stmt of sourceFile.statements) {
62
+ if (!isDomainImportDeclaration(stmt) || !stmt.importClause) continue;
63
+ const clause = stmt.importClause;
64
+ if (!clause.namedBindings || !isDomainNamedImports(clause.namedBindings)) continue;
65
+ const elements = clause.namedBindings.elements;
66
+ const cnSpecifier = elements.find((el) => el.name.text === "cn");
67
+ if (!cnSpecifier) continue;
68
+ if (elements.length === 1) {
69
+ const lineEnd = sourceText.indexOf("\n", stmt.end);
70
+ return sourceText.slice(0, stmt.pos) + (lineEnd !== -1 ? sourceText.slice(lineEnd + 1) : sourceText.slice(stmt.end));
71
+ }
72
+ return removeCnSpecifier(sourceText, cnSpecifier, elements);
73
+ }
74
+ return sourceText;
75
+ }
28
76
  /**
29
77
  * @since 0.3.16-canary.0
30
78
  */
@@ -56,4 +104,4 @@ function ensureCnImport(sourceFile, cnImportOverride) {
56
104
  return `${sourceText.slice(0, insertAt)}${importLine}\n${sourceText.slice(insertAt)}`;
57
105
  }
58
106
  //#endregion
59
- export { ensureCnImport };
107
+ export { dropCnImportIfUnused, ensureCnImport };
@@ -50,9 +50,9 @@ function isStateToken(token) {
50
50
  if (prefix.startsWith("supports-[")) return true;
51
51
  if (prefix.startsWith("in-data-")) return true;
52
52
  if (/^data-(?!\[)/.test(prefix)) return true;
53
- if (/^aria-/.test(prefix)) return true;
53
+ if (prefix.startsWith("aria-")) return true;
54
54
  if (/^(group|peer)-/.test(prefix)) return true;
55
- if (/^not-/.test(prefix)) return true;
55
+ if (prefix.startsWith("not-")) return true;
56
56
  if (isCompoundOrMediaVariantPrefix(prefix)) return true;
57
57
  if (prefix.startsWith("[") && prefix.endsWith("]")) return true;
58
58
  return false;
@@ -65,13 +65,13 @@ function isStateToken(token) {
65
65
  */
66
66
  function compositeSecondaryOrder(bareUtility) {
67
67
  const b = bareUtility;
68
- if (/^opacity(?:-|$)/.test(b) || /^mix-blend-/.test(b) || /^isolation(?:-|$)/.test(b) || b === "isolate") return 0;
68
+ if (/^opacity(?:-|$)/.test(b) || b.startsWith("mix-blend-") || /^isolation(?:-|$)/.test(b) || b === "isolate") return 0;
69
69
  if (b === "transform-3d" || /^perspective(?:-|$)/.test(b)) return 10;
70
70
  if (/^rotate-[xyz](?:-|$)/.test(b) || /^translate-z(?:-|$)/.test(b) || /^scale-z(?:-|$)/.test(b)) return 20;
71
71
  if (/^translate(?:-|$)/.test(b) || /^scale(?:-|$)/.test(b) || /^rotate(?:-|$)/.test(b) || /^skew(?:-|$)/.test(b)) return 30;
72
72
  if (/^transform(?:-(?:gpu|cpu|none))?$/.test(b)) return 35;
73
- if (/^blur(?:-|$)/.test(b) || /^backdrop-/.test(b) || /^filter(?:-|$)/.test(b) || /^brightness-/.test(b) || /^contrast-/.test(b) || /^grayscale(?:-|$)/.test(b) || /^hue-rotate-/.test(b) || /^invert(?:-|$)/.test(b) || /^saturate-/.test(b) || /^sepia(?:-|$)/.test(b)) return 40;
74
- if (/^will-change/.test(b)) return 50;
73
+ if (/^blur(?:-|$)/.test(b) || b.startsWith("backdrop-") || /^filter(?:-|$)/.test(b) || b.startsWith("brightness-") || b.startsWith("contrast-") || /^grayscale(?:-|$)/.test(b) || b.startsWith("hue-rotate-") || /^invert(?:-|$)/.test(b) || b.startsWith("saturate-") || /^sepia(?:-|$)/.test(b)) return 40;
74
+ if (b.startsWith("will-change")) return 50;
75
75
  return 99;
76
76
  }
77
77
  /**
@@ -82,7 +82,7 @@ function classifyBareUtility(bareUtility) {
82
82
  if (/^@container(?:\/[a-z][a-z0-9-]*)?$/i.test(b)) return "existence";
83
83
  if (/^(?:hidden|contents|sr-only|not-sr-only|list-item|flow-root)$/.test(b) || /^(?:block|inline-block|inline)$/.test(b)) return "existence";
84
84
  if (/^table(?:$|-)/.test(b)) return "existence";
85
- if (/^contain-/.test(b)) return "existence";
85
+ if (b.startsWith("contain-")) return "existence";
86
86
  if (/^(?:group|peer)(?:\/[a-z][a-z0-9-]*)?$/.test(b)) return "existence";
87
87
  if (/^(?:static|relative|absolute|fixed|sticky)$/.test(b)) return "position";
88
88
  if (/^-?(?:inset|top|right|bottom|left|start|end)(?:-|\/|$)/.test(b) || /^-?(?:inset-x|inset-y|inset-s|inset-e)-/.test(b)) return "position";
@@ -90,13 +90,13 @@ function classifyBareUtility(bareUtility) {
90
90
  if (/^(?:flex|inline-flex|grid|inline-grid|subgrid|masonry)(?:$|-)/.test(b) || /^(?:items|justify|justify-items|justify-self|content|self|place|place-content|place-items|place-self)-/.test(b) || /^-?(?:gap|space-[xy]|col-|row-|grid-|auto-cols|auto-rows|order-|order$)/.test(b) || /^-?(?:auto-flow|grid-flow)-/.test(b) || /^(?:columns-|break-after|break-before|break-inside)-/.test(b) || /^(?:float|clear)-/.test(b) || /^(?:wrap-|flex-wrap|flex-nowrap|flex-row|flex-col|flex-auto|flex-initial|flex-none|flex-1|flex-\[)/.test(b) || /^container$/.test(b)) return "layout";
91
91
  if (/^-?(?:w|h|min-w|max-w|min-h|max-h|size|aspect|shrink|grow|basis)-/.test(b) || /^(?:shrink|grow)(?:$|-)/.test(b) || /^-?overflow-/.test(b) || /^-?object-/.test(b)) return "sizing";
92
92
  if (/^-?(?:p|px|py|pt|pb|pl|pr|ps|pe|m|mx|my|mt|mb|ml|mr|ms|me)-/.test(b)) return "spacing";
93
- if (/^-?(?:rounded|border|ring|divide|inset-ring)(?:-|\/|$)/.test(b) || /^(?:rounded|border|ring|divide|inset-ring)$/.test(b) || /^ring-offset-/.test(b)) return "shape";
93
+ if (/^-?(?:rounded|border|ring|divide|inset-ring)(?:-|\/|$)/.test(b) || /^(?:rounded|border|ring|divide|inset-ring)$/.test(b) || b.startsWith("ring-offset-")) return "shape";
94
94
  if (/^-?bg-/.test(b) || /^(?:from|via|to)(?:-|\/|$)/.test(b) || /^bg-(?:linear|radial|conic)-/.test(b) || /^-?mask-/.test(b) || /^-?(?:fill|stroke)-/.test(b)) return "background";
95
95
  if (/^-?(?:shadow|inset-shadow|drop-shadow|text-shadow)(?:-|\/|$)/.test(b) || /^(?:shadow|inset-shadow)$/.test(b) || /^-?outline(?:-|\/|$)/.test(b) || /^outline$/.test(b)) return "shadow";
96
- if (/^-?(?:font|leading|tracking|list|indent|align|decoration|underline-offset|text-wrap|text-balance|text-pretty)-/.test(b) || /^-?(?:text-(?!shadow))/.test(b) || /^-?(?:whitespace|break|line-clamp|hyphens)-/.test(b) || /^(?:antialiased|subpixel-antialiased|italic|not-italic|overline|line-through|underline|no-underline|uppercase|lowercase|capitalize|normal-case|truncate|text-wrap|text-balance|text-pretty)$/.test(b) || /^text-wrap-/.test(b) || /^tabular-nums$|^slashed-zero$|^lining-nums$|^oldstyle-nums$|^proportional-nums$/.test(b)) return "typography";
97
- if (/^opacity(?:-|$)/.test(b) || /^mix-blend-/.test(b) || /^isolation(?:-|$)/.test(b) || b === "isolate" || /^transform(?:-(?:gpu|cpu|none))?$/.test(b) || /^-?(?:translate|scale|rotate|skew)-/.test(b) || /^perspective(?:-|$)/.test(b) || b === "transform-3d" || /^blur(?:-|$)/.test(b) || /^backdrop-/.test(b) || /^filter(?:-|$)/.test(b) || /^brightness-/.test(b) || /^contrast-/.test(b) || /^grayscale(?:-|$)/.test(b) || /^hue-rotate-/.test(b) || /^invert(?:-|$)/.test(b) || /^saturate-/.test(b) || /^sepia(?:-|$)/.test(b) || /^will-change/.test(b)) return "composite";
98
- if (/^(?:transition|duration|ease|delay|animate)(?:-|$)/.test(b) || /^ease-/.test(b)) return "motion";
99
- if (/^(?:cursor|pointer-events|select|touch|scroll|scroll-m|scroll-p|overscroll|snap|resize|field-sizing|appearance|scheme|color-scheme|accent|caret)(?:-|$)/.test(b) || /^(?:cursor|select|resize)$/.test(b) || /^scroll-behavior/.test(b) || /^scroll-snap/.test(b) || /^touch-action/.test(b) || b === "inert") return "behavior";
96
+ if (/^-?(?:font|leading|tracking|list|indent|align|decoration|underline-offset|text-wrap|text-balance|text-pretty)-/.test(b) || /^-?(?:text-(?!shadow))/.test(b) || /^-?(?:whitespace|break|line-clamp|hyphens)-/.test(b) || /^(?:antialiased|subpixel-antialiased|italic|not-italic|overline|line-through|underline|no-underline|uppercase|lowercase|capitalize|normal-case|truncate|text-wrap|text-balance|text-pretty)$/.test(b) || b.startsWith("text-wrap-") || /^tabular-nums$|^slashed-zero$|^lining-nums$|^oldstyle-nums$|^proportional-nums$/.test(b)) return "typography";
97
+ if (/^opacity(?:-|$)/.test(b) || b.startsWith("mix-blend-") || /^isolation(?:-|$)/.test(b) || b === "isolate" || /^transform(?:-(?:gpu|cpu|none))?$/.test(b) || /^-?(?:translate|scale|rotate|skew)-/.test(b) || /^perspective(?:-|$)/.test(b) || b === "transform-3d" || /^blur(?:-|$)/.test(b) || b.startsWith("backdrop-") || /^filter(?:-|$)/.test(b) || b.startsWith("brightness-") || b.startsWith("contrast-") || /^grayscale(?:-|$)/.test(b) || b.startsWith("hue-rotate-") || /^invert(?:-|$)/.test(b) || b.startsWith("saturate-") || /^sepia(?:-|$)/.test(b) || b.startsWith("will-change")) return "composite";
98
+ if (/^(?:transition|duration|ease|delay|animate)(?:-|$)/.test(b) || b.startsWith("ease-")) return "motion";
99
+ if (/^(?:cursor|pointer-events|select|touch|scroll|scroll-m|scroll-p|overscroll|snap|resize|field-sizing|appearance|scheme|color-scheme|accent|caret)(?:-|$)/.test(b) || /^(?:cursor|select|resize)$/.test(b) || b.startsWith("scroll-behavior") || b.startsWith("scroll-snap") || b.startsWith("touch-action") || b === "inert") return "behavior";
100
100
  return "other";
101
101
  }
102
102
  /**
@@ -106,7 +106,7 @@ function classifyBareUtility(bareUtility) {
106
106
  * Pure arbitrary **properties** (`[--x]:`, `[color:red]`) have no `&` in the leading `[…]` segment.
107
107
  */
108
108
  function isArbitraryParentSelectorStateToken(token) {
109
- if (/^\[&/.test(token)) return true;
109
+ if (token.startsWith("[&")) return true;
110
110
  if (!token.startsWith("[")) return false;
111
111
  const colonIdx = indexOfFirstVariantColon(token);
112
112
  if (colonIdx === -1 || colonIdx === 0 || token[colonIdx - 1] !== "]") return false;
@@ -1,6 +1,6 @@
1
- import { logger } from "../core/logger.mjs";
2
1
  import "./domain/constants.mjs";
3
2
  import { lineOf } from "./domain/ast/helpers.mjs";
3
+ import { logger } from "../core/logger.mjs";
4
4
  import path from "node:path";
5
5
  //#region src/arrange/output.ts
6
6
  /**
@@ -30,13 +30,21 @@ function printAnalyzeReport(resolvedTargetPath, report) {
30
30
  function printSyncResult(result, write) {
31
31
  logger.out(`\nTotal: ${result.filePaths.length} file(s), ${result.totalFound} site(s) (cn/tv/JSX className) to review.`);
32
32
  if (write) logger.out(`Applied: ${result.totalChanged} site(s) updated.`);
33
- else logger.out(`(Run "apply" to write changes, or "pnpm cli:arrange-apply" / "pnpm exec codefast arrange apply")`);
33
+ else logger.out(`(Re-run without --dry-run to write changes, or "pnpm cli:arrange" / "pnpm exec codefast arrange")`);
34
34
  if (write ? result.totalChanged > 0 : result.totalFound > 0) logger.out("Note: class order may change across concern groups — smoke-test the UI if you rely on cascade order.");
35
35
  if (result.hookError !== null) logger.err(result.hookError);
36
36
  }
37
37
  /**
38
38
  * @since 0.3.16-canary.0
39
39
  */
40
+ function printSimplifyResult(result, write) {
41
+ logger.out(`\nTotal: ${result.filePaths.length} file(s), ${result.totalFound} site(s) to simplify.`);
42
+ if (write) logger.out(`Applied: ${result.totalChanged} site(s) updated.`);
43
+ else logger.out(`(Run "simplify" to write changes, or "pnpm cli:arrange-simplify" / "pnpm exec codefast arrange simplify")`);
44
+ }
45
+ /**
46
+ * @since 0.3.16-canary.0
47
+ */
40
48
  function printGroupFilePreviewFromWork(work) {
41
49
  printGroupFilePreviewBody({
42
50
  filePath: work.filePath,
@@ -76,4 +84,4 @@ function printGroupFilePreviewBody(args) {
76
84
  }
77
85
  }
78
86
  //#endregion
79
- export { printAnalyzeReport, printGroupFilePreviewFromWork, printSyncResult };
87
+ export { printAnalyzeReport, printGroupFilePreviewFromWork, printSimplifyResult, printSyncResult };
@@ -1,6 +1,6 @@
1
1
  import { parseDomainSourceFile } from "./source-parse.mjs";
2
- import { buildGroupFileUnwrapState, countPersistedGroupFileEdits, groupFileDryRunNoEdits, groupFileEditsTouchJsxCn, groupFilePreviewTotals, groupFileWorkHasNothingToReport, mergeGroupFileBodyText, tryBuildGroupFileWorkPlan } from "./domain/grouping-service.mjs";
3
2
  import { ensureCnImport } from "./domain/imports.mjs";
3
+ import { buildGroupFileUnwrapState, countPersistedGroupFileEdits, groupFileDryRunNoEdits, groupFileEditsTouchJsxCn, groupFilePreviewTotals, groupFileWorkHasNothingToReport, mergeGroupFileBodyText, tryBuildGroupFileWorkPlan } from "./domain/grouping-service.mjs";
4
4
  //#region src/arrange/process-file.ts
5
5
  /**
6
6
  * @since 0.3.16-canary.0
@@ -3,13 +3,22 @@ import { logger } from "../core/logger.mjs";
3
3
  import { walkTsxFiles } from "../core/workspace/typescript-walk.mjs";
4
4
  import path from "node:path";
5
5
  //#region src/arrange/scan-target.ts
6
+ const TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
7
+ /**
8
+ * Test/spec files are excluded from directory scans: arrange rewrites class strings,
9
+ * and a `cn(...)` inside an assertion (e.g. `expect(cn("a", "b"))`) is intentional test
10
+ * data, not styling to reformat. An explicit single-file target is still honored.
11
+ */
12
+ function isTestFile(filePath) {
13
+ return TEST_FILE_PATTERN.test(path.basename(filePath));
14
+ }
6
15
  /**
7
16
  * @since 0.3.16-canary.0
8
17
  */
9
18
  function scanArrangeTargets(fs, targetPath) {
10
19
  const resolvedTargetPath = path.resolve(targetPath);
11
20
  if (fs.statSync(resolvedTargetPath).isDirectory()) try {
12
- return walkTsxFiles(resolvedTargetPath, fs);
21
+ return walkTsxFiles(resolvedTargetPath, fs).filter((filePath) => !isTestFile(filePath));
13
22
  } catch (caughtError) {
14
23
  logger.err(`[file-walker] ${messageFrom(caughtError)}`);
15
24
  throw caughtError;
@@ -0,0 +1,35 @@
1
+ import { applyEditsDescending } from "../core/source-text-edit.mjs";
2
+ import { parseDomainSourceFile } from "./source-parse.mjs";
3
+ import { collectSimplifyTargets } from "./domain/ast/simplify-targets.mjs";
4
+ import { dropCnImportIfUnused } from "./domain/imports.mjs";
5
+ //#region src/arrange/simplify-process-file.ts
6
+ /**
7
+ * @since 0.3.16-canary.0
8
+ */
9
+ function processArrangeSimplifyFile(fs, args) {
10
+ const { filePath, write } = args;
11
+ const sourceText = fs.readFileSync(filePath, "utf8");
12
+ const meaningful = collectSimplifyTargets(parseDomainSourceFile(filePath, sourceText)).filter((edit) => sourceText.slice(edit.start, edit.end) !== edit.replacement);
13
+ const textAfterEdits = meaningful.length > 0 ? applyEditsDescending(sourceText, meaningful) : sourceText;
14
+ const textAfterImportDrop = dropCnImportIfUnused(parseDomainSourceFile(filePath, textAfterEdits));
15
+ const importDropped = textAfterImportDrop !== textAfterEdits;
16
+ const totalFound = meaningful.length + (importDropped ? 1 : 0);
17
+ if (totalFound === 0) return {
18
+ filePath,
19
+ totalFound: 0,
20
+ changed: 0
21
+ };
22
+ if (!write) return {
23
+ filePath,
24
+ totalFound,
25
+ changed: 0
26
+ };
27
+ fs.writeFileSync(filePath, textAfterImportDrop, "utf8");
28
+ return {
29
+ filePath,
30
+ totalFound,
31
+ changed: totalFound
32
+ };
33
+ }
34
+ //#endregion
35
+ export { processArrangeSimplifyFile };
@@ -0,0 +1,32 @@
1
+ import { scanArrangeTargets } from "./scan-target.mjs";
2
+ import { ok } from "../core/result.mjs";
3
+ import { processArrangeSimplifyFile } from "./simplify-process-file.mjs";
4
+ //#region src/arrange/simplify-sync.ts
5
+ /**
6
+ * @since 0.3.16-canary.0
7
+ */
8
+ async function runArrangeSimplify(fs, args) {
9
+ const filePaths = scanArrangeTargets(fs, args.targetPath);
10
+ const modifiedFiles = [];
11
+ let totalFound = 0;
12
+ let totalChanged = 0;
13
+ for (const filePath of filePaths) {
14
+ const result = processArrangeSimplifyFile(fs, {
15
+ filePath,
16
+ write: args.write
17
+ });
18
+ totalFound += result.totalFound;
19
+ totalChanged += result.changed;
20
+ if (result.changed > 0) modifiedFiles.push(result.filePath);
21
+ }
22
+ return ok({
23
+ filePaths,
24
+ modifiedFiles,
25
+ totalFound,
26
+ totalChanged,
27
+ hookError: null,
28
+ previewPlans: []
29
+ });
30
+ }
31
+ //#endregion
32
+ export { runArrangeSimplify };
@@ -1,5 +1,5 @@
1
- import { suggestCnGroups, summarizeGroupBucketLabels } from "./domain/grouping.mjs";
2
1
  import { formatArray, formatCnCall } from "./domain/source-text-formatters.mjs";
2
+ import { suggestCnGroups, summarizeGroupBucketLabels } from "./domain/grouping.mjs";
3
3
  //#region src/arrange/suggest.ts
4
4
  /**
5
5
  * @since 0.3.16-canary.0
@@ -1,6 +1,6 @@
1
1
  import { messageFrom } from "../core/errors.mjs";
2
- import { ok } from "../core/result.mjs";
3
2
  import { scanArrangeTargets } from "./scan-target.mjs";
3
+ import { ok } from "../core/result.mjs";
4
4
  import { processArrangeGroupFile } from "./process-file.mjs";
5
5
  //#region src/arrange/sync.ts
6
6
  async function runOnAfterWriteHook(hook, modifiedFiles) {
@@ -1,8 +1,8 @@
1
1
  import { AppError, messageFrom } from "../core/errors.mjs";
2
2
  import { err, ok } from "../core/result.mjs";
3
+ import { resolveArrangeTargetPath } from "./resolve-target.mjs";
3
4
  import { loadCodefastConfig } from "../core/config.mjs";
4
5
  import { findRepoRoot } from "../core/workspace/resolver.mjs";
5
- import { resolveArrangeTargetPath } from "./resolve-target.mjs";
6
6
  //#region src/arrange/workspace.ts
7
7
  /**
8
8
  * @since 0.3.16-canary.0
package/dist/cli.mjs CHANGED
@@ -22,7 +22,7 @@ function readVersion() {
22
22
  */
23
23
  async function runCli(argv) {
24
24
  const program = new Command();
25
- program.name("codefast").description("Codefast monorepo developer CLI").version(readVersion()).option("--no-color", "Disable ANSI color output").configureHelp({ sortSubcommands: true }).showHelpAfterError("(use --help for usage)");
25
+ program.name("codefast").description("Codefast monorepo developer CLI").version(readVersion()).option("--no-color", "Disable ANSI color output").enablePositionalOptions().configureHelp({ sortSubcommands: true }).showHelpAfterError("(use --help for usage)");
26
26
  program.addCommand(createArrangeCommand());
27
27
  program.addCommand(createMirrorCommand());
28
28
  program.addCommand(createTagCommand());
@@ -1,6 +1,6 @@
1
+ import { logger } from "../logger.mjs";
1
2
  import { formatAppError } from "./format-error.mjs";
2
3
  import "../exit-codes.mjs";
3
- import { logger } from "../logger.mjs";
4
4
  import { isVerboseCliDiagnostics } from "../verbose-diagnostics.mjs";
5
5
  import process from "node:process";
6
6
  //#region src/core/cli/result-handle.ts
@@ -1,20 +1,41 @@
1
1
  import { z } from "zod";
2
2
  //#region src/core/config/schema.ts
3
3
  const afterWriteHookSchema = z.custom((value) => typeof value === "function", { message: "Expected a function" });
4
+ const mirrorCssConfigSchema = z.union([z.boolean(), z.object({
5
+ enabled: z.boolean().optional(),
6
+ customExports: z.record(z.string(), z.string()).optional(),
7
+ forceExportFiles: z.boolean().optional()
8
+ }).strict()]);
4
9
  /**
10
+ * Per-package mirror configuration. Setting a package to `false` skips it entirely.
11
+ *
12
+ * - `source` — include a `source` condition pointing to the original `.ts` file (default `true`).
13
+ * Pass a string to override the root-export source path explicitly.
14
+ * - `types` — include the `types` condition when `.d.ts` files exist (default `true`).
15
+ * - `import` — include the `import` condition (default `true`).
16
+ * - `css` — CSS export configuration (wildcard or per-file).
17
+ *
5
18
  * @since 0.3.16-canary.0
6
19
  */
7
- const mirrorConfigSchema = z.object({
8
- skipPackages: z.array(z.string()).optional(),
9
- pathTransformations: z.record(z.string(), z.object({ removePrefix: z.string().optional() }).strict()).optional(),
10
- customExports: z.record(z.string(), z.record(z.string(), z.string())).optional(),
11
- cssExports: z.record(z.string(), z.union([z.boolean(), z.object({
12
- enabled: z.boolean().optional(),
13
- customExports: z.record(z.string(), z.string()).optional(),
14
- forceExportFiles: z.boolean().optional()
15
- }).strict()])).optional()
20
+ const mirrorPackageConfigSchema = z.object({
21
+ /** Preserve the existing `package.json#exports` map and only add missing conditions
22
+ * (`source`, `types`, `import`). No dist/ scan is performed. */
23
+ preserve: z.boolean().optional(),
24
+ strip: z.string().optional(),
25
+ exports: z.record(z.string(), z.string()).optional(),
26
+ source: z.union([z.boolean(), z.string()]).default(true),
27
+ types: z.boolean().default(true),
28
+ import: z.boolean().default(true),
29
+ css: mirrorCssConfigSchema.optional()
16
30
  }).strict();
17
31
  /**
32
+ * Mirror config: a record keyed by package name. Set a package to `false` to skip it;
33
+ * omit it entirely to process it with default settings.
34
+ *
35
+ * @since 0.3.16-canary.0
36
+ */
37
+ const mirrorConfigSchema = z.record(z.string(), z.union([z.literal(false), mirrorPackageConfigSchema]));
38
+ /**
18
39
  * @since 0.3.16-canary.0
19
40
  */
20
41
  const codefastTagConfigSchema = z.object({
@@ -36,4 +57,4 @@ const codefastConfigRootSchema = z.object({
36
57
  arrange: codefastArrangeConfigSchema.optional()
37
58
  }).strict();
38
59
  //#endregion
39
- export { codefastArrangeConfigSchema, codefastConfigRootSchema, codefastTagConfigSchema, mirrorConfigSchema };
60
+ export { codefastArrangeConfigSchema, codefastConfigRootSchema, codefastTagConfigSchema, mirrorConfigSchema, mirrorPackageConfigSchema };