33
44import * as path from 'path' ;
55import 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
1011import type { RushCommandLineParser } from '../RushCommandLineParser' ;
1112import { 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
2830export 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 }
0 commit comments