ts-repo-utils 7.7.3 → 7.8.1
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 +197 -144
- package/dist/cmd/assert-repo-is-clean.mjs +1 -1
- package/dist/cmd/assert-repo-is-clean.mjs.map +1 -1
- package/dist/cmd/check-should-run-type-checks.mjs +1 -1
- package/dist/cmd/check-should-run-type-checks.mjs.map +1 -1
- package/dist/cmd/format-diff-from.mjs +1 -1
- package/dist/cmd/format-diff-from.mjs.map +1 -1
- package/dist/cmd/format-uncommitted.mjs +1 -1
- package/dist/cmd/format-uncommitted.mjs.map +1 -1
- package/dist/cmd/gen-index-ts.mjs +1 -1
- package/dist/cmd/gen-index-ts.mjs.map +1 -1
- package/dist/entry-point.mjs +2 -1
- package/dist/entry-point.mjs.map +1 -1
- package/dist/functions/assert-ext.d.mts +20 -1
- package/dist/functions/assert-ext.d.mts.map +1 -1
- package/dist/functions/assert-ext.mjs +52 -35
- package/dist/functions/assert-ext.mjs.map +1 -1
- package/dist/functions/create-result-assert.d.mts +18 -0
- package/dist/functions/create-result-assert.d.mts.map +1 -0
- package/dist/functions/create-result-assert.mjs +40 -0
- package/dist/functions/create-result-assert.mjs.map +1 -0
- package/dist/functions/exec-async.d.mts +5 -6
- package/dist/functions/exec-async.d.mts.map +1 -1
- package/dist/functions/exec-async.mjs +26 -7
- package/dist/functions/exec-async.mjs.map +1 -1
- package/dist/functions/format.d.mts.map +1 -1
- package/dist/functions/format.mjs +8 -2
- package/dist/functions/format.mjs.map +1 -1
- package/dist/functions/index.d.mts +1 -0
- package/dist/functions/index.d.mts.map +1 -1
- package/dist/functions/index.mjs +2 -1
- package/dist/functions/index.mjs.map +1 -1
- package/dist/functions/workspace-utils/execute-parallel.mjs.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/dist/node-global.d.mts +2 -0
- package/dist/node-global.d.mts.map +1 -1
- package/dist/node-global.mjs +3 -1
- package/dist/node-global.mjs.map +1 -1
- package/package.json +20 -14
- package/src/cmd/assert-repo-is-clean.mts +2 -2
- package/src/cmd/check-should-run-type-checks.mts +2 -2
- package/src/cmd/format-diff-from.mts +2 -2
- package/src/cmd/format-uncommitted.mts +2 -2
- package/src/cmd/gen-index-ts.mts +3 -3
- package/src/functions/assert-ext.mts +78 -52
- package/src/functions/create-result-assert.mts +59 -0
- package/src/functions/exec-async.mts +71 -14
- package/src/functions/exec-async.test.mts +5 -5
- package/src/functions/format.mts +11 -3
- package/src/functions/index.mts +1 -0
- package/src/functions/workspace-utils/execute-parallel.mts +1 -1
- package/src/functions/workspace-utils/run-cmd-in-stages.test.mts +5 -8
- package/src/node-global.mts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-repo-utils",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.8.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript"
|
|
@@ -84,34 +84,35 @@
|
|
|
84
84
|
"fast-glob": "^3.3.3",
|
|
85
85
|
"micromatch": "^4.0.8",
|
|
86
86
|
"prettier": "^3.6.2",
|
|
87
|
-
"ts-data-forge": "^3.3.
|
|
87
|
+
"ts-data-forge": "^3.3.1",
|
|
88
88
|
"tsx": "^4.20.6"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
|
-
"@octokit/core": "
|
|
92
|
-
"@rollup/plugin-replace": "^6.0.
|
|
91
|
+
"@octokit/core": "7.0.6",
|
|
92
|
+
"@rollup/plugin-replace": "^6.0.3",
|
|
93
93
|
"@rollup/plugin-strip": "^3.0.4",
|
|
94
|
-
"@rollup/plugin-typescript": "^12.
|
|
94
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
95
95
|
"@semantic-release/changelog": "^6.0.3",
|
|
96
96
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
97
97
|
"@semantic-release/exec": "^7.1.0",
|
|
98
98
|
"@semantic-release/git": "^10.0.1",
|
|
99
|
-
"@semantic-release/github": "^12.0.
|
|
99
|
+
"@semantic-release/github": "^12.0.1",
|
|
100
100
|
"@semantic-release/npm": "^13.1.1",
|
|
101
101
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
102
|
-
"@types/node": "^24.
|
|
103
|
-
"@vitest/coverage-v8": "^
|
|
104
|
-
"@vitest/ui": "^
|
|
102
|
+
"@types/node": "^24.10.0",
|
|
103
|
+
"@vitest/coverage-v8": "^4.0.6",
|
|
104
|
+
"@vitest/ui": "^4.0.6",
|
|
105
105
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
106
|
-
"cspell": "^9.2.
|
|
106
|
+
"cspell": "^9.2.2",
|
|
107
107
|
"dedent": "^1.7.0",
|
|
108
|
-
"eslint": "
|
|
109
|
-
"eslint-config-typed": "^1.
|
|
108
|
+
"eslint": "9.38.0",
|
|
109
|
+
"eslint-config-typed": "^3.1.1",
|
|
110
110
|
"fast-glob": "^3.3.3",
|
|
111
|
+
"jiti": "^2.6.1",
|
|
111
112
|
"markdownlint": "^0.39.0",
|
|
112
113
|
"markdownlint-cli2": "^0.18.1",
|
|
113
114
|
"npm-run-all2": "^8.0.4",
|
|
114
|
-
"octokit-safe-types": "^1.1.
|
|
115
|
+
"octokit-safe-types": "^1.1.2",
|
|
115
116
|
"prettier": "^3.6.2",
|
|
116
117
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
117
118
|
"prettier-plugin-packagejson": "^2.5.19",
|
|
@@ -120,12 +121,17 @@
|
|
|
120
121
|
"ts-fortress": "^5.2.0",
|
|
121
122
|
"ts-type-forge": "^2.3.0",
|
|
122
123
|
"tslib": "^2.8.1",
|
|
124
|
+
"tsx": "^4.20.6",
|
|
123
125
|
"typedoc": "^0.28.14",
|
|
124
126
|
"typedoc-plugin-markdown": "^4.9.0",
|
|
125
127
|
"typescript": "^5.9.3",
|
|
126
|
-
"vitest": "^
|
|
128
|
+
"vitest": "^4.0.6"
|
|
127
129
|
},
|
|
128
130
|
"peerDependencies": {
|
|
129
131
|
"prettier": "^3.6.2"
|
|
132
|
+
},
|
|
133
|
+
"volta": {
|
|
134
|
+
"node": "25.0.0",
|
|
135
|
+
"npm": "11.6.2"
|
|
130
136
|
}
|
|
131
137
|
}
|
|
@@ -5,7 +5,7 @@ import { assertRepoIsClean } from '../functions/index.mjs';
|
|
|
5
5
|
|
|
6
6
|
const cmdDef = cmd.command({
|
|
7
7
|
name: 'assert-repo-is-clean-cli',
|
|
8
|
-
version: '7.
|
|
8
|
+
version: '7.8.1',
|
|
9
9
|
args: {
|
|
10
10
|
silent: cmd.flag({
|
|
11
11
|
long: 'silent',
|
|
@@ -14,7 +14,7 @@ const cmdDef = cmd.command({
|
|
|
14
14
|
}),
|
|
15
15
|
},
|
|
16
16
|
handler: (args) => {
|
|
17
|
-
main(args).catch((error) => {
|
|
17
|
+
main(args).catch((error: unknown) => {
|
|
18
18
|
console.error('An error occurred:', error);
|
|
19
19
|
process.exit(1);
|
|
20
20
|
});
|
|
@@ -5,7 +5,7 @@ import { checkShouldRunTypeChecks } from '../functions/index.mjs';
|
|
|
5
5
|
|
|
6
6
|
const cmdDef = cmd.command({
|
|
7
7
|
name: 'check-should-run-type-checks-cli',
|
|
8
|
-
version: '7.
|
|
8
|
+
version: '7.8.1',
|
|
9
9
|
args: {
|
|
10
10
|
pathsIgnore: cmd.multioption({
|
|
11
11
|
long: 'paths-ignore',
|
|
@@ -21,7 +21,7 @@ const cmdDef = cmd.command({
|
|
|
21
21
|
}),
|
|
22
22
|
},
|
|
23
23
|
handler: (args) => {
|
|
24
|
-
main(args).catch((error) => {
|
|
24
|
+
main(args).catch((error: unknown) => {
|
|
25
25
|
console.error('An error occurred:', error);
|
|
26
26
|
process.exit(1);
|
|
27
27
|
});
|
|
@@ -6,7 +6,7 @@ import { formatDiffFrom } from '../functions/index.mjs';
|
|
|
6
6
|
|
|
7
7
|
const cmdDef = cmd.command({
|
|
8
8
|
name: 'format-diff-from-cli',
|
|
9
|
-
version: '7.
|
|
9
|
+
version: '7.8.1',
|
|
10
10
|
args: {
|
|
11
11
|
base: cmd.positional({
|
|
12
12
|
type: cmd.string,
|
|
@@ -50,7 +50,7 @@ const cmdDef = cmd.command({
|
|
|
50
50
|
excludeStaged: args.excludeStaged ?? false,
|
|
51
51
|
ignoreUnknown: args.ignoreUnknown ?? true,
|
|
52
52
|
silent: args.silent ?? false,
|
|
53
|
-
}).catch((error) => {
|
|
53
|
+
}).catch((error: unknown) => {
|
|
54
54
|
console.error('An error occurred:', error);
|
|
55
55
|
process.exit(1);
|
|
56
56
|
});
|
|
@@ -6,7 +6,7 @@ import { formatUncommittedFiles } from '../functions/index.mjs';
|
|
|
6
6
|
|
|
7
7
|
const cmdDef = cmd.command({
|
|
8
8
|
name: 'format-uncommitted-cli',
|
|
9
|
-
version: '7.
|
|
9
|
+
version: '7.8.1',
|
|
10
10
|
args: {
|
|
11
11
|
excludeUntracked: cmd.flag({
|
|
12
12
|
long: 'exclude-untracked',
|
|
@@ -44,7 +44,7 @@ const cmdDef = cmd.command({
|
|
|
44
44
|
excludeStaged: args.excludeStaged ?? false,
|
|
45
45
|
ignoreUnknown: args.ignoreUnknown ?? true,
|
|
46
46
|
silent: args.silent ?? false,
|
|
47
|
-
}).catch((error) => {
|
|
47
|
+
}).catch((error: unknown) => {
|
|
48
48
|
console.error('An error occurred:', error);
|
|
49
49
|
process.exit(1);
|
|
50
50
|
});
|
package/src/cmd/gen-index-ts.mts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as cmd from 'cmd-ts';
|
|
4
4
|
|
|
5
|
-
// eslint-disable-next-line import/no-internal-modules
|
|
5
|
+
// eslint-disable-next-line import-x/no-internal-modules
|
|
6
6
|
import { type InputOf, type OutputOf } from 'cmd-ts/dist/esm/from.js';
|
|
7
7
|
import { expectType } from 'ts-data-forge';
|
|
8
8
|
import { genIndex } from '../functions/index.mjs';
|
|
@@ -40,7 +40,7 @@ const nonEmptyArray = <T extends cmd.Type<any, any>>(
|
|
|
40
40
|
|
|
41
41
|
const cmdDef = cmd.command({
|
|
42
42
|
name: 'gen-index-ts-cli',
|
|
43
|
-
version: '7.
|
|
43
|
+
version: '7.8.1',
|
|
44
44
|
args: {
|
|
45
45
|
// required args
|
|
46
46
|
targetDirectory: cmd.positional({
|
|
@@ -109,7 +109,7 @@ const cmdDef = cmd.command({
|
|
|
109
109
|
expectType<typeof args.formatCommand, string | undefined>('=');
|
|
110
110
|
expectType<typeof args.silent, boolean | undefined>('=');
|
|
111
111
|
|
|
112
|
-
main(args).catch((error) => {
|
|
112
|
+
main(args).catch((error: unknown) => {
|
|
113
113
|
console.error('An error occurred:', error);
|
|
114
114
|
process.exit(1);
|
|
115
115
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Arr, type IMap, isString } from 'ts-data-forge';
|
|
1
|
+
import { Arr, type IMap, isString, Result } from 'ts-data-forge';
|
|
2
2
|
import '../node-global.mjs';
|
|
3
3
|
import { assertPathExists } from './assert-path-exists.mjs';
|
|
4
|
+
import { createResultAssert } from './create-result-assert.mjs';
|
|
4
5
|
|
|
5
6
|
/** Configuration for directory extension checking. */
|
|
6
7
|
export type CheckExtConfig = DeepReadonly<{
|
|
@@ -23,13 +24,21 @@ export type CheckExtConfig = DeepReadonly<{
|
|
|
23
24
|
}[];
|
|
24
25
|
}>;
|
|
25
26
|
|
|
27
|
+
export type CheckExtError = Readonly<{
|
|
28
|
+
message: string;
|
|
29
|
+
files: readonly string[];
|
|
30
|
+
}>;
|
|
31
|
+
|
|
26
32
|
/**
|
|
27
33
|
* Validates that all files in specified directories have the correct
|
|
28
|
-
* extensions.
|
|
34
|
+
* extensions.
|
|
29
35
|
*
|
|
30
36
|
* @param config - Configuration specifying directories and expected extensions.
|
|
37
|
+
* @returns Result.ok when all files pass, otherwise Result.err with details.
|
|
31
38
|
*/
|
|
32
|
-
export const
|
|
39
|
+
export const checkExt = async (
|
|
40
|
+
config: CheckExtConfig,
|
|
41
|
+
): Promise<Result<undefined, CheckExtError>> => {
|
|
33
42
|
// Check all directories in parallel
|
|
34
43
|
const results = await Promise.all(
|
|
35
44
|
config.directories.map(async ({ path: dir, extension, ignorePatterns }) => {
|
|
@@ -49,59 +58,43 @@ export const assertExt = async (config: CheckExtConfig): Promise<void> => {
|
|
|
49
58
|
// Collect all incorrect files
|
|
50
59
|
const allIncorrectFiles: readonly string[] = results.flat();
|
|
51
60
|
|
|
52
|
-
if (allIncorrectFiles.length
|
|
53
|
-
|
|
54
|
-
// Group directories by extension for a cleaner message
|
|
55
|
-
const extensionGroups: IMap<
|
|
56
|
-
string,
|
|
57
|
-
readonly Readonly<{
|
|
58
|
-
relativePath: string;
|
|
59
|
-
extKey: string;
|
|
60
|
-
}>[]
|
|
61
|
-
> = Arr.groupBy(
|
|
62
|
-
config.directories.map(({ path: dirPath, extension }) => {
|
|
63
|
-
const relativePath = path.relative(process.cwd(), dirPath);
|
|
64
|
-
const extKey = isString(extension)
|
|
65
|
-
? extension
|
|
66
|
-
: extension.join(' or ');
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
relativePath,
|
|
70
|
-
extKey,
|
|
71
|
-
};
|
|
72
|
-
}),
|
|
73
|
-
({ extKey }) => extKey,
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
// Generate message parts for each extension
|
|
77
|
-
const messageParts = Array.from(
|
|
78
|
-
extensionGroups.entries(),
|
|
79
|
-
([ext, dirs]) => {
|
|
80
|
-
const dirList =
|
|
81
|
-
dirs.length === 1
|
|
82
|
-
? dirs[0]?.relativePath
|
|
83
|
-
: dirs.map((d) => d.relativePath).join(', ');
|
|
84
|
-
return `${dirList} should have ${ext} extension`;
|
|
85
|
-
},
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
return `All files in ${messageParts.join(' and ')}.`;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const errorMessage = [
|
|
92
|
-
'Files with incorrect extensions found:',
|
|
93
|
-
...allIncorrectFiles.map((file) => ` - ${file}`),
|
|
94
|
-
'',
|
|
95
|
-
generateErrorMessage(),
|
|
96
|
-
].join('\n');
|
|
97
|
-
|
|
98
|
-
echo(errorMessage);
|
|
99
|
-
process.exit(1);
|
|
61
|
+
if (allIncorrectFiles.length === 0) {
|
|
62
|
+
return Result.ok(undefined);
|
|
100
63
|
}
|
|
101
64
|
|
|
102
|
-
|
|
65
|
+
const message = [
|
|
66
|
+
'Files with incorrect extensions found:',
|
|
67
|
+
...allIncorrectFiles.map((file) => ` - ${file}`),
|
|
68
|
+
'',
|
|
69
|
+
describeExpectedExtensions(config),
|
|
70
|
+
].join('\n');
|
|
71
|
+
|
|
72
|
+
return Result.err({
|
|
73
|
+
message,
|
|
74
|
+
files: allIncorrectFiles,
|
|
75
|
+
});
|
|
103
76
|
};
|
|
104
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Validates that all files in specified directories have the correct
|
|
80
|
+
* extensions. Exits with code 1 if any files have incorrect extensions.
|
|
81
|
+
*
|
|
82
|
+
* @param config - Configuration specifying directories and expected extensions.
|
|
83
|
+
*/
|
|
84
|
+
export const assertExt = createResultAssert<
|
|
85
|
+
CheckExtConfig,
|
|
86
|
+
undefined,
|
|
87
|
+
CheckExtError
|
|
88
|
+
>({
|
|
89
|
+
run: checkExt,
|
|
90
|
+
onError: (error) => {
|
|
91
|
+
echo(error.message);
|
|
92
|
+
},
|
|
93
|
+
onSuccess: () => {
|
|
94
|
+
echo('✓ All files have correct extensions');
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
105
98
|
/**
|
|
106
99
|
* Checks if all files in a directory have the expected extension.
|
|
107
100
|
*
|
|
@@ -134,3 +127,36 @@ const getFilesWithIncorrectExtension = async (
|
|
|
134
127
|
(file) => !expectedExtensions.some((ext) => file.endsWith(ext)),
|
|
135
128
|
);
|
|
136
129
|
};
|
|
130
|
+
|
|
131
|
+
const describeExpectedExtensions = (config: CheckExtConfig): string => {
|
|
132
|
+
// Group directories by extension for a cleaner message
|
|
133
|
+
const extensionGroups: IMap<
|
|
134
|
+
string,
|
|
135
|
+
readonly Readonly<{
|
|
136
|
+
relativePath: string;
|
|
137
|
+
extKey: string;
|
|
138
|
+
}>[]
|
|
139
|
+
> = Arr.groupBy(
|
|
140
|
+
config.directories.map(({ path: dirPath, extension }) => {
|
|
141
|
+
const relativePath = path.relative(process.cwd(), dirPath);
|
|
142
|
+
const extKey = isString(extension) ? extension : extension.join(' or ');
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
relativePath,
|
|
146
|
+
extKey,
|
|
147
|
+
};
|
|
148
|
+
}),
|
|
149
|
+
({ extKey }) => extKey,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Generate message parts for each extension
|
|
153
|
+
const messageParts = Array.from(extensionGroups.entries(), ([ext, dirs]) => {
|
|
154
|
+
const dirList =
|
|
155
|
+
dirs.length === 1
|
|
156
|
+
? dirs[0]?.relativePath
|
|
157
|
+
: dirs.map((d) => d.relativePath).join(', ');
|
|
158
|
+
return `${dirList} should have ${ext} extension`;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return `All files in ${messageParts.join(' and ')}.`;
|
|
162
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { hasKey, isRecord, isString, Result } from 'ts-data-forge';
|
|
2
|
+
import '../node-global.mjs';
|
|
3
|
+
|
|
4
|
+
type ResultProducer<TConfig, TOk, TErr> = (
|
|
5
|
+
config: TConfig,
|
|
6
|
+
) => Promise<Result<TOk, TErr>>;
|
|
7
|
+
|
|
8
|
+
export type CreateResultAssertOptions<Config, Ok, Err> = Readonly<{
|
|
9
|
+
run: ResultProducer<Config, Ok, Err>;
|
|
10
|
+
onSuccess?: (value: Ok, config: Config) => void | Promise<void>;
|
|
11
|
+
onError?: (error: Err, config: Config) => void | Promise<void>;
|
|
12
|
+
exitCode?: number;
|
|
13
|
+
}>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts a function that returns a Result into an assert-style variant that
|
|
17
|
+
* exits the process when the Result is Err. This is useful for building CLI
|
|
18
|
+
* commands that should stop execution on failure but remain composable when a
|
|
19
|
+
* Result is preferred.
|
|
20
|
+
*/
|
|
21
|
+
export const createResultAssert = <Config, Ok, Err>({
|
|
22
|
+
run,
|
|
23
|
+
onSuccess,
|
|
24
|
+
onError,
|
|
25
|
+
exitCode = 1,
|
|
26
|
+
}: CreateResultAssertOptions<Config, Ok, Err>): ((
|
|
27
|
+
config: Config,
|
|
28
|
+
) => Promise<Ok>) => {
|
|
29
|
+
const defaultOnError = (error: Err): void => {
|
|
30
|
+
if (
|
|
31
|
+
isRecord(error) &&
|
|
32
|
+
hasKey(error, 'message') &&
|
|
33
|
+
isString(error.message)
|
|
34
|
+
) {
|
|
35
|
+
echo(error.message);
|
|
36
|
+
} else {
|
|
37
|
+
console.error(error);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return async (config: Config): Promise<Ok> => {
|
|
42
|
+
const result = await run(config);
|
|
43
|
+
|
|
44
|
+
if (Result.isErr(result)) {
|
|
45
|
+
if (onError !== undefined) {
|
|
46
|
+
await onError(result.value, config);
|
|
47
|
+
} else {
|
|
48
|
+
defaultOnError(result.value);
|
|
49
|
+
}
|
|
50
|
+
process.exit(exitCode);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (onSuccess !== undefined) {
|
|
54
|
+
await onSuccess(result.value, config);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result.value;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -5,6 +5,18 @@ type ExecOptionsCustom = Readonly<{
|
|
|
5
5
|
silent?: boolean;
|
|
6
6
|
}>;
|
|
7
7
|
|
|
8
|
+
type ExecOptionsWithStringEncoding = Readonly<
|
|
9
|
+
childProcess.ExecOptionsWithStringEncoding & ExecOptionsCustom
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
type ExecOptionsWithBufferEncoding = Readonly<
|
|
13
|
+
childProcess.ExecOptionsWithBufferEncoding & ExecOptionsCustom
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
type NormalizedExecOptions = Readonly<
|
|
17
|
+
childProcess.ExecOptions & { encoding?: BufferEncoding | 'buffer' | null }
|
|
18
|
+
>;
|
|
19
|
+
|
|
8
20
|
export type ExecOptions = childProcess.ExecOptions & ExecOptionsCustom;
|
|
9
21
|
|
|
10
22
|
export type ExecResult<T extends string | Buffer> = Result<
|
|
@@ -23,47 +35,92 @@ export type ExecResult<T extends string | Buffer> = Result<
|
|
|
23
35
|
export function $(
|
|
24
36
|
command: string,
|
|
25
37
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
26
|
-
options?:
|
|
27
|
-
| ExecOptionsCustom
|
|
28
|
-
| Readonly<{ encoding: BufferEncoding } & ExecOptions>,
|
|
38
|
+
options?: ExecOptionsWithStringEncoding,
|
|
29
39
|
): Promise<ExecResult<string>>;
|
|
30
40
|
|
|
31
41
|
export function $(
|
|
32
42
|
command: string,
|
|
33
43
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
34
|
-
options
|
|
44
|
+
options: ExecOptionsWithBufferEncoding,
|
|
35
45
|
): Promise<ExecResult<Buffer>>;
|
|
36
46
|
|
|
47
|
+
export function $<
|
|
48
|
+
TOptions extends
|
|
49
|
+
| ExecOptionsWithBufferEncoding
|
|
50
|
+
| ExecOptionsWithStringEncoding
|
|
51
|
+
| undefined = undefined,
|
|
52
|
+
>(
|
|
53
|
+
command: string,
|
|
54
|
+
options?: TOptions,
|
|
55
|
+
): Promise<
|
|
56
|
+
ExecResult<TOptions extends ExecOptionsWithBufferEncoding ? Buffer : string>
|
|
57
|
+
>;
|
|
58
|
+
|
|
37
59
|
export function $(
|
|
38
60
|
command: string,
|
|
39
61
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
40
|
-
options?:
|
|
41
|
-
{ encoding?: BufferEncoding | 'buffer' | null } & ExecOptions
|
|
42
|
-
>,
|
|
62
|
+
options?: ExecOptionsWithStringEncoding | ExecOptionsWithBufferEncoding,
|
|
43
63
|
): Promise<ExecResult<string | Buffer>> {
|
|
44
64
|
const { silent = false, ...restOptions } = options ?? {};
|
|
65
|
+
const normalizedOptions: NormalizedExecOptions = restOptions;
|
|
45
66
|
|
|
46
67
|
if (!silent) {
|
|
47
68
|
echo(`$ ${command}`);
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
return new Promise((resolve) => {
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
const handleResult = <T extends string | Buffer>(
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
74
|
+
error: childProcess.ExecException | null,
|
|
75
|
+
stdout: T,
|
|
76
|
+
stderr: T,
|
|
77
|
+
): void => {
|
|
53
78
|
if (!silent) {
|
|
54
|
-
if (stdout
|
|
79
|
+
if (!isEmpty(stdout)) {
|
|
55
80
|
echo(stdout);
|
|
56
81
|
}
|
|
57
|
-
if (stderr
|
|
82
|
+
if (!isEmpty(stderr)) {
|
|
58
83
|
console.error(stderr);
|
|
59
84
|
}
|
|
60
85
|
}
|
|
61
86
|
|
|
62
87
|
if (error !== null) {
|
|
63
88
|
resolve(Result.err(error));
|
|
64
|
-
|
|
65
|
-
resolve(Result.ok({ stdout, stderr }));
|
|
89
|
+
return;
|
|
66
90
|
}
|
|
67
|
-
|
|
91
|
+
|
|
92
|
+
resolve(
|
|
93
|
+
Result.ok<Readonly<{ stdout: T; stderr: T }>>({ stdout, stderr }),
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const encoding = normalizedOptions.encoding;
|
|
98
|
+
|
|
99
|
+
if (encoding === 'buffer' || encoding === null) {
|
|
100
|
+
// eslint-disable-next-line security/detect-child-process
|
|
101
|
+
childProcess.exec(
|
|
102
|
+
command,
|
|
103
|
+
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
104
|
+
normalizedOptions as childProcess.ExecOptionsWithBufferEncoding,
|
|
105
|
+
(error, stdout, stderr) => {
|
|
106
|
+
handleResult(error, stdout, stderr);
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// eslint-disable-next-line security/detect-child-process
|
|
113
|
+
childProcess.exec(
|
|
114
|
+
command,
|
|
115
|
+
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
116
|
+
normalizedOptions as childProcess.ExecOptionsWithStringEncoding,
|
|
117
|
+
(error, stdout, stderr) => {
|
|
118
|
+
handleResult(error, stdout, stderr);
|
|
119
|
+
},
|
|
120
|
+
);
|
|
68
121
|
});
|
|
69
122
|
}
|
|
123
|
+
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
125
|
+
const isEmpty = (value: string | Buffer): boolean =>
|
|
126
|
+
typeof value === 'string' ? value === '' : value.length === 0;
|
|
@@ -368,8 +368,8 @@ describe('exec-async', () => {
|
|
|
368
368
|
// Type check for native exec with buffer encoding
|
|
369
369
|
exec('echo "test"', { encoding: 'buffer' }, (error, stdout, stderr) => {
|
|
370
370
|
expectType<ExecException | null, typeof error>('=');
|
|
371
|
-
expectType<Buffer, typeof stdout>('
|
|
372
|
-
expectType<Buffer, typeof stderr>('
|
|
371
|
+
expectType<Buffer, typeof stdout>('>=');
|
|
372
|
+
expectType<Buffer, typeof stderr>('>=');
|
|
373
373
|
});
|
|
374
374
|
|
|
375
375
|
// The $ function should produce the same types wrapped in Result (suppressed for clean output)
|
|
@@ -381,15 +381,15 @@ describe('exec-async', () => {
|
|
|
381
381
|
Promise<
|
|
382
382
|
Result<Readonly<{ stdout: Buffer; stderr: Buffer }>, ExecException>
|
|
383
383
|
>
|
|
384
|
-
>('
|
|
384
|
+
>('~=');
|
|
385
385
|
});
|
|
386
386
|
|
|
387
387
|
test('should match exec callback types for null encoding', () => {
|
|
388
388
|
// Type check for native exec with null encoding
|
|
389
389
|
exec('echo "test"', { encoding: null }, (error, stdout, stderr) => {
|
|
390
390
|
expectType<ExecException | null, typeof error>('=');
|
|
391
|
-
expectType<Buffer, typeof stdout>('
|
|
392
|
-
expectType<Buffer, typeof stderr>('
|
|
391
|
+
expectType<Buffer, typeof stdout>('>=');
|
|
392
|
+
expectType<Buffer, typeof stderr>('>=');
|
|
393
393
|
});
|
|
394
394
|
|
|
395
395
|
// The $ function should produce the same types wrapped in Result (suppressed for clean output)
|
package/src/functions/format.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ExecException } from 'node:child_process';
|
|
2
2
|
import * as prettier from 'prettier';
|
|
3
|
-
import { Arr, isNotUndefined, Result } from 'ts-data-forge';
|
|
3
|
+
import { Arr, isNotUndefined, pipe, Result } from 'ts-data-forge';
|
|
4
4
|
import '../node-global.mjs';
|
|
5
5
|
import {
|
|
6
6
|
getDiffFrom,
|
|
@@ -127,21 +127,28 @@ const defaultIgnoreFn = (filePath: string): boolean => {
|
|
|
127
127
|
ignoreFiles.has(filename) ||
|
|
128
128
|
filename.startsWith('.env') ||
|
|
129
129
|
ignoreExtensions.some((ext) => filePath.endsWith(ext)) ||
|
|
130
|
-
|
|
130
|
+
pipe(filePath.split(path.sep)).map((pathSegments) =>
|
|
131
|
+
pathSegments.some((segment) => ignoreDirs.includes(segment)),
|
|
132
|
+
).value
|
|
131
133
|
);
|
|
132
134
|
};
|
|
133
135
|
|
|
134
136
|
const ignoreFiles: ReadonlySet<string> = new Set([
|
|
135
137
|
'.DS_Store',
|
|
136
138
|
'package-lock.json',
|
|
139
|
+
'yarn.lock',
|
|
140
|
+
'pnpm-lock.yaml',
|
|
137
141
|
'LICENSE',
|
|
138
142
|
'.prettierignore',
|
|
139
143
|
'.editorconfig',
|
|
140
144
|
'.gitignore',
|
|
141
145
|
'.npmignore',
|
|
146
|
+
'.envrc',
|
|
147
|
+
'.nvmrc',
|
|
148
|
+
'.npmrc',
|
|
142
149
|
]);
|
|
143
150
|
|
|
144
|
-
const ignoreExtensions: readonly string[] = [
|
|
151
|
+
const ignoreExtensions: readonly `.${string}`[] = [
|
|
145
152
|
'.svg',
|
|
146
153
|
'.png',
|
|
147
154
|
'.jpg',
|
|
@@ -171,6 +178,7 @@ const ignoreDirs: readonly string[] = [
|
|
|
171
178
|
'.cache',
|
|
172
179
|
'.vscode',
|
|
173
180
|
'.yarn',
|
|
181
|
+
'.wireit',
|
|
174
182
|
] as const;
|
|
175
183
|
|
|
176
184
|
/**
|
package/src/functions/index.mts
CHANGED
|
@@ -57,7 +57,7 @@ export const executeParallel = async (
|
|
|
57
57
|
|
|
58
58
|
const wrappedPromise = promise
|
|
59
59
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
|
60
|
-
.catch((error) => {
|
|
60
|
+
.catch((error: unknown) => {
|
|
61
61
|
mut_failed = true;
|
|
62
62
|
throw error; // Re-throw to ensure fail-fast propagation
|
|
63
63
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable vitest/no-restricted-vi-methods */
|
|
2
|
+
import { type MockInstance } from 'vitest';
|
|
2
3
|
import '../../node-global.mjs';
|
|
3
4
|
import { executeStages } from './execute-parallel.mjs';
|
|
4
5
|
import { getWorkspacePackages } from './get-workspace-packages.mjs';
|
|
@@ -16,9 +17,9 @@ vi.mock('./get-workspace-packages.mjs', () => ({
|
|
|
16
17
|
|
|
17
18
|
describe('runCmdInStagesAcrossWorkspaces', () => {
|
|
18
19
|
type MockedSpies = Readonly<{
|
|
19
|
-
consoleLogSpy:
|
|
20
|
-
consoleErrorSpy:
|
|
21
|
-
processExitSpy:
|
|
20
|
+
consoleLogSpy: MockInstance<typeof console.log>;
|
|
21
|
+
consoleErrorSpy: MockInstance<typeof console.error>;
|
|
22
|
+
processExitSpy: MockInstance<typeof process.exit>;
|
|
22
23
|
}>;
|
|
23
24
|
|
|
24
25
|
const setupSpies = (): MockedSpies => {
|
|
@@ -30,14 +31,10 @@ describe('runCmdInStagesAcrossWorkspaces', () => {
|
|
|
30
31
|
.spyOn(console, 'error')
|
|
31
32
|
.mockImplementation((): void => {});
|
|
32
33
|
|
|
33
|
-
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
34
34
|
const processExitSpy = vi
|
|
35
35
|
.spyOn(process, 'exit')
|
|
36
|
-
|
|
37
36
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
38
|
-
.mockImplementation((): never => undefined as never)
|
|
39
|
-
typeof vi.spyOn
|
|
40
|
-
>;
|
|
37
|
+
.mockImplementation((): never => undefined as never);
|
|
41
38
|
|
|
42
39
|
return { consoleLogSpy, consoleErrorSpy, processExitSpy };
|
|
43
40
|
};
|
package/src/node-global.mts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
/* eslint-disable import/no-internal-modules */
|
|
1
|
+
/* eslint-disable import-x/no-internal-modules */
|
|
2
2
|
import glob_ from 'fast-glob';
|
|
3
3
|
import * as fs_ from 'node:fs/promises';
|
|
4
4
|
import * as os_ from 'node:os';
|
|
5
5
|
import * as path_ from 'node:path';
|
|
6
|
+
import { chdir as chdir_ } from 'node:process';
|
|
6
7
|
import { Result as Result_ } from 'ts-data-forge';
|
|
7
8
|
import { $ as $_ } from './functions/exec-async.mjs';
|
|
8
9
|
import { isDirectlyExecuted as isDirectlyExecuted_ } from './functions/is-directly-executed.mjs';
|
|
@@ -19,6 +20,7 @@ const globalsDef = {
|
|
|
19
20
|
$: $_,
|
|
20
21
|
Result: Result_,
|
|
21
22
|
echo: console.log,
|
|
23
|
+
cd: chdir_,
|
|
22
24
|
isDirectlyExecuted: isDirectlyExecuted_,
|
|
23
25
|
} as const;
|
|
24
26
|
|
|
@@ -35,5 +37,6 @@ declare global {
|
|
|
35
37
|
const Result: typeof Result_;
|
|
36
38
|
type Result<S, E> = Result_<S, E>;
|
|
37
39
|
const echo: typeof console.log;
|
|
40
|
+
const cd: typeof chdir_;
|
|
38
41
|
const isDirectlyExecuted: typeof isDirectlyExecuted_;
|
|
39
42
|
}
|