ts-repo-utils 6.1.0 → 7.0.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 (51) hide show
  1. package/README.md +153 -33
  2. package/dist/cmd/assert-repo-is-clean.mjs +1 -1
  3. package/dist/cmd/check-should-run-type-checks.mjs +1 -1
  4. package/dist/cmd/format-diff-from.mjs +29 -8
  5. package/dist/cmd/format-diff-from.mjs.map +1 -1
  6. package/dist/cmd/format-uncommitted.d.mts +3 -0
  7. package/dist/cmd/format-uncommitted.d.mts.map +1 -0
  8. package/dist/cmd/format-uncommitted.mjs +59 -0
  9. package/dist/cmd/format-uncommitted.mjs.map +1 -0
  10. package/dist/cmd/gen-index-ts.mjs +1 -1
  11. package/dist/functions/assert-repo-is-clean.d.mts.map +1 -1
  12. package/dist/functions/assert-repo-is-clean.mjs +30 -30
  13. package/dist/functions/assert-repo-is-clean.mjs.map +1 -1
  14. package/dist/functions/diff.d.mts +32 -2
  15. package/dist/functions/diff.d.mts.map +1 -1
  16. package/dist/functions/diff.mjs +47 -29
  17. package/dist/functions/diff.mjs.map +1 -1
  18. package/dist/functions/exec-async.d.mts +4 -4
  19. package/dist/functions/exec-async.d.mts.map +1 -1
  20. package/dist/functions/exec-async.mjs +5 -5
  21. package/dist/functions/exec-async.mjs.map +1 -1
  22. package/dist/functions/format.d.mts +20 -11
  23. package/dist/functions/format.d.mts.map +1 -1
  24. package/dist/functions/format.mjs +136 -110
  25. package/dist/functions/format.mjs.map +1 -1
  26. package/dist/functions/gen-index.d.mts +2 -1
  27. package/dist/functions/gen-index.d.mts.map +1 -1
  28. package/dist/functions/gen-index.mjs +10 -8
  29. package/dist/functions/gen-index.mjs.map +1 -1
  30. package/dist/functions/index.mjs +2 -2
  31. package/dist/index.mjs +2 -2
  32. package/package.json +2 -2
  33. package/src/cmd/assert-repo-is-clean.mts +1 -1
  34. package/src/cmd/check-should-run-type-checks.mts +1 -1
  35. package/src/cmd/format-diff-from.mts +35 -9
  36. package/src/cmd/format-uncommitted.mts +67 -0
  37. package/src/cmd/gen-index-ts.mts +1 -1
  38. package/src/functions/assert-repo-is-clean.mts +43 -34
  39. package/src/functions/diff.mts +85 -32
  40. package/src/functions/diff.test.mts +569 -102
  41. package/src/functions/exec-async.mts +21 -29
  42. package/src/functions/exec-async.test.mts +77 -47
  43. package/src/functions/format.mts +222 -150
  44. package/src/functions/format.test.mts +625 -20
  45. package/src/functions/gen-index.mts +16 -10
  46. package/src/functions/workspace-utils/run-cmd-in-stages.test.mts +266 -0
  47. package/dist/cmd/format-untracked.d.mts +0 -3
  48. package/dist/cmd/format-untracked.d.mts.map +0 -1
  49. package/dist/cmd/format-untracked.mjs +0 -34
  50. package/dist/cmd/format-untracked.mjs.map +0 -1
  51. package/src/cmd/format-untracked.mts +0 -31
@@ -1,89 +1,120 @@
1
+ import { type ExecException } from 'node:child_process';
1
2
  import * as prettier from 'prettier';
2
- import { Arr, Result } from 'ts-data-forge';
3
+ import { Arr, isNotUndefined, Result } from 'ts-data-forge';
3
4
  import '../node-global.mjs';
4
- import { getDiffFrom, getUntrackedFiles } from './diff.mjs';
5
+ import {
6
+ getDiffFrom,
7
+ getModifiedFiles,
8
+ getStagedFiles,
9
+ getUntrackedFiles,
10
+ } from './diff.mjs';
5
11
 
