-
-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathcli.js
More file actions
executable file
·336 lines (296 loc) · 10.8 KB
/
cli.js
File metadata and controls
executable file
·336 lines (296 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
/* eslint-disable no-console, unicorn/no-process-exit */
import pathModule from 'node:path';
import { createRequire } from 'node:module';
import chalk from 'chalk';
import { formatHelpMessage, peowly } from 'peowly';
import { messageWithCauses, stackWithCauses } from 'pony-cause';
import { installedCheck, ROOT } from 'installed-check-core';
import resolveWorkspaceRootPkg from 'resolve-workspace-root';
const { resolveWorkspaceRootAsync } = resolveWorkspaceRootPkg;
// createRequire is needed to load package.json in ESM context
// @ts-expect-error - TS doesn't recognize that require is used below
const require = createRequire(import.meta.url);
const pkg = require('./package.json');
const EXIT_CODE_ERROR_RESULT = 1;
const EXIT_CODE_INVALID_INPUT = 2;
const EXIT_CODE_UNEXPECTED_ERROR = 4;
/**
* Log a debug message to stderr if debug mode is enabled
*
* @param {boolean | undefined} debug
* @param {string} label
* @param {string} message
*/
function debugLog (debug, label, message) {
if (debug) {
console.error(chalk.blue(label + ':') + ' ' + message);
}
}
const baseFlags = /** @satisfies {Record<string, import('peowly').AnyFlag>} */ ({
debug: {
type: 'boolean',
'default': false,
description: 'Prints debug info',
},
verbose: {
'short': 'v',
type: 'boolean',
'default': false,
description: 'Shows warnings',
},
});
const checkFlags = /** @satisfies {Record<string, import('peowly').AnyFlag & { listGroup: 'Checks' }>} */ ({
'engine-check': {
'short': 'e',
type: 'boolean',
'default': false,
description: 'Override default checks and explicitly request an engine range check',
listGroup: 'Checks',
},
'peer-check': {
'short': 'p',
type: 'boolean',
'default': false,
description: 'Override default checks and explicitly request a peer dependency range check',
listGroup: 'Checks',
},
'version-check': {
'short': 'c',
type: 'boolean',
'default': false,
description: 'Override default checks and explicitly request a check of installed versions',
listGroup: 'Checks',
},
});
const checkOptionFlags = /** @satisfies {Record<string, import('peowly').AnyFlag & { listGroup: 'Check options' }>} */ ({
ignore: {
'short': 'i',
type: 'string',
multiple: true,
description: 'Excludes the named dependency from non-version checks (Supports globs)',
listGroup: 'Check options',
},
'ignore-dev': {
'short': 'd',
type: 'boolean',
'default': false,
description: 'Excludes dev dependencies from non-version checks',
listGroup: 'Check options',
},
strict: {
'short': 's',
type: 'boolean',
'default': false,
description: 'Treat warnings as errors',
listGroup: 'Check options',
},
});
const fixFlags = /** @satisfies {Record<string, import('peowly').AnyFlag & { listGroup: 'Fix options' }>} */ ({
fix: {
type: 'boolean',
'default': false,
description: 'Tries to apply all suggestions and write them back to disk',
listGroup: 'Fix options',
},
});
const workspaceFlags = /** @satisfies {Record<string, import('peowly').AnyFlag & { listGroup: 'Workspace options' }>} */ ({
'no-include-workspace-root': {
type: 'boolean',
'default': false,
description: 'Excludes the workspace root package',
listGroup: 'Workspace options',
},
'no-parent-workspace': {
type: 'boolean',
'default': false,
description: 'Disables detection and use of parent workspace root for module resolution',
listGroup: 'Workspace options',
},
'no-workspaces': {
type: 'boolean',
'default': false,
description: 'Excludes workspace packages',
listGroup: 'Workspace options',
},
workspace: {
'short': 'w',
type: 'string',
multiple: true,
description: 'Excludes all workspace packages not matching these names / paths',
listGroup: 'Workspace options',
},
'workspace-ignore': {
type: 'string',
multiple: true,
description: 'Excludes the specified paths from workspace lookup (Supports globs)',
listGroup: 'Workspace options',
},
});
const deprecatedFlags = /** @satisfies {Record<string, import('peowly').AnyFlag & { listGroup: 'Deprecated options' }>} */ ({
'engine-ignore': {
type: 'string',
multiple: true,
description: 'Deprecated: use --ignore instead',
listGroup: 'Deprecated options',
},
'engine-no-dev': {
type: 'boolean',
'default': false,
description: 'Deprecated: use --ignore-dev instead',
listGroup: 'Deprecated options',
},
});
const flags = /** @satisfies {import('peowly').AnyFlags} */ ({
...baseFlags,
...checkFlags,
...checkOptionFlags,
...fixFlags,
...workspaceFlags,
...deprecatedFlags,
});
const cli = peowly({
options: flags,
help: formatHelpMessage('installed-check', {
flags,
usage: '<path to module folder>',
}),
name: 'installed-check',
pkg,
});
if (cli.input.length > 1) {
console.error(chalk.bgRed('Invalid input:') + ` Can only handle a single folder path, but received ${cli.input.length} paths: "${cli.input.join('", "')}"` + '\n');
process.exit(EXIT_CODE_INVALID_INPUT);
}
const {
debug,
'engine-check': engineCheck,
'engine-ignore': engineIgnore, // deprecated
'engine-no-dev': engineNoDev, // deprecated
fix,
'peer-check': peerCheck,
strict,
verbose,
'version-check': versionCheck,
workspace,
'workspace-ignore': workspaceIgnore,
} = cli.flags;
let {
ignore,
'ignore-dev': ignoreDev,
} = cli.flags;
const includeWorkspaceRoot = !cli.flags['no-include-workspace-root'];
const parentWorkspace = !cli.flags['no-parent-workspace'];
const workspaces = !cli.flags['no-workspaces'];
// Handle deprecated flags
if (engineIgnore?.length) {
ignore = [...ignore || [], ...engineIgnore];
console.error(chalk.bgRed.black('DEPRECATED:') + ' --engine-ignore is replace by --ignore');
}
if (engineNoDev) {
ignoreDev = engineNoDev;
console.error(chalk.bgRed.black('DEPRECATED:') + ' --engine-no-dev is replace by --ignore-dev');
}
/** @type {import('installed-check-core').InstalledChecks[]} */
let checks = [
...engineCheck ? /** @type {const} */ (['engine']) : [],
...peerCheck ? /** @type {const} */ (['peer']) : [],
...versionCheck ? /** @type {const} */ (['version']) : [],
];
// Detect if we're in a workspace within a larger monorepo
// If so, use the parent workspace root to enable access to parent's node_modules
const requestedCwd = pathModule.resolve(cli.input[0] || process.cwd());
let resolvedCwd = requestedCwd;
let workspaceFilter = workspace;
let resolvedIncludeWorkspaceRoot = includeWorkspaceRoot;
// Only detect parent workspace if:
// - User hasn't explicitly opted out with --no-parent-workspace
// - User hasn't provided explicit workspace filters (which would be incompatible)
if (parentWorkspace && !workspace?.length) {
debugLog(debug, 'Parent workspace detection', 'Attempting to resolve parent workspace root');
const parentWorkspaceRoot = await resolveWorkspaceRootAsync(requestedCwd);
if (parentWorkspaceRoot) {
debugLog(debug, 'Parent workspace detection', 'Found parent workspace root: ' + parentWorkspaceRoot);
} else {
debugLog(debug, 'Parent workspace detection', 'No parent workspace root found');
}
// If we found a parent workspace root different from our requested cwd,
// we're in a workspace situation
if (parentWorkspaceRoot && parentWorkspaceRoot !== requestedCwd) {
// Use the parent workspace root as cwd to get access to its node_modules
resolvedCwd = parentWorkspaceRoot;
// Filter to just the current workspace to avoid checking all workspaces in the parent monorepo
workspaceFilter = [requestedCwd];
// Don't include the workspace root (parent) in checks, only the filtered workspace
resolvedIncludeWorkspaceRoot = false;
debugLog(debug, 'Parent workspace detection', 'Using parent workspace root, filtering to current workspace');
} else if (parentWorkspaceRoot === requestedCwd) {
debugLog(debug, 'Parent workspace detection', 'Parent workspace root is same as requested cwd, not applying');
}
} else if (debug) {
/** @type {string[]} */
const reasons = [];
if (!parentWorkspace) reasons.push('--no-parent-workspace flag is set');
if (workspace?.length) reasons.push('explicit workspace filters provided');
debugLog(debug, 'Parent workspace detection', 'Skipped (' + reasons.join(', ') + ')');
}
/** @type {import('installed-check-core').LookupOptions} */
const lookupOptions = {
cwd: resolvedCwd,
ignorePaths: workspaceIgnore,
includeWorkspaceRoot: resolvedIncludeWorkspaceRoot,
skipWorkspaces: !workspaces,
workspace: workspaceFilter,
};
/** @type {import('installed-check-core').InstalledCheckOptions} */
const checkOptions = {
noDev: ignoreDev,
ignore,
strict,
};
if (checks.length === 0) {
checks = ['engine', 'peer', 'version'];
}
if (debug) {
const { inspect } = await import('node:util');
debugLog(debug, 'Checks', inspect(checks, { colors: true, compact: true }));
debugLog(debug, 'Lookup options', inspect(lookupOptions, { colors: true, compact: true }));
debugLog(debug, 'Check options', inspect(checkOptions, { colors: true, compact: true }));
}
try {
const result = await installedCheck(checks, lookupOptions, { ...checkOptions, fix });
if (verbose && result.warnings.length) {
console.log('\n' + chalk.bgYellow.black('Warnings:') + '\n\n' + result.warnings.join('\n') + '\n');
} else if (result.errors.length) {
console.log('');
}
if (result.errors.length) {
console.error(chalk.bgRed.black('Errors:') + '\n\n' + result.errors.join('\n') + '\n');
}
if (result.suggestions.length) {
console.error(chalk.bgCyanBright.black('Suggestions:') + '\n\n' + result.suggestions.join('\n') + '\n');
}
const workspaceSuccess = /** @type {const} */ ([
...Object.entries(result.workspaceSuccess),
...(result.workspaceSuccess[ROOT] === undefined ? [] : /** @type {const} */ ([['root', result.workspaceSuccess[ROOT]]])),
]);
if (verbose && workspaceSuccess.length) {
if (result.errors.length === 0 && workspaceSuccess.length === 1 && result.workspaceSuccess[ROOT]) {
console.log(chalk.bgGreen.black('Successful!') + '\n');
} else {
const success = workspaceSuccess.filter(([, value]) => value);
const failure = workspaceSuccess.filter(([, value]) => !value);
if (success.length) {
console.log(chalk.bgGreen.black('Successful workspaces:') + ' ' + success.map(([key]) => key).join(', ') + '\n');
}
if (failure.length) {
console.log(chalk.bgRed.black('Unsuccessful workspaces:') + ' ' + failure.map(([key]) => key).join(', ') + '\n');
}
}
}
if (result.errors.length) {
process.exit(EXIT_CODE_ERROR_RESULT);
}
} catch (err) {
console.error(chalk.bgRed('Unexpected error:') + ' ' + (err instanceof Error ? messageWithCauses(err) + '\n\n' + stackWithCauses(err) : err) + '\n');
process.exit(EXIT_CODE_UNEXPECTED_ERROR);
}