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.
- package/README.md +153 -33
- package/dist/cmd/assert-repo-is-clean.mjs +1 -1
- package/dist/cmd/check-should-run-type-checks.mjs +1 -1
- package/dist/cmd/format-diff-from.mjs +29 -8
- package/dist/cmd/format-diff-from.mjs.map +1 -1
- package/dist/cmd/format-uncommitted.d.mts +3 -0
- package/dist/cmd/format-uncommitted.d.mts.map +1 -0
- package/dist/cmd/format-uncommitted.mjs +59 -0
- package/dist/cmd/format-uncommitted.mjs.map +1 -0
- package/dist/cmd/gen-index-ts.mjs +1 -1
- package/dist/functions/assert-repo-is-clean.d.mts.map +1 -1
- package/dist/functions/assert-repo-is-clean.mjs +30 -30
- package/dist/functions/assert-repo-is-clean.mjs.map +1 -1
- package/dist/functions/diff.d.mts +32 -2
- package/dist/functions/diff.d.mts.map +1 -1
- package/dist/functions/diff.mjs +47 -29
- package/dist/functions/diff.mjs.map +1 -1
- package/dist/functions/exec-async.d.mts +4 -4
- package/dist/functions/exec-async.d.mts.map +1 -1
- package/dist/functions/exec-async.mjs +5 -5
- package/dist/functions/exec-async.mjs.map +1 -1
- package/dist/functions/format.d.mts +20 -11
- package/dist/functions/format.d.mts.map +1 -1
- package/dist/functions/format.mjs +136 -110
- package/dist/functions/format.mjs.map +1 -1
- package/dist/functions/gen-index.d.mts +2 -1
- package/dist/functions/gen-index.d.mts.map +1 -1
- package/dist/functions/gen-index.mjs +10 -8
- package/dist/functions/gen-index.mjs.map +1 -1
- package/dist/functions/index.mjs +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +2 -2
- package/src/cmd/assert-repo-is-clean.mts +1 -1
- package/src/cmd/check-should-run-type-checks.mts +1 -1
- package/src/cmd/format-diff-from.mts +35 -9
- package/src/cmd/format-uncommitted.mts +67 -0
- package/src/cmd/gen-index-ts.mts +1 -1
- package/src/functions/assert-repo-is-clean.mts +43 -34
- package/src/functions/diff.mts +85 -32
- package/src/functions/diff.test.mts +569 -102
- package/src/functions/exec-async.mts +21 -29
- package/src/functions/exec-async.test.mts +77 -47
- package/src/functions/format.mts +222 -150
- package/src/functions/format.test.mts +625 -20
- package/src/functions/gen-index.mts +16 -10
- package/src/functions/workspace-utils/run-cmd-in-stages.test.mts +266 -0
- package/dist/cmd/format-untracked.d.mts +0 -3
- package/dist/cmd/format-untracked.d.mts.map +0 -1
- package/dist/cmd/format-untracked.mjs +0 -34
- package/dist/cmd/format-untracked.mjs.map +0 -1
- package/src/cmd/format-untracked.mts +0 -31
package/src/functions/format.mts
CHANGED
|
@@ -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 {
|
|
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
|
|
17
|
+
export const formatFiles = async (
|
|
13
18
|
files: readonly string[],
|
|
14
19
|
options?: Readonly<{ silent?: boolean }>,
|
|
15
|
-
): Promise<
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
return 'ok';
|
|
26
|
+
conditionalEcho('No files to format');
|
|
27
|
+
return Result.ok(undefined);
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
echo(`Formatting ${files.length} files...`);
|
|
27
|
-
}
|
|
30
|
+
conditionalEcho(`Formatting ${files.length} files...`);
|
|
28
31
|
|
|
29
32
|
// Format each file
|
|
30
|
-
const results
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
47
|
+
// Read file content
|
|
48
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
ignorePath: '.prettierignore',
|
|
42
|
-
});
|
|
50
|
+
// Resolve prettier config for this file
|
|
51
|
+
const prettierOptions = await prettier.resolveConfig(filePath);
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
await fs.writeFile(filePath, formatted, 'utf8');
|
|
77
|
+
return Result.ok(undefined);
|
|
78
|
+
} catch (error) {
|
|
60
79
|
if (!silent) {
|
|
61
|
-
|
|
80
|
+
console.error(`Error formatting ${filePath}:`, error);
|
|
62
81
|
}
|
|
82
|
+
return Result.err(error);
|
|
63
83
|
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
112
|
+
export const formatFilesGlob = async (
|
|
83
113
|
pathGlob: string,
|
|
84
114
|
options?: Readonly<{ silent?: boolean }>,
|
|
85
|
-
): Promise<
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
return 'ok';
|
|
128
|
+
conditionalEcho('No files found matching pattern:', pathGlob);
|
|
129
|
+
return Result.ok(undefined);
|
|
101
130
|
}
|
|
102
131
|
|
|
103
|
-
return await
|
|
132
|
+
return await formatFiles(files, { silent });
|
|
104
133
|
} catch (error) {
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
117
|
-
options?: Readonly<{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
172
|
+
if (!silent) {
|
|
173
|
+
console.error(
|
|
174
|
+
'Error getting changed files:',
|
|
175
|
+
untrackedFilesResult.value,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return untrackedFilesResult;
|
|
129
179
|
}
|
|
130
180
|
|
|
131
|
-
|
|
181
|
+
mut_files.push(...untrackedFilesResult.value);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (modified) {
|
|
185
|
+
const diffFilesResult = await getModifiedFiles({ silent });
|
|
132
186
|
|
|
133
|
-
if (
|
|
187
|
+
if (Result.isErr(diffFilesResult)) {
|
|
134
188
|
if (!silent) {
|
|
135
|
-
|
|
189
|
+
console.error('Error getting changed files:', diffFilesResult.value);
|
|
136
190
|
}
|
|
137
|
-
return
|
|
191
|
+
return diffFilesResult;
|
|
138
192
|
}
|
|
139
193
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
194
|
+
mut_files.push(...diffFilesResult.value);
|
|
195
|
+
}
|
|
143
196
|
|
|
144
|
-
|
|
145
|
-
const
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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<{
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
286
|
+
const files = filesResult.value;
|
|
220
287
|
|
|
221
288
|
// Combine and deduplicate files
|
|
222
|
-
mut_allFiles
|
|
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
|
-
|
|
237
|
-
if (!silent) {
|
|
238
|
-
echo(`No files to format`);
|
|
239
|
-
}
|
|
240
|
-
return 'ok';
|
|
241
|
-
}
|
|
293
|
+
const allFiles = Arr.uniq(mut_allFiles);
|
|
242
294
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
};
|