@codefast/cli 0.10.0 → 0.11.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 (220) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +16 -2
  3. package/dist/arrange/command.js +96 -132
  4. package/dist/arrange/domain/ast/simplify-targets.js +18 -23
  5. package/dist/arrange/{typescript-ast-translator.d.ts → domain/ast/translator.d.ts} +5 -4
  6. package/dist/arrange/{typescript-ast-translator.js → domain/ast/translator.js} +19 -22
  7. package/dist/arrange/group/cli-result.d.ts +7 -0
  8. package/dist/arrange/group/cli-result.js +12 -0
  9. package/dist/arrange/group/cli-schema.d.ts +17 -0
  10. package/dist/arrange/group/cli-schema.js +13 -0
  11. package/dist/arrange/group/output.d.ts +7 -0
  12. package/dist/arrange/group/output.js +10 -0
  13. package/dist/arrange/{suggest.d.ts → group/suggest.d.ts} +1 -1
  14. package/dist/arrange/inspect/cli-result.d.ts +7 -0
  15. package/dist/arrange/inspect/cli-result.js +8 -0
  16. package/dist/arrange/inspect/cli-schema.d.ts +15 -0
  17. package/dist/arrange/inspect/cli-schema.js +9 -0
  18. package/dist/arrange/inspect/output.d.ts +7 -0
  19. package/dist/arrange/inspect/output.js +42 -0
  20. package/dist/arrange/{analyze.d.ts → inspect/run.d.ts} +2 -2
  21. package/dist/arrange/{analyze.js → inspect/run.js} +2 -2
  22. package/dist/arrange/{workspace.d.ts → prepare.d.ts} +2 -2
  23. package/dist/arrange/{workspace.js → prepare.js} +5 -8
  24. package/dist/arrange/regroup/cli-result.d.ts +13 -0
  25. package/dist/arrange/regroup/cli-result.js +23 -0
  26. package/dist/arrange/regroup/cli-schema.d.ts +20 -0
  27. package/dist/arrange/regroup/cli-schema.js +14 -0
  28. package/dist/arrange/regroup/output.d.ts +14 -0
  29. package/dist/arrange/regroup/output.js +72 -0
  30. package/dist/arrange/{process-file.d.ts → regroup/process-file.d.ts} +2 -2
  31. package/dist/arrange/{sync.d.ts → regroup/run.d.ts} +3 -3
  32. package/dist/arrange/{sync.js → regroup/run.js} +2 -2
  33. package/dist/arrange/resolve-target.d.ts +2 -2
  34. package/dist/arrange/scan-target.d.ts +2 -2
  35. package/dist/arrange/simplify/cli-result.d.ts +7 -0
  36. package/dist/arrange/simplify/cli-result.js +8 -0
  37. package/dist/arrange/simplify/cli-schema.d.ts +17 -0
  38. package/dist/arrange/simplify/cli-schema.js +11 -0
  39. package/dist/arrange/simplify/fold-targets.d.ts +13 -0
  40. package/dist/arrange/simplify/fold-targets.js +139 -0
  41. package/dist/arrange/simplify/output.d.ts +7 -0
  42. package/dist/arrange/simplify/output.js +15 -0
  43. package/dist/arrange/{simplify-process-file.d.ts → simplify/process-file.d.ts} +4 -2
  44. package/dist/arrange/simplify/process-file.js +49 -0
  45. package/dist/arrange/{simplify-sync.d.ts → simplify/run.d.ts} +3 -2
  46. package/dist/arrange/simplify/run.js +38 -0
  47. package/dist/arrange/simplify/variant-classname-probe.d.ts +35 -0
  48. package/dist/arrange/simplify/variant-classname-probe.js +95 -0
  49. package/dist/arrange/source-parse.js +1 -1
  50. package/dist/audit/command.js +134 -211
  51. package/dist/audit/comments/cli-result.d.ts +13 -0
  52. package/dist/audit/comments/cli-result.js +22 -0
  53. package/dist/audit/comments/cli-schema.d.ts +19 -0
  54. package/dist/audit/comments/cli-schema.js +13 -0
  55. package/dist/audit/comments/output.d.ts +7 -0
  56. package/dist/audit/comments/output.js +27 -0
  57. package/dist/audit/comments/prepare.d.ts +16 -0
  58. package/dist/audit/comments/prepare.js +12 -0
  59. package/dist/audit/{run-comments.d.ts → comments/run.d.ts} +2 -2
  60. package/dist/audit/{run-comments.js → comments/run.js} +5 -5
  61. package/dist/audit/display-names/cli-result.d.ts +13 -0
  62. package/dist/audit/display-names/cli-result.js +22 -0
  63. package/dist/audit/display-names/cli-schema.d.ts +18 -0
  64. package/dist/audit/display-names/cli-schema.js +12 -0
  65. package/dist/audit/display-names/output.d.ts +7 -0
  66. package/dist/audit/display-names/output.js +21 -0
  67. package/dist/audit/display-names/prepare.d.ts +16 -0
  68. package/dist/audit/display-names/prepare.js +12 -0
  69. package/dist/audit/{run-display-names.d.ts → display-names/run.d.ts} +2 -2
  70. package/dist/audit/{run-display-names.js → display-names/run.js} +1 -1
  71. package/dist/audit/imports/cli-result.d.ts +13 -0
  72. package/dist/audit/imports/cli-result.js +22 -0
  73. package/dist/audit/imports/cli-schema.d.ts +18 -0
  74. package/dist/audit/imports/cli-schema.js +12 -0
  75. package/dist/audit/{domain → imports/domain}/import-policy.d.ts +2 -2
  76. package/dist/audit/{domain → imports/domain}/import-policy.js +4 -11
  77. package/dist/audit/imports/output.d.ts +7 -0
  78. package/dist/audit/imports/output.js +21 -0
  79. package/dist/audit/imports/prepare.d.ts +16 -0
  80. package/dist/audit/imports/prepare.js +12 -0
  81. package/dist/audit/{run-imports.d.ts → imports/run.d.ts} +2 -2
  82. package/dist/audit/{run-imports.js → imports/run.js} +2 -2
  83. package/dist/audit/links/cli-result.d.ts +13 -0
  84. package/dist/audit/links/cli-result.js +22 -0
  85. package/dist/audit/links/cli-schema.d.ts +18 -0
  86. package/dist/audit/links/cli-schema.js +12 -0
  87. package/dist/audit/links/output.d.ts +7 -0
  88. package/dist/audit/links/output.js +21 -0
  89. package/dist/audit/links/prepare.d.ts +16 -0
  90. package/dist/audit/links/prepare.js +12 -0
  91. package/dist/audit/{run-links.d.ts → links/run.d.ts} +2 -2
  92. package/dist/audit/{run-links.js → links/run.js} +1 -1
  93. package/dist/audit/prepare.d.ts +11 -50
  94. package/dist/audit/prepare.js +9 -132
  95. package/dist/audit/rtl/cli-result.d.ts +13 -0
  96. package/dist/audit/rtl/cli-result.js +22 -0
  97. package/dist/audit/rtl/cli-schema.d.ts +18 -0
  98. package/dist/audit/rtl/cli-schema.js +12 -0
  99. package/dist/audit/{domain → rtl/domain}/audit-file.js +2 -2
  100. package/dist/audit/rtl/output.d.ts +7 -0
  101. package/dist/audit/rtl/output.js +21 -0
  102. package/dist/audit/rtl/prepare.d.ts +13 -0
  103. package/dist/audit/rtl/prepare.js +41 -0
  104. package/dist/audit/{run.d.ts → rtl/run.d.ts} +2 -2
  105. package/dist/audit/{run.js → rtl/run.js} +1 -1
  106. package/dist/core/cli/command-pipeline.d.ts +91 -0
  107. package/dist/core/cli/command-pipeline.js +83 -0
  108. package/dist/core/cli/global-options.d.ts +1 -1
  109. package/dist/core/cli/global-options.js +1 -1
  110. package/dist/core/cli/resolve-root.d.ts +9 -0
  111. package/dist/core/cli/resolve-root.js +16 -0
  112. package/dist/core/cli/result-handle.d.ts +1 -7
  113. package/dist/core/cli/result-handle.js +0 -13
  114. package/dist/core/config/loader.d.ts +2 -2
  115. package/dist/core/config/schema.d.ts +1 -1
  116. package/dist/core/config/schema.js +1 -1
  117. package/dist/core/config.d.ts +2 -2
  118. package/dist/core/filesystem/{port.d.ts → filesystem.d.ts} +1 -1
  119. package/dist/core/filesystem/node.d.ts +3 -3
  120. package/dist/core/filesystem/node.js +1 -1
  121. package/dist/core/source-text-edit.d.ts +14 -0
  122. package/dist/core/source-text-edit.js +22 -0
  123. package/dist/core/workspace/markdown-walk.d.ts +2 -2
  124. package/dist/core/workspace/package-version.d.ts +2 -2
  125. package/dist/core/workspace/resolver.d.ts +3 -3
  126. package/dist/core/workspace/source-walk.d.ts +2 -2
  127. package/dist/core/workspace/typescript-walk.d.ts +2 -2
  128. package/dist/core/workspace/walk-files.d.ts +2 -2
  129. package/dist/mirror/cli-schema.d.ts +2 -2
  130. package/dist/mirror/cli-schema.js +1 -1
  131. package/dist/mirror/command.js +31 -60
  132. package/dist/mirror/{dist-filesystem-impl.d.ts → dist-filesystem-node.d.ts} +3 -3
  133. package/dist/mirror/{dist-filesystem-impl.js → dist-filesystem-node.js} +1 -1
  134. package/dist/mirror/domain/dirent-guard.d.ts +1 -1
  135. package/dist/mirror/domain/types.d.ts +57 -0
  136. package/dist/mirror/output.d.ts +1 -3
  137. package/dist/mirror/output.js +126 -1
  138. package/dist/mirror/package-path.d.ts +3 -3
  139. package/dist/mirror/prepare.d.ts +3 -3
  140. package/dist/mirror/prepare.js +6 -10
  141. package/dist/mirror/run.d.ts +12 -0
  142. package/dist/mirror/supplement-exports.d.ts +3 -3
  143. package/dist/mirror/sync-workspace-package.d.ts +2 -2
  144. package/dist/mirror/sync-workspace-package.js +1 -1
  145. package/dist/mirror/write-exports.d.ts +2 -2
  146. package/dist/pack-slim/cli-schema.d.ts +1 -1
  147. package/dist/pack-slim/cli-schema.js +1 -1
  148. package/dist/pack-slim/command.js +31 -66
  149. package/dist/pack-slim/prepare.d.ts +21 -0
  150. package/dist/pack-slim/prepare.js +14 -0
  151. package/dist/pack-slim/{sync.d.ts → run.d.ts} +2 -2
  152. package/dist/tag/cli-result.d.ts +8 -2
  153. package/dist/tag/cli-result.js +14 -1
  154. package/dist/tag/cli-schema.d.ts +4 -4
  155. package/dist/tag/cli-schema.js +3 -4
  156. package/dist/tag/command.js +33 -60
  157. package/dist/tag/domain/skip-filter.d.ts +13 -0
  158. package/dist/tag/domain/skip-filter.js +29 -0
  159. package/dist/tag/domain/types.d.ts +21 -1
  160. package/dist/tag/domain/version-summary.d.ts +13 -0
  161. package/dist/tag/domain/version-summary.js +24 -0
  162. package/dist/tag/output.d.ts +4 -4
  163. package/dist/tag/output.js +6 -9
  164. package/dist/tag/prepare.d.ts +2 -2
  165. package/dist/tag/prepare.js +2 -2
  166. package/dist/tag/run.d.ts +10 -0
  167. package/dist/tag/{sync.js → run.js} +20 -50
  168. package/dist/tag/{target-candidates.d.ts → target/candidates.d.ts} +2 -2
  169. package/dist/tag/{target-candidates.js → target/candidates.js} +1 -1
  170. package/dist/tag/{resolve-target-path.d.ts → target/resolve-path.d.ts} +2 -2
  171. package/dist/tag/{target-runner.d.ts → target/runner.d.ts} +2 -2
  172. package/dist/tag/{target-runner.js → target/runner.js} +2 -2
  173. package/dist/tag/{since-writer.d.ts → writer/since-writer.d.ts} +2 -2
  174. package/dist/tag/{version-resolver.d.ts → writer/version-resolver.d.ts} +2 -2
  175. package/package.json +10 -2
  176. package/dist/arrange/cli-schema.d.ts +0 -50
  177. package/dist/arrange/cli-schema.js +0 -34
  178. package/dist/arrange/output.d.ts +0 -26
  179. package/dist/arrange/output.js +0 -127
  180. package/dist/arrange/simplify-process-file.js +0 -30
  181. package/dist/arrange/simplify-sync.js +0 -30
  182. package/dist/audit/cli-schema.d.ts +0 -93
  183. package/dist/audit/cli-schema.js +0 -66
  184. package/dist/audit/output.d.ts +0 -91
  185. package/dist/audit/output.js +0 -213
  186. package/dist/mirror/sync-reporter.d.ts +0 -60
  187. package/dist/mirror/sync-reporter.js +0 -128
  188. package/dist/mirror/sync-types.d.ts +0 -43
  189. package/dist/mirror/sync-types.js +0 -1
  190. package/dist/mirror/sync.d.ts +0 -12
  191. package/dist/tag/sync.d.ts +0 -42
  192. /package/dist/arrange/{suggest.js → group/suggest.js} +0 -0
  193. /package/dist/arrange/{domain → inspect/domain}/analyze-service.d.ts +0 -0
  194. /package/dist/arrange/{domain → inspect/domain}/analyze-service.js +0 -0
  195. /package/dist/arrange/{process-file.js → regroup/process-file.js} +0 -0
  196. /package/dist/audit/{domain → comments/domain}/comment-content.d.ts +0 -0
  197. /package/dist/audit/{domain → comments/domain}/comment-content.js +0 -0
  198. /package/dist/audit/{domain → comments/domain}/comment-dividers.d.ts +0 -0
  199. /package/dist/audit/{domain → comments/domain}/comment-dividers.js +0 -0
  200. /package/dist/audit/{domain → comments/domain}/link-references.d.ts +0 -0
  201. /package/dist/audit/{domain → comments/domain}/link-references.js +0 -0
  202. /package/dist/audit/{domain → comments/domain}/since-versions.d.ts +0 -0
  203. /package/dist/audit/{domain → comments/domain}/since-versions.js +0 -0
  204. /package/dist/audit/{domain → comments/domain}/tsdoc-syntax.d.ts +0 -0
  205. /package/dist/audit/{domain → comments/domain}/tsdoc-syntax.js +0 -0
  206. /package/dist/audit/{domain → display-names/domain}/display-names.d.ts +0 -0
  207. /package/dist/audit/{domain → display-names/domain}/display-names.js +0 -0
  208. /package/dist/audit/{domain → links/domain}/markdown-links.d.ts +0 -0
  209. /package/dist/audit/{domain → links/domain}/markdown-links.js +0 -0
  210. /package/dist/audit/{domain → rtl/domain}/audit-file.d.ts +0 -0
  211. /package/dist/audit/{domain → rtl/domain}/mappings.d.ts +0 -0
  212. /package/dist/audit/{domain → rtl/domain}/mappings.js +0 -0
  213. /package/dist/audit/{domain → rtl/domain}/tokenize.d.ts +0 -0
  214. /package/dist/audit/{domain → rtl/domain}/tokenize.js +0 -0
  215. /package/dist/core/filesystem/{port.js → filesystem.js} +0 -0
  216. /package/dist/mirror/{sync.js → run.js} +0 -0
  217. /package/dist/pack-slim/{sync.js → run.js} +0 -0
  218. /package/dist/tag/{resolve-target-path.js → target/resolve-path.js} +0 -0
  219. /package/dist/tag/{since-writer.js → writer/since-writer.js} +0 -0
  220. /package/dist/tag/{version-resolver.js → writer/version-resolver.js} +0 -0
