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
@@ -71,12 +71,16 @@ type GenIndexConfigInternal = DeepReadonly<{
71
71
  * @param config - Configuration for index file generation
72
72
  * @throws Error if any step fails.
73
73
  */
74
- export const genIndex = async (config: GenIndexConfig): Promise<void> => {
75
- echo('Starting index file generation...\n');
76
-
74
+ export const genIndex = async (
75
+ config: GenIndexConfig,
76
+ ): Promise<Result<undefined, unknown>> => {
77
77
  // Merge config with defaults
78
78
  const filledConfig: GenIndexConfigInternal = fillConfig(config);
79
79
 
80
+ const conditionalEcho = filledConfig.silent ? () => {} : echo;
81
+
82
+ conditionalEcho('Starting index file generation...\n');
83
+
80
84
  // Normalize target directories to array
81
85
  const targetDirs =
82
86
  typeof config.targetDirectory === 'string'
@@ -92,30 +96,32 @@ export const genIndex = async (config: GenIndexConfig): Promise<void> => {
92
96
  }
93
97
 
94
98
  // Step 2: Generate index files
95
- echo('Generating index files...');
99
+ conditionalEcho('Generating index files...');
96
100
  for (const dir of targetDirs) {
97
101
  const resolvedDir = path.resolve(dir);
98
102
  // eslint-disable-next-line no-await-in-loop
99
103
  await generateIndexFileForDir(resolvedDir, filledConfig);
100
104
  }
101
- echo('✓ Index files generated\n');
105
+ conditionalEcho('✓ Index files generated\n');
102
106
 
103
107
  // Step 3: Format generated files
104
108
  if (filledConfig.formatCommand !== undefined) {
105
- echo('Formatting generated files...');
109
+ conditionalEcho('Formatting generated files...');
106
110
  const fmtResult = await $(filledConfig.formatCommand, {
107
111
  silent: filledConfig.silent,
108
112
  });
109
113
  if (Result.isErr(fmtResult)) {
110
114
  throw new Error(`Formatting failed: ${fmtResult.value.message}`);
111
115
  }
112
- echo('✓ Formatting completed\n');
116
+ conditionalEcho('✓ Formatting completed\n');
113
117
  }
114
118
 
115
- echo('✅ Index file generation completed successfully!\n');
119
+ conditionalEcho('✅ Index file generation completed successfully!\n');
120
+
121
+ return Result.ok(undefined);
116
122
  } catch (error) {
117
- echo(`❌ Index generation failed: ${String(error)}\n`);
118
- throw error;
123
+ conditionalEcho(`❌ Index generation failed: ${String(error)}\n`);
124
+ return Result.err(error);
119
125
  }
120
126
  };
121
127
 
@@ -0,0 +1,266 @@
1
+ /* eslint-disable vitest/no-restricted-vi-methods */
2
+ import '../../node-global.mjs';
3
+ import { executeStages } from './execute-parallel.mjs';
4
+ import { getWorkspacePackages } from './get-workspace-packages.mjs';
5
+ import { runCmdInStagesAcrossWorkspaces } from './run-cmd-in-stages.mjs';
6
+ import { type Package } from './types.mjs';
7
+
8
+ // Mock the dependencies
9
+ vi.mock('./execute-parallel.mjs', () => ({
10
+ executeStages: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('./get-workspace-packages.mjs', () => ({
14
+ getWorkspacePackages: vi.fn(),
15
+ }));
16
+
17
+ describe('runCmdInStagesAcrossWorkspaces', () => {
18
+ type MockedSpies = Readonly<{
19
+ consoleLogSpy: ReturnType<typeof vi.spyOn>;
20
+ consoleErrorSpy: ReturnType<typeof vi.spyOn>;
21
+ processExitSpy: ReturnType<typeof vi.spyOn>;
22
+ }>;
23
+
24
+ const setupSpies = (): MockedSpies => {
25
+ vi.clearAllMocks();
26
+ const consoleLogSpy = vi
27
+ .spyOn(console, 'log')
28
+ .mockImplementation((): void => {});
29
+ const consoleErrorSpy = vi
30
+ .spyOn(console, 'error')
31
+ .mockImplementation((): void => {});
32
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
33
+ const processExitSpy = vi
34
+ .spyOn(process, 'exit')
35
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
36
+ .mockImplementation((): never => undefined as never) as ReturnType<
37
+ typeof vi.spyOn
38
+ >;
39
+
40
+ return { consoleLogSpy, consoleErrorSpy, processExitSpy };
41
+ };
42
+
43
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
44
+ const cleanupSpies = (spies: MockedSpies): void => {
45
+ spies.consoleLogSpy.mockRestore();
46
+ spies.consoleErrorSpy.mockRestore();
47
+ spies.processExitSpy.mockRestore();
48
+ };
49
+
50
+ test('should fail fast and exit immediately when executeStages throws an error', async () => {
51
+ const spies = setupSpies();
52
+
53
+ try {
54
+ // Mock workspace packages
55
+ const mockPackages: Package[] = [
56
+ {
57
+ name: 'package-a',
58
+ path: '/test/package-a',
59
+ packageJson: { name: 'package-a', scripts: { test: 'exit 1' } },
60
+ dependencies: {},
61
+ },
62
+ {
63
+ name: 'package-b',
64
+ path: '/test/package-b',
65
+ packageJson: { name: 'package-b', scripts: { test: 'echo success' } },
66
+ dependencies: {},
67
+ },
68
+ {
69
+ name: 'package-c',
70
+ path: '/test/package-c',
71
+ packageJson: { name: 'package-c', scripts: { test: 'echo success' } },
72
+ dependencies: {},
73
+ },
74
+ ];
75
+
76
+ vi.mocked(getWorkspacePackages).mockResolvedValue(mockPackages);
77
+
78
+ // Mock executeStages to throw an error (simulating a failed command)
79
+ const mockError = new Error('package-a exited with code 1');
80
+ vi.mocked(executeStages).mockRejectedValue(mockError);
81
+
82
+ // Record start time
83
+ const startTime = Date.now();
84
+
85
+ // Execute the function
86
+ await runCmdInStagesAcrossWorkspaces({
87
+ rootPackageJsonDir: '/test',
88
+ cmd: 'test',
89
+ concurrency: 2,
90
+ });
91
+
92
+ // Record end time
93
+ const endTime = Date.now();
94
+ const executionTime = endTime - startTime;
95
+
96
+ // Verify that execution was fast (fail-fast behavior)
97
+ // Should complete within 100ms since it should fail immediately
98
+ expect(executionTime).toBeLessThan(100);
99
+
100
+ // Verify executeStages was called
101
+ expect(executeStages).toHaveBeenCalledWith(mockPackages, 'test', 2);
102
+
103
+ // Verify console.error was called with fail-fast message
104
+ expect(spies.consoleErrorSpy).toHaveBeenCalledWith(
105
+ '\n❌ test failed (fail-fast mode stopped execution):',
106
+ );
107
+ expect(spies.consoleErrorSpy).toHaveBeenCalledWith(
108
+ 'package-a exited with code 1',
109
+ );
110
+
111
+ // Verify process.exit was called with code 1
112
+ expect(spies.processExitSpy).toHaveBeenCalledWith(1);
113
+
114
+ // Verify success message was NOT called
115
+ expect(spies.consoleLogSpy).not.toHaveBeenCalledWith(
116
+ expect.stringContaining('✅ test completed successfully'),
117
+ );
118
+ } finally {
119
+ cleanupSpies(spies);
120
+ }
121
+ });
122
+
123
+ test('should complete successfully when no errors occur', async () => {
124
+ const spies = setupSpies();
125
+
126
+ try {
127
+ // Mock workspace packages
128
+ const mockPackages: Package[] = [
129
+ {
130
+ name: 'package-a',
131
+ path: '/test/package-a',
132
+ packageJson: { name: 'package-a', scripts: { test: 'echo test' } },
133
+ dependencies: {},
134
+ },
135
+ {
136
+ name: 'package-b',
137
+ path: '/test/package-b',
138
+ packageJson: { name: 'package-b', scripts: { test: 'echo test' } },
139
+ dependencies: {},
140
+ },
141
+ ];
142
+
143
+ vi.mocked(getWorkspacePackages).mockResolvedValue(mockPackages);
144
+ vi.mocked(executeStages).mockResolvedValue(undefined);
145
+
146
+ // Execute the function
147
+ await runCmdInStagesAcrossWorkspaces({
148
+ rootPackageJsonDir: '/test',
149
+ cmd: 'test',
150
+ concurrency: 2,
151
+ });
152
+
153
+ // Verify executeStages was called
154
+ expect(executeStages).toHaveBeenCalledWith(mockPackages, 'test', 2);
155
+
156
+ // Verify success messages were called
157
+ expect(spies.consoleLogSpy).toHaveBeenCalledWith(
158
+ '\nStarting test across 2 packages (fail-fast mode)...',
159
+ );
160
+ expect(spies.consoleLogSpy).toHaveBeenCalledWith(
161
+ '\n✅ test completed successfully (all stages)',
162
+ );
163
+
164
+ // Verify process.exit was NOT called
165
+ expect(spies.processExitSpy).not.toHaveBeenCalled();
166
+
167
+ // Verify error messages were NOT called
168
+ expect(spies.consoleErrorSpy).not.toHaveBeenCalled();
169
+ } finally {
170
+ cleanupSpies(spies);
171
+ }
172
+ });
173
+
174
+ test('should apply package filtering correctly', async () => {
175
+ const spies = setupSpies();
176
+
177
+ try {
178
+ // Mock workspace packages
179
+ const mockPackages: Package[] = [
180
+ {
181
+ name: 'package-a',
182
+ path: '/test/package-a',
183
+ packageJson: { name: 'package-a', scripts: { test: 'echo test' } },
184
+ dependencies: {},
185
+ },
186
+ {
187
+ name: 'package-b',
188
+ path: '/test/package-b',
189
+ packageJson: { name: 'package-b', scripts: { test: 'echo test' } },
190
+ dependencies: {},
191
+ },
192
+ {
193
+ name: 'other-package',
194
+ path: '/test/other-package',
195
+ packageJson: {
196
+ name: 'other-package',
197
+ scripts: { test: 'echo test' },
198
+ },
199
+ dependencies: {},
200
+ },
201
+ ];
202
+
203
+ vi.mocked(getWorkspacePackages).mockResolvedValue(mockPackages);
204
+ vi.mocked(executeStages).mockResolvedValue(undefined);
205
+
206
+ // Filter to only packages starting with 'package-'
207
+ const filterFn = (name: string): boolean => name.startsWith('package-');
208
+
209
+ // Execute the function with filter
210
+ await runCmdInStagesAcrossWorkspaces({
211
+ rootPackageJsonDir: '/test',
212
+ cmd: 'test',
213
+ concurrency: 2,
214
+ filterWorkspacePattern: filterFn,
215
+ });
216
+
217
+ // Verify executeStages was called with filtered packages
218
+ const expectedFilteredPackages = mockPackages.filter((pkg) =>
219
+ pkg.name.startsWith('package-'),
220
+ );
221
+ expect(executeStages).toHaveBeenCalledWith(
222
+ expectedFilteredPackages,
223
+ 'test',
224
+ 2,
225
+ );
226
+
227
+ // Verify log shows correct package count
228
+ expect(spies.consoleLogSpy).toHaveBeenCalledWith(
229
+ '\nStarting test across 2 packages (fail-fast mode)...',
230
+ );
231
+ } finally {
232
+ cleanupSpies(spies);
233
+ }
234
+ });
235
+
236
+ test('should handle workspace package loading errors', async () => {
237
+ const spies = setupSpies();
238
+
239
+ try {
240
+ // Mock getWorkspacePackages to throw an error
241
+ const mockError = new Error('Failed to load workspace packages');
242
+ vi.mocked(getWorkspacePackages).mockRejectedValue(mockError);
243
+
244
+ // Execute the function
245
+ await runCmdInStagesAcrossWorkspaces({
246
+ rootPackageJsonDir: '/test',
247
+ cmd: 'test',
248
+ concurrency: 2,
249
+ });
250
+
251
+ // Verify executeStages was NOT called
252
+ expect(executeStages).not.toHaveBeenCalled();
253
+
254
+ // Verify error handling
255
+ expect(spies.consoleErrorSpy).toHaveBeenCalledWith(
256
+ '\n❌ test failed (fail-fast mode stopped execution):',
257
+ );
258
+ expect(spies.consoleErrorSpy).toHaveBeenCalledWith(
259
+ 'Failed to load workspace packages',
260
+ );
261
+ expect(spies.processExitSpy).toHaveBeenCalledWith(1);
262
+ } finally {
263
+ cleanupSpies(spies);
264
+ }
265
+ });
266
+ });
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env -S npx tsx
2
- export {};
3
- //# sourceMappingURL=format-untracked.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"format-untracked.d.mts","sourceRoot":"","sources":["../../src/cmd/format-untracked.mts"],"names":[],"mappings":""}
@@ -1,34 +0,0 @@
1
- #!/usr/bin/env -S npx tsx
2
- import * as cmd from 'cmd-ts';
3
- import 'ts-data-forge';
4
- import '../node-global.mjs';
5
- import 'node:child_process';
6
- import { formatUntracked } from '../functions/format.mjs';
7
- import 'micromatch';
8
- import 'child_process';
9
-
10
- const cmdDef = cmd.command({
11
- name: 'format-untracked-cli',
12
- version: '6.1.0',
13
- args: {
14
- silent: cmd.flag({
15
- long: 'silent',
16
- type: cmd.optional(cmd.boolean),
17
- description: 'If true, suppresses output messages (default: false)',
18
- }),
19
- },
20
- handler: (args) => {
21
- main(args).catch((error) => {
22
- console.error('An error occurred:', error);
23
- process.exit(1);
24
- });
25
- },
26
- });
27
- const main = async (args) => {
28
- const result = await formatUntracked({ silent: args.silent });
29
- if (result === 'err') {
30
- process.exit(1);
31
- }
32
- };
33
- await cmd.run(cmdDef, process.argv.slice(2));
34
- //# sourceMappingURL=format-untracked.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"format-untracked.mjs","sources":["../../src/cmd/format-untracked.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;;;;;;AAKA,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC;AACzB,IAAA,IAAI,EAAE,sBAAsB;AAC5B,IAAA,OAAO,EAAE,OAAO;AAChB,IAAA,IAAI,EAAE;AACJ,QAAA,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC;AACf,YAAA,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC;AAC/B,YAAA,WAAW,EAAE,sDAAsD;SACpE,CAAC;AACH,KAAA;AACD,IAAA,OAAO,EAAE,CAAC,IAAI,KAAI;QAChB,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,KAAI;AACzB,YAAA,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,KAAK,CAAC;AAC1C,YAAA,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AACjB,QAAA,CAAC,CAAC;IACJ,CAAC;AACF,CAAA,CAAC;AAEF,MAAM,IAAI,GAAG,OAAO,IAAoC,KAAmB;AACzE,IAAA,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AAC7D,IAAA,IAAI,MAAM,KAAK,KAAK,EAAE;AACpB,QAAA,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACjB;AACF,CAAC;AAED,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC"}
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env -S npx tsx
2
-
3
- import * as cmd from 'cmd-ts';
4
- import { formatUntracked } from '../functions/index.mjs';
5
-
6
- const cmdDef = cmd.command({
7
- name: 'format-untracked-cli',
8
- version: '6.1.0',
9
- args: {
10
- silent: cmd.flag({
11
- long: 'silent',
12
- type: cmd.optional(cmd.boolean),
13
- description: 'If true, suppresses output messages (default: false)',
14
- }),
15
- },
16
- handler: (args) => {
17
- main(args).catch((error) => {
18
- console.error('An error occurred:', error);
19
- process.exit(1);
20
- });
21
- },
22
- });
23
-
24
- const main = async (args: Readonly<{ silent?: boolean }>): Promise<void> => {
25
- const result = await formatUntracked({ silent: args.silent });
26
- if (result === 'err') {
27
- process.exit(1);
28
- }
29
- };
30
-
31
- await cmd.run(cmdDef, process.argv.slice(2));