6
12
  /**
7
13
  * Format a list of files using Prettier
8
14
  *
9
15
  * @param files - Array of file paths to format
10
- * @returns 'ok' if successful, 'err' if any errors occurred
11
16
  */
12
- export const formatFilesList = async (
17
+ export const formatFiles = async (
13
18
  files: readonly string[],
14
19
  options?: Readonly<{ silent?: boolean }>,
15
- ): Promise<'ok' | 'err'> => {
20
+ ): Promise<Result<undefined, readonly unknown[]>> => {
16
21
  const silent = options?.silent ?? false;
17
22
 
23
+ const conditionalEcho = silent ? () => {} : echo;
24
+
18
25
  if (files.length === 0) {
19
- if (!silent) {
20
- echo('No files to format');
21
- }
22
- return 'ok';
26
+ conditionalEcho('No files to format');
27
+ return Result.ok(undefined);
23
28
  }
24
29
 
25
- if (!silent) {
26
- echo(`Formatting ${files.length} files...`);
27
- }
30
+ conditionalEcho(`Formatting ${files.length} files...`);
28
31
 
29
32
  // Format each file
30
- const results = await Promise.allSettled(
31
- files.map(async (filePath) => {
32
- try {
33
- // Read file content
34
- const content = await fs.readFile(filePath, 'utf8');
33
+ const results: readonly PromiseSettledResult<Result<undefined, unknown>>[] =
34
+ // NOTE: Using Promise.allSettled to ensure all files are processed even if some fail
35
+ await Promise.allSettled(
36
+ files.map(async (filePath) => {
37
+ try {
38
+ // Check if file exists first
39
+ try {
40
+ await fs.access(filePath);
41
+ } catch {
42
+ // File doesn't exist, skip it
43
+ conditionalEcho(`Skipping non-existent file: ${filePath}`);
44
+ return Result.ok(undefined);
45
+ }
35
46
 
36
- // Resolve prettier config for this file
37
- const prettierOptions = await prettier.resolveConfig(filePath);
47
+ // Read file content
48
+ const content = await fs.readFile(filePath, 'utf8');
38
49
 
39
- // Check if file is ignored by prettier
40
- const fileInfo = await prettier.getFileInfo(filePath, {
41
- ignorePath: '.prettierignore',
42
- });
50
+ // Resolve prettier config for this file
51
+ const prettierOptions = await prettier.resolveConfig(filePath);
43
52
 
44
- if (fileInfo.ignored) {
45
- if (!silent) {
46
- echo(`Skipping ignored file: ${filePath}`);
53
+ // Check if file is ignored by prettier
54
+ const fileInfo = await prettier.getFileInfo(filePath, {
55
+ ignorePath: '.prettierignore',
56
+ });
57
+
58
+ if (fileInfo.ignored) {
59
+ conditionalEcho(`Skipping ignored file: ${filePath}`);
60
+ return Result.ok(undefined);
47
61
  }
48
- return;
49
- }
50
62
 
51
- // Format the content
52
- const formatted = await prettier.format(content, {
53
- ...prettierOptions,
54
- filepath: filePath,
55
- });
63
+ // Format the content
64
+ const formatted = await prettier.format(content, {
65
+ ...prettierOptions,
66
+ filepath: filePath,
67
+ });
68
+
69
+ // Only write if content changed
70
+ if (formatted === content) {
71
+ conditionalEcho(`Unchanged: ${filePath}`);
72
+ } else {
73
+ await fs.writeFile(filePath, formatted, 'utf8');
74
+ conditionalEcho(`Formatted: ${filePath}`);
75
+ }
56
76
 
57
- // Only write if content changed
58
- if (formatted !== content) {
59
- await fs.writeFile(filePath, formatted, 'utf8');
77
+ return Result.ok(undefined);
78
+ } catch (error) {
60
79
  if (!silent) {
61
- echo(`Formatted: ${filePath}`);
80
+ console.error(`Error formatting ${filePath}:`, error);
62
81
  }
82
+ return Result.err(error);
63
83
  }
64
- } catch (error) {
65
- console.error(`Error formatting ${filePath}:`, error);
66
- throw error;
67
- }
68
- }),
69
- );
84
+ }),
85
+ );
70
86
 
71
- // Check if any formatting failed
72
- const hasErrors = results.some((result) => result.status === 'rejected');
73
- return hasErrors ? 'err' : 'ok';
87
+ if (results.every((r) => r.status === 'fulfilled')) {
88
+ const fulfilled = results.map((r) => r.value);
89
+ if (fulfilled.every(Result.isOk)) {
90
+ return Result.ok(undefined);
91
+ } else {
92
+ const errors: readonly unknown[] = fulfilled
93
+ .filter(Result.isErr)
94
+ .map((r) => r.value);
95
+
96
+ return Result.err(errors);
97
+ }
98
+ } else {
99
+ const errors: readonly unknown[] = results
100
+ .filter((r) => r.status === 'rejected')
101
+ .map((r) => r.reason as unknown);
102
+
103
+ return Result.err(errors);
104
+ }
74
105
  };
75
106
 
76
107
  /**
77
108
  * Format files matching the given glob pattern using Prettier
78
109
  *
79
110
  * @param pathGlob - Glob pattern to match files
80
- * @returns 'ok' if successful, 'err' if any errors occurred
81
111
  */
82
- export const formatFiles = async (
112
+ export const formatFilesGlob = async (
83
113
  pathGlob: string,
84
114
  options?: Readonly<{ silent?: boolean }>,
85
- ): Promise<'ok' | 'err'> => {
115
+ ): Promise<Result<undefined, unknown>> => {
86
116
  const silent = options?.silent ?? false;
117
+ const conditionalEcho = silent ? () => {} : echo;
87
118
 
88
119
  try {
89
120
  // Find all files matching the glob
@@ -94,16 +125,16 @@ export const formatFiles = async (
94
125
  });
95
126
 
96
127
  if (files.length === 0) {
97
- if (!silent) {
98
- echo('No files found matching pattern:', pathGlob);
99
- }
100
- return 'ok';
128
+ conditionalEcho('No files found matching pattern:', pathGlob);
129
+ return Result.ok(undefined);
101
130
  }
102
131
 
103
- return await formatFilesList(files, { silent });
132
+ return await formatFiles(files, { silent });
104
133
  } catch (error) {
105
- console.error('Error in formatFiles:', error);
106
- return 'err';
134
+ if (!silent) {
135
+ console.error('Error in formatFiles:', error);
136
+ }
137
+ return Result.err(error);
107
138
  }
108
139
  };
109
140
 
@@ -111,63 +142,72 @@ export const formatFiles = async (
111
142
  * Format only files that have been changed (git status)
112
143
  *
113
144
  * @param options - Options for formatting
114
- * @returns 'ok' if successful, 'err' if any errors occurred
115
145
  */
116
- export const formatUntracked = async (
117
- options?: Readonly<{ silent?: boolean }>,
118
- ): Promise<'ok' | 'err'> => {
119
- const silent = options?.silent ?? false;
120
-
121
- try {
122
- const untrackedFilesResult = await getUntrackedFiles({
123
- silent,
124
- });
146
+ export const formatUncommittedFiles = async (
147
+ options?: Readonly<{
148
+ untracked?: boolean;
149
+ modified?: boolean;
150
+ staged?: boolean;
151
+ silent?: boolean;
152
+ }>,
153
+ ): Promise<
154
+ Result<
155
+ undefined,
156
+ ExecException | Readonly<{ message: string }> | readonly unknown[]
157
+ >
158
+ > => {
159
+ const {
160
+ untracked = true,
161
+ modified = true,
162
+ staged = true,
163
+ silent = false,
164
+ } = options ?? {};
165
+
166
+ const mut_files: string[] = [];
167
+
168
+ if (untracked) {
169
+ const untrackedFilesResult = await getUntrackedFiles({ silent });
125
170
 
126
171
  if (Result.isErr(untrackedFilesResult)) {
127
- console.error('Error getting changed files:', untrackedFilesResult.value);
128
- return 'err';
172
+ if (!silent) {
173
+ console.error(
174
+ 'Error getting changed files:',
175
+ untrackedFilesResult.value,
176
+ );
177
+ }
178
+ return untrackedFilesResult;
129
179
  }
130
180
 
131
- const files = untrackedFilesResult.value;
181
+ mut_files.push(...untrackedFilesResult.value);
182
+ }
183
+
184
+ if (modified) {
185
+ const diffFilesResult = await getModifiedFiles({ silent });
132
186
 
133
- if (files.length === 0) {
187
+ if (Result.isErr(diffFilesResult)) {
134
188
  if (!silent) {
135
- echo('No changed files to format');
189
+ console.error('Error getting changed files:', diffFilesResult.value);
136
190
  }
137
- return 'ok';
191
+ return diffFilesResult;
138
192
  }
139
193
 
140
- if (!silent) {
141
- echo('Formatting changed files:', files);
142
- }
194
+ mut_files.push(...diffFilesResult.value);
195
+ }
143
196
 
144
- // Filter out non-existent files before formatting
145
- const fileExistenceChecks = await Promise.allSettled(
146
- files.map(async (filePath) => {
147
- try {
148
- await fs.readFile(filePath, 'utf8');
149
- return filePath;
150
- } catch {
151
- if (!silent) {
152
- echo(`Skipping non-existent file: ${filePath}`);
153
- }
154
- return undefined;
155
- }
156
- }),
157
- );
197
+ if (staged) {
198
+ const stagedFilesResult = await getStagedFiles({ silent });
158
199
 
159
- const existingFiles = fileExistenceChecks
160
- .filter(
161
- (result): result is PromiseFulfilledResult<string> =>
162
- result.status === 'fulfilled' && result.value !== undefined,
163
- )
164
- .map((result) => result.value);
200
+ if (Result.isErr(stagedFilesResult)) {
201
+ if (!silent) {
202
+ console.error('Error getting changed files:', stagedFilesResult.value);
203
+ }
204
+ return stagedFilesResult;
205
+ }
165
206
 
166
- return await formatFilesList(existingFiles, { silent });
167
- } catch (error) {
168
- console.error('Error in formatUntracked:', error);
169
- return 'err';
207
+ mut_files.push(...stagedFilesResult.value);
170
208
  }
209
+
210
+ return formatFiles(Arr.uniq(mut_files), { silent });
171
211
  };
172
212
 
173
213
  /**
@@ -178,71 +218,103 @@ export const formatUntracked = async (
178
218
  * @param options - Options for formatting
179
219
  * @param options.includeUntracked - Include untracked files in addition to diff
180
220
  * files (default is true)
221
+ * @param options.includeStaged - Include staged files in addition to diff files
222
+ * (default is true)
181
223
  * @param options.silent - Silent mode to suppress command output (default is
182
224
  * false)
183
- * @returns 'ok' if successful, 'err' if any errors occurred
184
225
  */
185
226
  export const formatDiffFrom = async (
186
227
  base: string,
187
- options?: Readonly<{ includeUntracked?: boolean; silent?: boolean }>,
188
- ): Promise<'ok' | 'err'> => {
189
- const silent = options?.silent ?? false;
190
-
191
- try {
192
- // Get files that differ from base branch/commit (excluding deleted files)
193
- const diffFromBaseResult = await getDiffFrom(base, {
194
- silent,
195
- });
196
-
197
- if (Result.isErr(diffFromBaseResult)) {
228
+ options?: Readonly<{
229
+ includeUntracked?: boolean;
230
+ includeModified?: boolean;
231
+ includeStaged?: boolean;
232
+ silent?: boolean;
233
+ }>,
234
+ ): Promise<
235
+ Result<
236
+ undefined,
237
+ | ExecException
238
+ | Readonly<{
239
+ message: string;
240
+ }>
241
+ | readonly unknown[]
242
+ >
243
+ > => {
244
+ // const silent = options?.silent ?? false;
245
+ const {
246
+ silent = false,
247
+ includeUntracked = true,
248
+ includeModified = true,
249
+ includeStaged = true,
250
+ } = options ?? {};
251
+
252
+ const conditionalEcho = silent ? () => {} : echo;
253
+
254
+ // Get files that differ from base branch/commit (excluding deleted files)
255
+ const diffFromBaseResult = await getDiffFrom(base, {
256
+ silent,
257
+ });
258
+
259
+ if (Result.isErr(diffFromBaseResult)) {
260
+ if (!silent) {
198
261
  console.error('Error getting changed files:', diffFromBaseResult.value);
199
- return 'err';
200
262
  }
263
+ return diffFromBaseResult;
264
+ }
201
265
 
202
- const diffFiles = diffFromBaseResult.value;
203
- let mut_allFiles = diffFiles;
204
-
205
- // If includeUntracked is true, also get untracked files
206
- if (options?.includeUntracked ?? true) {
207
- const untrackedFilesResult = await getUntrackedFiles({
208
- silent,
209
- });
210
-
211
- if (Result.isErr(untrackedFilesResult)) {
212
- console.error(
213
- 'Error getting untracked files:',
214
- untrackedFilesResult.value,
215
- );
216
- return 'err';
266
+ const diffFiles = diffFromBaseResult.value;
267
+ const mut_allFiles: string[] = diffFiles.slice();
268
+
269
+ // If includeUntracked is true, also get untracked files
270
+ for (const { type, flag, fn } of [
271
+ { type: 'untracked', flag: includeUntracked, fn: getUntrackedFiles },
272
+ { type: 'modified', flag: includeModified, fn: getModifiedFiles },
273
+ { type: 'staged', flag: includeStaged, fn: getStagedFiles },
274
+ ]) {
275
+ if (flag) {
276
+ // eslint-disable-next-line no-await-in-loop
277
+ const filesResult = await fn({ silent });
278
+
279
+ if (Result.isErr(filesResult)) {
280
+ if (!silent) {
281
+ console.error(`Error getting ${type} files:`, filesResult.value);
282
+ }
283
+ return filesResult;
217
284
  }
218
285
 
219
- const untrackedFiles = untrackedFilesResult.value;
286
+ const files = filesResult.value;
220
287
 
221
288
  // Combine and deduplicate files
222
- mut_allFiles = Arr.uniq([...diffFiles, ...untrackedFiles]);
223
-
224
- if (!silent) {
225
- echo(
226
- `Formatting files that differ from ${base} and untracked files:`,
227
- mut_allFiles,
228
- );
229
- }
230
- } else {
231
- if (!silent) {
232
- echo(`Formatting files that differ from ${base}:`, mut_allFiles);
233
- }
289
+ mut_allFiles.push(...files);
234
290
  }
291
+ }
235
292
 
236
- if (mut_allFiles.length === 0) {
237
- if (!silent) {
238
- echo(`No files to format`);
239
- }
240
- return 'ok';
241
- }
293
+ const allFiles = Arr.uniq(mut_allFiles);
242
294
 
243
- return await formatFilesList(mut_allFiles, { silent });
244
- } catch (error) {
245
- console.error('Error in formatDiffFrom:', error);
246
- return 'err';
295
+ if (!silent) {
296
+ const includedFileTypes = [
297
+ includeUntracked ? 'untracked files' : undefined,
298
+ includeModified ? 'modified files' : undefined,
299
+ includeStaged ? 'staged files' : undefined,
300
+ ].filter(isNotUndefined);
301
+
302
+ const message = [
303
+ `Formatting files that differ from ${base}`,
304
+ includedFileTypes
305
+ .map((s, i) =>
306
+ i !== includedFileTypes.length - 1 ? `, ${s}` : ` and ${s}`,
307
+ )
308
+ .join(''),
309
+ ].join('');
310
+
311
+ conditionalEcho(`${message}:`, allFiles);
247
312
  }
313
+
314
+ if (allFiles.length === 0) {
315
+ conditionalEcho('No files to format');
316
+ return Result.ok(undefined);
317
+ }
318
+
319
+ return formatFiles(allFiles, { silent });
248
320
  };