@@ -0,0 +1,42 @@
1
+ import path from "node:path";
2
+ import { LONG_STRING_TOKEN_THRESHOLD, MAX_REPORT_LINES } from "#/arrange/domain/constants";
3
+ import { logger } from "#/core/logger";
4
+ /**
5
+ * Presents the human-readable summary of an `arrange inspect` report.
6
+ *
7
+ * @since 0.3.16-canary.0
8
+ */
9
+ export function presentAnalyzeReport(resolvedTargetPath, report) {
10
+ logger.out(`Path: ${path.resolve(resolvedTargetPath)}`);
11
+ logger.out(`.ts/.tsx files: ${report.files}`);
12
+ logger.out(`cn(...) call sites: ${report.cnCallExpressions}`);
13
+ logger.out(`tv(...) call sites: ${report.tvCallExpressions}`);
14
+ logger.out(`\nLong cn(...) string literals (≥${LONG_STRING_TOKEN_THRESHOLD} tokens — consider splitting arguments): ${report.longCnStringLiterals.length}`);
15
+ for (const findingEntry of report.longCnStringLiterals.slice(0, MAX_REPORT_LINES)) {
16
+ logger.out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.tokenCount} tokens) ${findingEntry.preview}`);
17
+ }
18
+ if (report.longCnStringLiterals.length > MAX_REPORT_LINES) {
19
+ logger.out(` … and ${report.longCnStringLiterals.length - MAX_REPORT_LINES} more`);
20
+ }
21
+ logger.out(`\nLong tv({...}) string literals in base/variants/… (≥${LONG_STRING_TOKEN_THRESHOLD} tokens): ${report.longTvStringLiterals.length}`);
22
+ for (const findingEntry of report.longTvStringLiterals.slice(0, MAX_REPORT_LINES)) {
23
+ logger.out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.tokenCount} tokens) ${findingEntry.preview}`);
24
+ }
25
+ if (report.longTvStringLiterals.length > MAX_REPORT_LINES) {
26
+ logger.out(` … and ${report.longTvStringLiterals.length - MAX_REPORT_LINES} more`);
27
+ }
28
+ logger.out(`\nJSX className="..." or className={'...'} (static strings, ≥${LONG_STRING_TOKEN_THRESHOLD} tokens): ${report.longJsxClassNameLiterals.length}`);
29
+ for (const findingEntry of report.longJsxClassNameLiterals.slice(0, MAX_REPORT_LINES)) {
30
+ logger.out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.tokenCount} tokens) ${findingEntry.preview}`);
31
+ }
32
+ if (report.longJsxClassNameLiterals.length > MAX_REPORT_LINES) {
33
+ logger.out(` … and ${report.longJsxClassNameLiterals.length - MAX_REPORT_LINES} more`);
34
+ }
35
+ logger.out(`\ncn(...) nested inside tv({...}) (prefer a string or array — preview/apply can rewrite): ${report.cnInsideTvCalls.length}`);
36
+ for (const findingEntry of report.cnInsideTvCalls.slice(0, MAX_REPORT_LINES)) {
37
+ logger.out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.argCount} args) ${findingEntry.preview}`);
38
+ }
39
+ if (report.cnInsideTvCalls.length > MAX_REPORT_LINES) {
40
+ logger.out(` … and ${report.cnInsideTvCalls.length - MAX_REPORT_LINES} more`);
41
+ }
42
+ }
@@ -1,10 +1,10 @@
1
1
  import type { AnalyzeReport } from "#/arrange/domain/types";
