Skip to content

Commit 6ce11b3

Browse files
committed
feat: modify rush scan, support executing projects under rush and custom scanning folders.
1 parent fc48c64 commit 6ce11b3

File tree

10 files changed

+234
-61
lines changed

10 files changed

+234
-61
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": " Modify rush scan, support executing projects under rush and custom scanning folders.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}

libraries/rush-lib/config/jest.config.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
"roots": ["<rootDir>/lib-commonjs"],
55

6-
"testMatch": ["<rootDir>/lib-commonjs/**/*.test.js"],
6+
"testMatch": ["<rootDir>/lib-commonjs/cli/actions/test/ScanAction.test.js"],
77

88
"collectCoverageFrom": [
99
"lib-commonjs/**/*.js",

libraries/rush-lib/src/cli/actions/ScanAction.ts

+130-60
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33

44
import * as path from 'path';
55
import builtinPackageNames from 'builtin-modules';
6-
import { Colorize } from '@rushstack/terminal';
7-
import type { CommandLineFlagParameter } from '@rushstack/ts-command-line';
8-
import { FileSystem } from '@rushstack/node-core-library';
6+
import { Colorize, type ITerminal } from '@rushstack/terminal';
7+
import type { CommandLineFlagParameter, CommandLineStringListParameter } from '@rushstack/ts-command-line';
8+
import { FileSystem, FileConstants, JsonFile } from '@rushstack/node-core-library';
9+
import type FastGlob from 'fast-glob';
910

1011
import type { RushCommandLineParser } from '../RushCommandLineParser';
1112
import { BaseConfiglessRushAction } from './BaseRushAction';
13+
import type { RushConfigurationProject } from '../../api/RushConfigurationProject';
1214

13-
export interface IJsonOutput {
15+
export interface IScanResult {
1416
/**
1517
* Dependencies scan from source code
1618
*/
@@ -26,8 +28,11 @@ export interface IJsonOutput {
2628
}
2729

2830
export class ScanAction extends BaseConfiglessRushAction {
31+
private readonly _terminal: ITerminal;
2932
private readonly _jsonFlag: CommandLineFlagParameter;
3033
private readonly _allFlag: CommandLineFlagParameter;
34+
private readonly _folders: CommandLineStringListParameter;
35+
private readonly _projects: CommandLineStringListParameter;
3136

3237
public constructor(parser: RushCommandLineParser) {
3338
super({
@@ -40,7 +45,7 @@ export class ScanAction extends BaseConfiglessRushAction {
4045
` declaring them as dependencies in the package.json file. Such "phantom dependencies"` +
4146
` can cause problems. Rush and PNPM use symlinks specifically to protect against phantom dependencies.` +
4247
` These protections may cause runtime errors for existing projects when they are first migrated into` +
43-
` a Rush monorepo. The "rush scan" command is a handy tool for fixing these errors. It scans the "./src"` +
48+
` a Rush monorepo. The "rush scan" command is a handy tool for fixing these errors. It default scans the "./src"` +
4449
` and "./lib" folders for import syntaxes such as "import __ from '__'", "require('__')",` +
4550
` and "System.import('__'). It prints a report of the referenced packages. This heuristic is` +
4651
` not perfect, but it can save a lot of time when migrating projects.`,
@@ -56,14 +61,31 @@ export class ScanAction extends BaseConfiglessRushAction {
5661
parameterLongName: '--all',
5762
description: 'If this flag is specified, output will list all detected dependencies.'
5863
});
64+
this._folders = this.defineStringListParameter({
65+
parameterLongName: '--folder',
66+
parameterShortName: '-f',
67+
argumentName: 'FOLDER',
68+
description:
69+
'The folders that need to be scanned, default is src and lib.' +
70+
'Normally we can input all the folders under the project directory, excluding the ignored folders.'
71+
});
72+
this._projects = this.defineStringListParameter({
73+
parameterLongName: '--only',
74+
parameterShortName: '-o',
75+
argumentName: 'PROJECT',
76+
description: 'Projects that need to be checked for phantom dependencies.'
77+
});
78+
this._terminal = parser.terminal;
5979
}
6080

61-
protected async runAsync(): Promise<void> {
62-
const packageJsonFilename: string = path.resolve('./package.json');
63-
64-
if (!FileSystem.exists(packageJsonFilename)) {
65-
throw new Error('You must run "rush scan" in a project folder containing a package.json file.');
66-
}
81+
private async _scanAsync(params: {
82+
packageJsonFilePath: string;
83+
folders: readonly string[];
84+
glob: typeof FastGlob;
85+
terminal: ITerminal;
86+
}): Promise<IScanResult> {
87+
const { packageJsonFilePath, folders, glob, terminal } = params;
88+
const packageJsonFilename: string = path.resolve(packageJsonFilePath);
6789

6890
const requireRegExps: RegExp[] = [
6991
// Example: require('something')
@@ -114,8 +136,13 @@ export class ScanAction extends BaseConfiglessRushAction {
114136

115137
const requireMatches: Set<string> = new Set<string>();
116138

117-
const { default: glob } = await import('fast-glob');
118-
const scanResults: string[] = await glob(['./*.{ts,js,tsx,jsx}', './{src,lib}/**/*.{ts,js,tsx,jsx}']);
139+
const scanResults: string[] = await glob(
140+
[
141+
'./*.{ts,js,tsx,jsx}',
142+
`./${folders.length > 1 ? '{' + folders.join(',') + '}' : folders[0]}/**/*.{ts,js,tsx,jsx}`
143+
],
144+
{ cwd: path.dirname(packageJsonFilePath), absolute: true }
145+
);
119146
for (const filename of scanResults) {
120147
try {
121148
const contents: string = FileSystem.readFile(filename);
@@ -131,7 +158,8 @@ export class ScanAction extends BaseConfiglessRushAction {
131158
}
132159
} catch (error) {
133160
// eslint-disable-next-line no-console
134-
console.log(Colorize.bold('Skipping file due to error: ' + filename));
161+
console.log(error);
162+
terminal.writeErrorLine(Colorize.bold('Skipping file due to error: ' + filename));
135163
}
136164
}
137165

@@ -175,8 +203,7 @@ export class ScanAction extends BaseConfiglessRushAction {
175203
}
176204
}
177205
} catch (e) {
178-
// eslint-disable-next-line no-console
179-
console.error(`JSON.parse ${packageJsonFilename} error`);
206+
terminal.writeErrorLine(`JSON.parse ${packageJsonFilename} error`);
180207
}
181208

182209
for (const detectedPkgName of detectedPackageNames) {
@@ -200,65 +227,108 @@ export class ScanAction extends BaseConfiglessRushAction {
200227
}
201228
}
202229

203-
const output: IJsonOutput = {
230+
const output: IScanResult = {
204231
detectedDependencies: detectedPackageNames,
205232
missingDependencies: missingDependencies,
206233
unusedDependencies: unusedDependencies
207234
};
208235

236+
return output;
237+
}
238+
239+
private _getPackageJsonPathsFromProjects(projectNames: readonly string[]): string[] {
240+
const result: string[] = [];
241+
if (!this.rushConfiguration) {
242+
throw new Error(``);
243+
}
244+
for (const projectName of projectNames) {
245+
const project: RushConfigurationProject | undefined =
246+
this.rushConfiguration.getProjectByName(projectName);
247+
if (!project) {
248+
throw new Error(``);
249+
}
250+
const packageJsonFilePath: string = path.join(project.projectFolder, FileConstants.PackageJson);
251+
result.push(packageJsonFilePath);
252+
}
253+
return result;
254+
}
255+
256+
protected async runAsync(): Promise<void> {
257+
const packageJsonFilePaths: string[] = this._projects.values.length
258+
? this._getPackageJsonPathsFromProjects(this._projects.values)
259+
: [path.resolve('./package.json')];
260+
const { default: glob } = await import('fast-glob');
261+
const folders: readonly string[] = this._folders.values.length ? this._folders.values : ['src', 'lib'];
262+
263+
const output: Record<string, IScanResult> = {};
264+
265+
for (const packageJsonFilePath of packageJsonFilePaths) {
266+
if (!FileSystem.exists(packageJsonFilePath)) {
267+
throw new Error(`${packageJsonFilePath} is not exist`);
268+
}
269+
const packageName: string = JsonFile.load(packageJsonFilePath).name;
270+
const scanResult: IScanResult = await this._scanAsync({
271+
packageJsonFilePath,
272+
folders,
273+
glob,
274+
terminal: this._terminal
275+
});
276+
output[packageName] = scanResult;
277+
}
209278
if (this._jsonFlag.value) {
210-
// eslint-disable-next-line no-console
211-
console.log(JSON.stringify(output, undefined, 2));
279+
this._terminal.writeLine(JSON.stringify(output, undefined, 2));
212280
} else if (this._allFlag.value) {
213-
if (detectedPackageNames.length !== 0) {
214-
// eslint-disable-next-line no-console
215-
console.log('Dependencies that seem to be imported by this project:');
216-
for (const packageName of detectedPackageNames) {
217-
// eslint-disable-next-line no-console
218-
console.log(' ' + packageName);
281+
for (const [packageName, scanResult] of Object.entries(output)) {
282+
this._terminal.writeLine(`-------------------- ${packageName} result start --------------------`);
283+
const { detectedDependencies } = scanResult;
284+
if (detectedDependencies.length !== 0) {
285+
this._terminal.writeLine(`Dependencies that seem to be imported by this project ${packageName}:`);
286+
for (const detectedDependency of detectedDependencies) {
287+
this._terminal.writeLine(' ' + detectedDependency);
288+
}
289+
} else {
290+
this._terminal.writeLine(`This project ${packageName} does not seem to import any NPM packages.`);
219291
}
220-
} else {
221-
// eslint-disable-next-line no-console
222-
console.log('This project does not seem to import any NPM packages.');
292+
this._terminal.writeLine(`-------------------- ${packageName} result end --------------------`);
223293
}
224294
} else {
225-
let wroteAnything: boolean = false;
226-
227-
if (missingDependencies.length > 0) {
228-
// eslint-disable-next-line no-console
229-
console.log(
230-
Colorize.yellow('Possible phantom dependencies') +
231-
" - these seem to be imported but aren't listed in package.json:"
232-
);
233-
for (const packageName of missingDependencies) {
234-
// eslint-disable-next-line no-console
235-
console.log(' ' + packageName);
295+
for (const [packageName, scanResult] of Object.entries(output)) {
296+
this._terminal.writeLine(`-------------------- ${packageName} result start --------------------`);
297+
const { missingDependencies, unusedDependencies } = scanResult;
298+
let wroteAnything: boolean = false;
299+
300+
if (missingDependencies.length > 0) {
301+
this._terminal.writeWarningLine(
302+
Colorize.yellow('Possible phantom dependencies') +
303+
" - these seem to be imported but aren't listed in package.json:"
304+
);
305+
for (const missingDependency of missingDependencies) {
306+
this._terminal.writeLine(' ' + missingDependency);
307+
}
308+
wroteAnything = true;
236309
}
237-
wroteAnything = true;
238-
}
239310

240-
if (unusedDependencies.length > 0) {
241-
if (wroteAnything) {
242-
// eslint-disable-next-line no-console
243-
console.log('');
311+
if (unusedDependencies.length > 0) {
312+
if (wroteAnything) {
313+
this._terminal.writeLine('');
314+
}
315+
this._terminal.writeWarningLine(
316+
Colorize.yellow('Possible unused dependencies') +
317+
" - these are listed in package.json but don't seem to be imported:"
318+
);
319+
for (const unusedDependency of unusedDependencies) {
320+
this._terminal.writeLine(' ' + unusedDependency);
321+
}
322+
wroteAnything = true;
244323
}
245-
// eslint-disable-next-line no-console
246-
console.log(
247-
Colorize.yellow('Possible unused dependencies') +
248-
" - these are listed in package.json but don't seem to be imported:"
249-
);
250-
for (const packageName of unusedDependencies) {
251-
// eslint-disable-next-line no-console
252-
console.log(' ' + packageName);
324+
325+
if (!wroteAnything) {
326+
this._terminal.writeLine(
327+
Colorize.green('Everything looks good.') + ' No missing or unused dependencies were found.'
328+
);
253329
}
254-
wroteAnything = true;
255-
}
256330

257-
if (!wroteAnything) {
258-
// eslint-disable-next-line no-console
259-
console.log(
260-
Colorize.green('Everything looks good.') + ' No missing or unused dependencies were found.'
261-
);
331+
this._terminal.writeLine(`-------------------- ${packageName} result end --------------------`);
262332
}
263333
}
264334
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
import '../../test/mockRushCommandLineParser';
4+
5+
import '../../test/mockRushCommandLineParser';
6+
import { ScanAction } from '../ScanAction';
7+
import { RushCommandLineParser } from '../../RushCommandLineParser';
8+
9+
import { Terminal } from '@rushstack/terminal';
10+
11+
describe(ScanAction.name, () => {
12+
describe('basic "rush remove" tests', () => {
13+
let terminalMock: jest.SpyInstance;
14+
let oldExitCode: number | undefined;
15+
let oldArgs: string[];
16+
17+
beforeEach(() => {
18+
terminalMock = jest.spyOn(Terminal.prototype, 'write').mockImplementation(() => {});
19+
20+
jest.spyOn(process, 'exit').mockImplementation();
21+
22+
oldExitCode = process.exitCode;
23+
oldArgs = process.argv;
24+
});
25+
26+
afterEach(() => {
27+
jest.clearAllMocks();
28+
process.exitCode = oldExitCode;
29+
process.argv = oldArgs;
30+
});
31+
32+
describe("'scan' action", () => {
33+
it(`scan the repository to find phantom dependencies. `, async () => {
34+
const aPath: string = `${__dirname}/scanRepo/a`;
35+
36+
const parser: RushCommandLineParser = new RushCommandLineParser({ cwd: aPath });
37+
38+
jest.spyOn(process, 'cwd').mockReturnValue(aPath);
39+
40+
process.argv = ['pretend-this-is-node.exe', 'pretend-this-is-rush', 'scan', '--json'];
41+
await expect(parser.executeAsync()).resolves.toEqual(true);
42+
expect(terminalMock).toHaveBeenCalledTimes(1);
43+
});
44+
});
45+
});
46+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
common/temp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
/* eslint-disable @typescript-eslint/no-unused-vars */
5+
/* eslint-disable import/no-extraneous-dependencies */
6+
import { ConsoleTerminalProvider, Terminal } from '@rushstack/terminal';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "a",
3+
"version": "1.0.0",
4+
"description": "Test package a",
5+
"dependencies": {
6+
"assert": "workspace:*"
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "b",
3+
"version": "1.0.0",
4+
"description": "Test package b",
5+
"dependencies": {
6+
"assert": "workspace:*",
7+
"rimraf": "workspace:*"
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
/* eslint-disable @typescript-eslint/no-unused-vars */
5+
/* eslint-disable import/no-extraneous-dependencies */
6+
import { ConsoleTerminalProvider, Terminal } from '@rushstack/terminal';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"npmVersion": "6.4.1",
3+
"rushVersion": "5.5.2",
4+
"projectFolderMinDepth": 1,
5+
"projectFolderMaxDepth": 99,
6+
7+
"projects": [
8+
{
9+
"packageName": "a",
10+
"projectFolder": "a"
11+
},
12+
{
13+
"packageName": "b",
14+
"projectFolder": "b"
15+
}
16+
]
17+
}

0 commit comments

Comments
 (0)