2
2
  import { AppError } from "#/core/errors";
3
- import type { FilesystemPort } from "#/core/filesystem/port";
3
+ import type { Filesystem } from "#/core/filesystem/filesystem";
4
4
  import type { Result } from "#/core/result";
5
5
  /**
6
6
  * Scans a directory's arrange targets and returns the accumulated analyze report.
7
7
  *
8
8
  * @since 0.3.16-canary.0
9
9
  */
10
- export declare function analyzeDirectory(fs: FilesystemPort, analyzeRootPath: string): Result<AnalyzeReport, AppError>;
10
+ export declare function runArrangeInspect(fs: Filesystem, analyzeRootPath: string): Result<AnalyzeReport, AppError>;
@@ -1,4 +1,4 @@
1
- import { accumulateAnalyzeReportForSourceFile, createEmptyAnalyzeReport } from "#/arrange/domain/analyze-service";
1
+ import { accumulateAnalyzeReportForSourceFile, createEmptyAnalyzeReport, } from "#/arrange/inspect/domain/analyze-service";
2
2
  import { scanArrangeTargets } from "#/arrange/scan-target";
3
3
  import { parseDomainSourceFile } from "#/arrange/source-parse";
4
4
  import { AppError } from "#/core/errors";
@@ -9,7 +9,7 @@ import { err, ok } from "#/core/result";
9
9
  *
10
10
  * @since 0.3.16-canary.0
11
11
  */
12
- export function analyzeDirectory(fs, analyzeRootPath) {
12
+ export function runArrangeInspect(fs, analyzeRootPath) {
13
13
  const report = createEmptyAnalyzeReport();
14
14
  try {
15
15
  const files = scanArrangeTargets(fs, analyzeRootPath);
@@ -1,13 +1,13 @@
1
1
  import type { ArrangeTargetWorkspaceAndConfig } from "#/arrange/domain/types";
2
2
  import { AppError } from "#/core/errors";
3
- import type { FilesystemPort } from "#/core/filesystem/port";
3
+ import type { Filesystem } from "#/core/filesystem/filesystem";
4
4
  import type { Result } from "#/core/result";
5
5
  /**
6
6
  * Resolves the arrange target, repo root, and loaded config an arrange run needs.
7
7
  *
8
8
  * @since 0.3.16-canary.0
9
9
  */
10
- export declare function prepareArrangeWorkspace(fs: FilesystemPort, args: {
10
+ export declare function prepareArrangeWorkspace(fs: Filesystem, args: {
11
11
  readonly currentWorkingDirectory: string;
12
12
  readonly rawTarget: string | undefined;
13
13
  }): Promise<Result<ArrangeTargetWorkspaceAndConfig, AppError>>;
@@ -1,9 +1,8 @@
1
1
  import { resolveArrangeTargetPath } from "#/arrange/resolve-target";
2
+ import { resolveProjectRootResult } from "#/core/cli/resolve-root";
2
3
  import { loadCodefastConfig } from "#/core/config";
3
4
  import { AppError } from "#/core/errors";
4
- import { messageFrom } from "#/core/errors";
5
5
  import { err, ok } from "#/core/result";
6
- import { resolveProjectRoot } from "#/core/workspace/resolver";
7
6
  /**
8
7
  * Resolves the arrange target, repo root, and loaded config an arrange run needs.
9
8
  *
@@ -17,13 +16,11 @@ export async function prepareArrangeWorkspace(fs, args) {
17
16
  if (!fs.existsSync(resolvedTarget)) {
18
17
  return err(new AppError("NOT_FOUND", `Not found: ${resolvedTarget}`));
19
18
  }
20
- let rootDir;
21
- try {
22
- rootDir = resolveProjectRoot(args.currentWorkingDirectory, fs).rootDir;
23
- }
24
- catch (caughtError) {
25
- return err(new AppError("INFRA_FAILURE", messageFrom(caughtError), caughtError));
19
+ const rootOutcome = resolveProjectRootResult(fs, args.currentWorkingDirectory);
20
+ if (!rootOutcome.ok) {
21
+ return rootOutcome;
26
22
  }
23
+ const rootDir = rootOutcome.value;
27
24
  const loadedOutcome = await loadCodefastConfig(rootDir, fs);
28
25
  if (!loadedOutcome.ok) {
29
26
  return loadedOutcome;
@@ -0,0 +1,13 @@
1
+ import type { ArrangeRunResult } from "#/arrange/domain/types";
2
+ /**
3
+ * Machine-readable `arrange` run summary for `--json`, minus the non-serializable preview plans.
4
+ *
5
+ * @since 0.11.0
6
+ */
7
+ export declare function formatArrangeJsonOutput(result: ArrangeRunResult, write: boolean): string;
8
+ /**
9
+ * Exit `1` when an after-write hook failed, `0` otherwise.
10
+ *
11
+ * @since 0.11.0
12
+ */
13
+ export declare function exitCodeForArrangeResult(result: ArrangeRunResult): number;
@@ -0,0 +1,23 @@
1
+ import { CLI_EXIT_GENERAL_ERROR, CLI_EXIT_SUCCESS } from "#/core/exit-codes";
2
+ /**
3
+ * Machine-readable `arrange` run summary for `--json`, minus the non-serializable preview plans.
4
+ *
5
+ * @since 0.11.0
6
+ */
7
+ export function formatArrangeJsonOutput(result, write) {
8
+ const { previewPlans: _plans, ...serializableResult } = result;
9
+ return JSON.stringify({
10
+ schemaVersion: 1,
11
+ ok: result.hookError === null,
12
+ write,
13
+ result: serializableResult,
14
+ });
15
+ }
16
+ /**
17
+ * Exit `1` when an after-write hook failed, `0` otherwise.
18
+ *
19
+ * @since 0.11.0
20
+ */
21
+ export function exitCodeForArrangeResult(result) {
22
+ return result.hookError !== null ? CLI_EXIT_GENERAL_ERROR : CLI_EXIT_SUCCESS;
23
+ }
@@ -0,0 +1,20 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * The request payload for a top-level `arrange` run.
4
+ *
5
+ * @since 0.3.16-canary.0
6
+ */
7
+ export type ArrangeRunRequest = {
8
+ rootDir: string;
9
+ targetPath: string;
10
+ write: boolean;
11
+ withClassName?: boolean | undefined;
12
+ cnImport?: string | undefined;
13
+ config?: unknown;
14
+ };
15
+ /**
16
+ * The `zod` schema validating an `arrange` run request.
17
+ *
18
+ * @since 0.3.16-canary.0
19
+ */
20
+ export declare const arrangeRunRequestSchema: z.ZodType<ArrangeRunRequest>;
@@ -0,0 +1,14 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * The `zod` schema validating an `arrange` run request.
4
+ *
5
+ * @since 0.3.16-canary.0
6
+ */
7
+ export const arrangeRunRequestSchema = z.object({
8
+ rootDir: z.string().min(1),
9
+ targetPath: z.string().min(1),
10
+ write: z.boolean(),
11
+ withClassName: z.boolean().optional(),
12
+ cnImport: z.string().optional(),
13
+ config: z.unknown().optional(),
14
+ });
@@ -0,0 +1,14 @@
1
+ import type { GroupFileWorkPlan } from "#/arrange/domain/grouping-service";
2
+ import type { ArrangeRunResult } from "#/arrange/domain/types";
3
+ /**
4
+ * Presents the totals and follow-up hints for an `arrange` run.
5
+ *
6
+ * @since 0.3.16-canary.0
7
+ */
8
+ export declare function presentArrangeResult(result: ArrangeRunResult, write: boolean): void;
9
+ /**
10
+ * Presents the per-file preview of a work plan's unwrap and grouping edits.
11
+ *
12
+ * @since 0.3.16-canary.0
13
+ */
14
+ export declare function presentGroupFilePreviewFromWork(work: GroupFileWorkPlan): void;
@@ -0,0 +1,72 @@
1
+ import { lineOf } from "#/arrange/domain/ast/helpers";
2
+ import { logger } from "#/core/logger";
3
+ /**
4
+ * Presents the totals and follow-up hints for an `arrange` run.
5
+ *
6
+ * @since 0.3.16-canary.0
7
+ */
8
+ export function presentArrangeResult(result, write) {
9
+ logger.out(`\nTotal: ${result.filePaths.length} file(s), ${result.totalFound} site(s) (cn/tv/JSX className) to review.`);
10
+ if (write) {
11
+ logger.out(`Applied: ${result.totalChanged} site(s) updated.`);
12
+ }
13
+ else {
14
+ logger.out(`(Re-run without --dry-run to write changes, or "pnpm cli:arrange" / "pnpm exec codefast arrange")`);
15
+ }
16
+ const shouldShowCascadeHint = write ? result.totalChanged > 0 : result.totalFound > 0;
17
+ if (shouldShowCascadeHint) {
18
+ logger.out("Note: class order may change across concern groups — smoke-test the UI if you rely on cascade order.");
19
+ }
20
+ if (result.hookError !== null) {
21
+ logger.err(result.hookError);
22
+ }
23
+ }
24
+ /**
25
+ * Presents the per-file preview of a work plan's unwrap and grouping edits.
26
+ *
27
+ * @since 0.3.16-canary.0
28
+ */
29
+ export function presentGroupFilePreviewFromWork(work) {
30
+ presentGroupFilePreviewBody({
31
+ filePath: work.filePath,
32
+ reportTotal: work.reportTotal,
33
+ cnInTvNoReplacement: work.cnInTvNoReplacement,
34
+ cnInTvCalls: work.cnInTvCalls,
35
+ unwrapReplacementByCall: work.unwrapReplacementByCall,
36
+ sourceText: work.sourceText,
37
+ domainSf: work.domainSfForLineNumbers,
38
+ unwrapEdits: work.unwrapEdits,
39
+ plannedGroupEdits: work.plannedGroupEdits,
40
+ });
41
+ }
42
+ function presentGroupFilePreviewBody(args) {
43
+ const { filePath, reportTotal, cnInTvNoReplacement, cnInTvCalls, unwrapReplacementByCall, sourceText, domainSf, unwrapEdits, plannedGroupEdits, } = args;
44
+ let header = `\n── ${filePath} (${reportTotal} site(s)`;
45
+ if (cnInTvNoReplacement > 0) {
46
+ header += `; plus ${cnInTvNoReplacement} cn() inside tv left unchanged (0 args)`;
47
+ }
48
+ header += `) ──`;
49
+ logger.out(header);
50
+ for (const call of cnInTvCalls) {
51
+ const replacement = unwrapReplacementByCall.get(call);
52
+ if (replacement === undefined) {
53
+ logger.out(` Line ${lineOf(domainSf, call)} [tv ⊃ cn]: cn(...) has no arguments — skipped`);
54
+ continue;
55
+ }
56
+ const start = call.pos;
57
+ const end = call.end;
58
+ if (sourceText.slice(start, end) === replacement) {
59
+ continue;
60
+ }
61
+ logger.out(` Line ${lineOf(domainSf, call)} [tv ⊃ cn → string/array]:`);
62
+ logger.out(` ${replacement.split("\n").join("\n ")}`);
63
+ }
64
+ if (unwrapEdits.length > 0 && plannedGroupEdits.length > 0) {
65
+ logger.out(" ([cn] / [tv] / [JSX className] lines below reflect content after unwrap of cn inside tv.)");
66
+ }
67
+ for (const plan of plannedGroupEdits) {
68
+ logger.out(` Line ${lineOf(plan.lineSf, plan.reportNode)} [${plan.label}]:`);
69
+ logger.out(` ${plan.replacement.split("\n").join("\n ")}`);
70
+ logger.out(` // Buckets: ${JSON.stringify(plan.bucketSummary)}`);
71
+ }
72
+ }
@@ -1,11 +1,11 @@
1
1
  import type { ArrangeGroupFileOptions, GroupFileResult } from "#/arrange/domain/types";
2
- import type { FilesystemPort } from "#/core/filesystem/port";
2
+ import type { Filesystem } from "#/core/filesystem/filesystem";
3
3
  /**
4
4
  * Runs the grouping pipeline on one file — preview or write — and returns its per-file result.
5
5
  *
6
6
  * @since 0.3.16-canary.0
7
7
  */
8
- export declare function processArrangeGroupFile(fs: FilesystemPort, args: {
8
+ export declare function processArrangeGroupFile(fs: Filesystem, args: {
9
9
  readonly filePath: string;
10
10
  readonly options: ArrangeGroupFileOptions;
11
11
  }): GroupFileResult;
@@ -1,11 +1,11 @@
1
- import type { ArrangeSyncRunRequest } from "#/arrange/cli-schema";
2
1
  import type { ArrangeRunResult } from "#/arrange/domain/types";
2
+ import type { ArrangeRunRequest } from "#/arrange/regroup/cli-schema";
3
3
  import type { AppError } from "#/core/errors";
4
- import type { FilesystemPort } from "#/core/filesystem/port";
4
+ import type { Filesystem } from "#/core/filesystem/filesystem";
5
5
  import type { Result } from "#/core/result";
6
6
  /**
7
7
  * Runs the grouping pipeline over every target file and returns the aggregated result.
8
8
  *
9
9
  * @since 0.3.16-canary.0
10
10
  */
11
- export declare function runArrangeSync(fs: FilesystemPort, request: ArrangeSyncRunRequest): Promise<Result<ArrangeRunResult, AppError>>;
11
+ export declare function runArrange(fs: Filesystem, request: ArrangeRunRequest): Promise<Result<ArrangeRunResult, AppError>>;
@@ -1,4 +1,4 @@
1
- import { processArrangeGroupFile } from "#/arrange/process-file";
1
+ import { processArrangeGroupFile } from "#/arrange/regroup/process-file";
2
2
  import { scanArrangeTargets } from "#/arrange/scan-target";
3
3
  import { messageFrom } from "#/core/errors";
4
4
  import { ok } from "#/core/result";
@@ -19,7 +19,7 @@ async function runOnAfterWriteHook(hook, modifiedFiles) {
19
19
  *
20
20
  * @since 0.3.16-canary.0
21
21
  */
22
- export async function runArrangeSync(fs, request) {
22
+ export async function runArrange(fs, request) {
23
23
  const filePaths = scanArrangeTargets(fs, request.targetPath);
24
24
  const modifiedFiles = [];
25
25
  const previewPlans = [];
@@ -1,10 +1,10 @@
1
- import type { FilesystemPort } from "#/core/filesystem/port";
1
+ import type { Filesystem } from "#/core/filesystem/filesystem";
2
2
  /**
3
3
  * Resolves the arrange target to a canonical path, defaulting to the nearest package directory.
4
4
  *
5
5
  * @since 0.3.16-canary.0
6
6
  */
7
- export declare function resolveArrangeTargetPath(fs: FilesystemPort, args: {
7
+ export declare function resolveArrangeTargetPath(fs: Filesystem, args: {
8
8
  readonly currentWorkingDirectory: string;
9
9
  readonly rawTarget: string | undefined;
10
10
  }): string;
@@ -1,7 +1,7 @@
1
- import type { FilesystemPort } from "#/core/filesystem/port";
1
+ import type { Filesystem } from "#/core/filesystem/filesystem";
2
2
  /**
3
3
  * Lists the files an arrange run visits — a directory's non-test `.ts`/`.tsx` files, or the single target.
4
4
  *
5
5
  * @since 0.3.16-canary.0
6
6
  */
7
- export declare function scanArrangeTargets(fs: FilesystemPort, targetPath: string): Array<string>;
7
+ export declare function scanArrangeTargets(fs: Filesystem, targetPath: string): Array<string>;
@@ -0,0 +1,7 @@
1
+ import type { ArrangeRunResult } from "#/arrange/domain/types";
2
+ /**
3
+ * Machine-readable `arrange simplify` summary for `--json`.
4
+ *
5
+ * @since 0.11.0
6
+ */
7
+ export declare function formatArrangeSimplifyJsonOutput(result: ArrangeRunResult, write: boolean): string;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Machine-readable `arrange simplify` summary for `--json`.
3
+ *
4
+ * @since 0.11.0
5
+ */
6
+ export function formatArrangeSimplifyJsonOutput(result, write) {
7
+ return JSON.stringify({ schemaVersion: 1, ok: true, write, result });
8
+ }
@@ -0,0 +1,17 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * The request payload for an `arrange simplify` run.
4
+ *
5
+ * @since 0.11.0
6
+ */
7
+ export type ArrangeSimplifyRunRequest = {
8
+ targetPath: string;
9
+ write: boolean;
10
+ foldVariantClassName?: boolean | undefined;
11
+ };
12
+ /**
13
+ * The `zod` schema validating an `arrange simplify` run request.
14
+ *
15
+ * @since 0.11.0
16
+ */
17
+ export declare const arrangeSimplifyRunRequestSchema: z.ZodType<ArrangeSimplifyRunRequest>;
@@ -0,0 +1,11 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * The `zod` schema validating an `arrange simplify` run request.
4
+ *
5
+ * @since 0.11.0
6
+ */
7
+ export const arrangeSimplifyRunRequestSchema = z.object({
8
+ targetPath: z.string().min(1),
9
+ write: z.boolean(),
10
+ foldVariantClassName: z.boolean().optional(),
11
+ });
@@ -0,0 +1,13 @@
1
+ /** Collects cn() calls whose override classes can fold into a variant function's `className` option. */
2
+ import type { DomainSourceFile } from "#/arrange/domain/ast/ast-node";
3
+ import type { PlannedSimplifyEdit } from "#/arrange/domain/ast/simplify-targets";
4
+ import type { FileClassNameProbe } from "#/arrange/simplify/variant-classname-probe";
5
+ /**
6
+ * Collects `className` fold edits for every foldable `cn(variant({…}), …)` call in a source file.
7
+ *
8
+ * @remarks The type server is loaded lazily via `resolveFileProbe` — only once a syntactic candidate
9
+ * exists — so files with no foldable call never pay for a type query.
10
+ *
11
+ * @since 0.11.0
12
+ */
13
+ export declare function collectClassNameFoldTargets(sourceFile: DomainSourceFile, resolveFileProbe: () => FileClassNameProbe | null): Array<PlannedSimplifyEdit>;
@@ -0,0 +1,139 @@
1
+ /** Collects cn() calls whose override classes can fold into a variant function's `className` option. */
2
+ import { forEachDomainChild, isDomainCallExpression, isDomainIdentifier, isDomainObjectLiteralExpression, isDomainPropertyAccessExpression, isDomainPropertyAssignment, isDomainTailwindClassLiteral, } from "#/arrange/domain/ast/ast-node";
3
+ import { buildKnownCnTvBindings, isCnOrTvIdentifier, propertyAssignmentNameText } from "#/arrange/domain/ast/helpers";
4
+ import { escapeTsStringLiteralContent } from "#/arrange/domain/source-text-formatters";
5
+ /** Groups a run of trailing cn() arguments, coalescing adjacent static literals into one piece. */
6
+ function coalesceTrailingArgs(args, sourceText) {
7
+ const pieces = [];
8
+ for (const arg of args) {
9
+ if (isDomainTailwindClassLiteral(arg)) {
10
+ const last = pieces.at(-1);
11
+ if (last?.kind === "static") {
12
+ last.texts.push(arg.text);
13
+ }
14
+ else {
15
+ pieces.push({ kind: "static", texts: [arg.text] });
16
+ }
17
+ }
18
+ else {
19
+ pieces.push({ kind: "dynamic", src: sourceText.slice(arg.pos, arg.end) });
20
+ }
21
+ }
22
+ return pieces;
23
+ }
24
+ /** Renders one class piece as either a quoted merged string or the raw source of a dynamic argument. */
25
+ function renderPiece(piece) {
26
+ return piece.kind === "static" ? `"${escapeTsStringLiteralContent(piece.texts.join(" ").trim())}"` : piece.src;
27
+ }
28
+ /** The source offset of the callee's name — an identifier or the property of a member access. */
29
+ function calleeNameOffset(expression) {
30
+ if (isDomainIdentifier(expression)) {
31
+ return expression.pos;
32
+ }
33
+ if (isDomainPropertyAccessExpression(expression)) {
34
+ return expression.name.pos;
35
+ }
36
+ return null;
37
+ }
38
+ /** True when the object literal is a plain bag of assignments that does not already set `className`/`class`. */
39
+ function objectAcceptsClassNameFold(objectArg) {
40
+ for (const property of objectArg.properties) {
41
+ // A spread or shorthand parses as a non-assignment node — its `className` cannot be reasoned about.
42
+ if (!isDomainPropertyAssignment(property)) {
43
+ return false;
44
+ }
45
+ const name = propertyAssignmentNameText(property);
46
+ if (name === "className" || name === "class") {
47
+ return false;
48
+ }
49
+ }
50
+ return true;
51
+ }
52
+ /** Builds a fold candidate from syntax alone — no type server — or `null` when the shape is not foldable. */
53
+ function buildFoldCandidate(cnCall, sourceText) {
54
+ const args = cnCall.arguments;
55
+ const variantCall = args[0];
56
+ if (args.length < 2 || variantCall === undefined || !isDomainCallExpression(variantCall)) {
57
+ return null;
58
+ }
59
+ const objectArg = variantCall.arguments[0];
60
+ if (variantCall.arguments.length !== 1 ||
61
+ objectArg === undefined ||
62
+ !isDomainObjectLiteralExpression(objectArg) ||
63
+ !objectAcceptsClassNameFold(objectArg)) {
64
+ return null;
65
+ }
66
+ const calleeOffset = calleeNameOffset(variantCall.expression);
67
+ if (calleeOffset === null) {
68
+ return null;
69
+ }
70
+ const pieces = coalesceTrailingArgs(args.slice(1), sourceText);
71
+ if (pieces.length === 0) {
72
+ return null;
73
+ }
74
+ // A dynamic override or more than one piece becomes an array, so the option must accept one; a
75
+ // single static override stays a scalar string. Types decide whether either shape is legal.
76
+ const needsArray = pieces.length > 1 || pieces.some((piece) => piece.kind === "dynamic");
77
+ const rendered = pieces.map(renderPiece);
78
+ const classNameValue = pieces.length > 1 ? `[${rendered.join(", ")}]` : rendered[0];
79
+ // Splice `className` in right after the last property (or just inside an empty object), keeping the
80
+ // object's original text — a trailing comma or line comment would break a re-serialized `{ … }`.
81
+ const lastProperty = objectArg.properties.at(-1);
82
+ const insertOffset = lastProperty ? lastProperty.end : objectArg.pos + 1;
83
+ const insertion = lastProperty ? `, className: ${classNameValue}` : ` className: ${classNameValue} `;
84
+ const replacement = sourceText.slice(variantCall.pos, insertOffset) + insertion + sourceText.slice(insertOffset, variantCall.end);
85
+ return {
86
+ calleeOffset,
87
+ needsArray,
88
+ edit: {
89
+ start: cnCall.pos,
90
+ end: cnCall.end,
91
+ replacement,
92
+ label: "cn-fold-classname",
93
+ },
94
+ };
95
+ }
96
+ /**
97
+ * Collects `className` fold edits for every foldable `cn(variant({…}), …)` call in a source file.
98
+ *
99
+ * @remarks The type server is loaded lazily via `resolveFileProbe` — only once a syntactic candidate
100
+ * exists — so files with no foldable call never pay for a type query.
101
+ *
102
+ * @since 0.11.0
103
+ */
104
+ export function collectClassNameFoldTargets(sourceFile, resolveFileProbe) {
105
+ const sourceText = sourceFile.text;
106
+ const knownBindings = buildKnownCnTvBindings(sourceFile);
107
+ const candidates = [];
108
+ const visit = (node) => {
109
+ if (isDomainCallExpression(node) && isCnOrTvIdentifier(node.expression, "cn", knownBindings)) {
110
+ const candidate = buildFoldCandidate(node, sourceText);
111
+ if (candidate) {
112
+ candidates.push(candidate);
113
+ }
114
+ }
115
+ forEachDomainChild(node, visit);
116
+ };
117
+ for (const statement of sourceFile.statements) {
118
+ visit(statement);
119
+ }
120
+ if (candidates.length === 0) {
121
+ return [];
122
+ }
123
+ const fileProbe = resolveFileProbe();
124
+ if (!fileProbe) {
125
+ return [];
126
+ }
127
+ const results = [];
128
+ for (const candidate of candidates) {
129
+ const acceptance = fileProbe.classNameAcceptance(candidate.calleeOffset);
130
+ if (!acceptance) {
131
+ continue;
132
+ }
133
+ if (candidate.needsArray ? !acceptance.acceptsArray : !acceptance.acceptsString) {
134
+ continue;
135
+ }
136
+ results.push(candidate.edit);
137
+ }
138
+ return results;
139
+ }
@@ -0,0 +1,7 @@
1
+ import type { ArrangeRunResult } from "#/arrange/domain/types";
2
+ /**
3
+ * Presents the totals for an `arrange simplify` run.
4
+ *
5
+ * @since 0.3.16-canary.0
6
+ */
7
+ export declare function presentSimplifyResult(result: ArrangeRunResult, write: boolean): void;
@@ -0,0 +1,15 @@
1
+ import { logger } from "#/core/logger";
2
+ /**
3
+ * Presents the totals for an `arrange simplify` run.
4
+ *
5
+ * @since 0.3.16-canary.0
6
+ */
7
+ export function presentSimplifyResult(result, write) {
8
+ logger.out(`\nTotal: ${result.filePaths.length} file(s), ${result.totalFound} site(s) to simplify.`);
9
+ if (write) {
10
+ logger.out(`Applied: ${result.totalChanged} site(s) updated.`);
11
+ }
12
+ else {
13
+ logger.out(`(Run "simplify" to write changes, or "pnpm cli:arrange-simplify" / "pnpm exec codefast arrange simplify")`);
14
+ }
15
+ }
@@ -1,11 +1,13 @@
1
1
  import type { GroupFileResult } from "#/arrange/domain/types";
2
- import type { FilesystemPort } from "#/core/filesystem/port";
2
+ import type { VariantClassNameProbe } from "#/arrange/simplify/variant-classname-probe";
3
+ import type { Filesystem } from "#/core/filesystem/filesystem";
3
4
  /**
4
5
  * Runs the simplify pass on one file — flattening class expressions and pruning an unused `cn` import.
5
6
  *
6
7
  * @since 0.3.16-canary.0
7
8
  */
8
- export declare function processArrangeSimplifyFile(fs: FilesystemPort, args: {
9
+ export declare function processArrangeSimplifyFile(fs: Filesystem, args: {
9
10
  readonly filePath: string;
10
11
  readonly write: boolean;
12
+ readonly probe?: VariantClassNameProbe | null;
11
13
  }): GroupFileResult;
@@ -0,0 +1,49 @@
1
+ import { collectSimplifyTargets } from "#/arrange/domain/ast/simplify-targets";
2
+ import { dropCnImportIfUnused } from "#/arrange/domain/imports";
3
+ import { collectClassNameFoldTargets } from "#/arrange/simplify/fold-targets";
4
+ import { parseDomainSourceFile } from "#/arrange/source-parse";
5
+ import { applyEditsDescending, dropOverlappingEdits } from "#/core/source-text-edit";
6
+ // A file simplify can act on names `cn`/`tv` (a call or an import to prune) or carries a `class`/`className`;
7
+ // a source with none of these has nothing to flatten or fold, so it never needs parsing.
8
+ const SIMPLIFY_MARKER = /\b(?:cn|tv)\b|className|\bclass\s*[=:]/;
9
+ /**
10
+ * Runs the simplify pass on one file — flattening class expressions and pruning an unused `cn` import.
11
+ *
12
+ * @since 0.3.16-canary.0
13
+ */
14
+ export function processArrangeSimplifyFile(fs, args) {
15
+ const { filePath, write, probe } = args;
16
+ const sourceText = fs.readFileSync(filePath, "utf8");
17
+ if (!SIMPLIFY_MARKER.test(sourceText)) {
18
+ return { filePath, totalFound: 0, changed: 0 };
19
+ }
20
+ const domainSf = parseDomainSourceFile(filePath, sourceText);
21
+ // Resolve the file's type-server project at most once, and only if a syntactic fold candidate needs it.
22
+ let resolvedFileProbe;
23
+ const resolveFileProbe = () => {
24
+ resolvedFileProbe ??= probe ? probe.forFile(filePath) : null;
25
+ return resolvedFileProbe;
26
+ };
27
+ // Combine fold and base edits, drop no-ops, then remove overlaps so applyEditsDescending only sees
28
+ // non-overlapping ranges. Fold edits lead the list, so a fold wins an exact-range tie with a base
29
+ // edit on the same call and nested calls keep only the outermost edit.
30
+ const foldEdits = probe ? collectClassNameFoldTargets(domainSf, resolveFileProbe) : [];
31
+ const baseEdits = collectSimplifyTargets(domainSf);
32
+ const meaningful = dropOverlappingEdits([...foldEdits, ...baseEdits].filter((edit) => sourceText.slice(edit.start, edit.end) !== edit.replacement));
33
+ // Apply class-simplification edits first, then prune any cn import that
34
+ // became (or was already) unused. With no edits the text is unchanged, so the
35
+ // already-parsed tree is reused rather than parsing every untouched file twice.
36
+ const textAfterEdits = meaningful.length > 0 ? applyEditsDescending(sourceText, meaningful) : sourceText;
37
+ const domainSfAfterEdits = meaningful.length > 0 ? parseDomainSourceFile(filePath, textAfterEdits) : domainSf;
38
+ const textAfterImportDrop = dropCnImportIfUnused(domainSfAfterEdits);
39
+ const importDropped = textAfterImportDrop !== textAfterEdits;
40
+ const totalFound = meaningful.length + (importDropped ? 1 : 0);
41
+ if (totalFound === 0) {
42
+ return { filePath, totalFound: 0, changed: 0 };
43
+ }
44
+ if (!write) {
45
+ return { filePath, totalFound, changed: 0 };
46
+ }
47
+ fs.writeFileSync(filePath, textAfterImportDrop, "utf8");
48
+ return { filePath, totalFound, changed: totalFound };
49
+ }