11const t = require ( 'tap' )
2- const spawn = require ( '@npmcli/promise-spawn' )
32const { spawnSync } = require ( 'child_process' )
4- const { resolve, join } = require ( 'path' )
5- const { readFileSync, chmodSync } = require ( 'fs' )
3+ const { resolve, join, extname , sep } = require ( 'path' )
4+ const { readFileSync, chmodSync, readdirSync } = require ( 'fs' )
65const Diff = require ( 'diff' )
6+ const { sync : which } = require ( 'which' )
77const { version } = require ( '../../package.json' )
88
9- const root = resolve ( __dirname , '../..' )
10- const npmShim = join ( root , 'bin/npm' )
11- const npxShim = join ( root , 'bin/npx' )
9+ const ROOT = resolve ( __dirname , '../..' )
10+ const BIN = join ( ROOT , 'bin' )
11+ const SHIMS = readdirSync ( BIN ) . reduce ( ( acc , shim ) => {
12+ if ( extname ( shim ) !== '.js' ) {
13+ acc [ shim ] = readFileSync ( join ( BIN , shim ) , 'utf-8' )
14+ }
15+ return acc
16+ } , { } )
17+
18+ // windows requires each segment of a command path to be quoted when using shell: true
19+ const quoteWhich = ( cmd ) => which ( cmd )
20+ . split ( sep )
21+ . map ( p => p . includes ( ' ' ) ? `"${ p } "` : p )
22+ . join ( sep )
1223
13- t . test ( 'npm vs npx ' , t => {
24+ t . test ( 'shim contents ' , t => {
1425 // these scripts should be kept in sync so this tests the contents of each
1526 // and does a diff to ensure the only differences between them are necessary
16- const diffFiles = ( ext = '' ) => Diff . diffChars (
17- readFileSync ( `${ npmShim } ${ ext } ` , 'utf8' ) ,
18- readFileSync ( `${ npxShim } ${ ext } ` , 'utf8' )
19- ) . filter ( v => v . added || v . removed ) . map ( ( v , i ) => i === 0 ? v . value : v . value . toUpperCase ( ) )
27+ const diffFiles = ( npm , npx ) => Diff . diffChars ( npm , npx )
28+ . filter ( v => v . added || v . removed )
29+ . reduce ( ( acc , v ) => {
30+ if ( v . value . length === 1 ) {
31+ acc . letters . add ( v . value . toUpperCase ( ) )
32+ } else {
33+ acc . diff . push ( v . value )
34+ }
35+ return acc
36+ } , { diff : [ ] , letters : new Set ( ) } )
37+
38+ t . plan ( 3 )
2039
2140 t . test ( 'bash' , t => {
22- const [ npxCli , ...changes ] = diffFiles ( )
23- const npxCliLine = npxCli . split ( '\n' ) . reverse ( ) . join ( '' )
24- t . match ( npxCliLine , / ^ N P X _ C L I _ J S = / , 'has NPX_CLI' )
25- t . equal ( changes . length , 20 )
26- t . strictSame ( [ ...new Set ( changes ) ] , [ 'M' , 'X' ] , 'all other changes are m->x' )
41+ const { diff, letters } = diffFiles ( SHIMS . npm , SHIMS . npx )
42+ t . match ( diff [ 0 ] . split ( '\n' ) . reverse ( ) . join ( '' ) , / ^ N P X _ C L I _ J S = / , 'has NPX_CLI' )
43+ t . equal ( diff . length , 1 )
44+ t . strictSame ( [ ...letters ] , [ 'M' , 'X' ] , 'all other changes are m->x' )
2745 t . end ( )
2846 } )
2947
3048 t . test ( 'cmd' , t => {
31- const [ npxCli , ... changes ] = diffFiles ( ' .cmd')
32- t . match ( npxCli , / ^ S E T " N P X _ C L I _ J S = / , 'has NPX_CLI' )
33- t . equal ( changes . length , 12 )
34- t . strictSame ( [ ...new Set ( changes ) ] , [ 'M' , 'X' ] , 'all other changes are m->x' )
49+ const { diff , letters } = diffFiles ( SHIMS [ 'npm .cmd'] , SHIMS [ 'npx.cmd' ] )
50+ t . match ( diff [ 0 ] , / ^ S E T " N P X _ C L I _ J S = / , 'has NPX_CLI' )
51+ t . equal ( diff . length , 1 )
52+ t . strictSame ( [ ...letters ] , [ 'M' , 'X' ] , 'all other changes are m->x' )
3553 t . end ( )
3654 } )
3755
38- t . end ( )
56+ t . test ( 'pwsh' , t => {
57+ const { diff, letters } = diffFiles ( SHIMS [ 'npm.ps1' ] , SHIMS [ 'npx.ps1' ] )
58+ t . equal ( diff . length , 0 )
59+ t . strictSame ( [ ...letters ] , [ 'M' , 'X' ] , 'all other changes are m->x' )
60+ t . end ( )
61+ } )
3962} )
4063
41- t . test ( 'basic' , async t => {
42- if ( process . platform !== 'win32' ) {
43- t . comment ( 'test only relevant on windows' )
44- return
45- }
46-
64+ t . test ( 'run shims' , t => {
4765 const path = t . testdir ( {
66+ ...SHIMS ,
4867 'node.exe' : readFileSync ( process . execPath ) ,
49- npm : readFileSync ( npmShim ) ,
50- npx : readFileSync ( npxShim ) ,
5168 // simulate the state where one version of npm is installed
5269 // with node, but we should load the globally installed one
5370 'global-prefix' : {
5471 node_modules : {
55- npm : t . fixture ( 'symlink' , root ) ,
72+ npm : t . fixture ( 'symlink' , ROOT ) ,
5673 } ,
5774 } ,
5875 // put in a shim that ONLY prints the intended global prefix,
5976 // and should not be used for anything else.
6077 node_modules : {
6178 npm : {
6279 bin : {
63- 'npx-cli.js' : `
64- throw new Error('this should not be called')
65- ` ,
80+ 'npx-cli.js' : `throw new Error('this should not be called')` ,
6681 'npm-cli.js' : `
6782 const assert = require('assert')
6883 const args = process.argv.slice(2)
@@ -76,70 +91,96 @@ t.test('basic', async t => {
7691 } ,
7792 } )
7893
79- chmodSync ( join ( path , 'npm' ) , 0o755 )
80- chmodSync ( join ( path , 'npx' ) , 0o755 )
81-
82- const { ProgramFiles, SystemRoot, NYC_CONFIG } = process . env
83- const gitBash = join ( ProgramFiles , 'Git' , 'bin' , 'bash.exe' )
84- const gitUsrBinBash = join ( ProgramFiles , 'Git' , 'usr' , 'bin' , 'bash.exe' )
85- const wslBash = join ( SystemRoot , 'System32' , 'bash.exe' )
86- const cygwinBash = join ( SystemRoot , '/' , 'cygwin64' , 'bin' , 'bash.exe' )
87-
88- const bashes = Object . entries ( {
89- 'wsl bash' : wslBash ,
90- 'git bash' : gitBash ,
91- 'git internal bash' : gitUsrBinBash ,
92- 'cygwin bash' : cygwinBash ,
93- } ) . map ( ( [ name , bash ] ) => {
94- let skip
95- if ( bash === cygwinBash && NYC_CONFIG ) {
96- skip = 'does not play nicely with NYC, run without coverage'
97- } else {
94+ const spawn = ( cmd , args , opts ) => {
95+ const result = spawnSync ( cmd , args , {
96+ // don't hit the registry for the update check
97+ env : { PATH : path , npm_config_update_notifier : 'false' } ,
98+ cwd : path ,
99+ windowsHide : true ,
100+ ...opts ,
101+ } )
102+ result . stdout = result . stdout . toString ( ) . trim ( )
103+ result . stderr = result . stderr . toString ( ) . trim ( )
104+ return result
105+ }
106+
107+ for ( const shim of Object . keys ( SHIMS ) ) {
108+ chmodSync ( join ( path , shim ) , 0o755 )
109+ }
110+
111+ const { ProgramFiles = '' , SystemRoot = '' , NYC_CONFIG , WINDOWS_SHIMS_TEST } = process . env
112+ const failOnMissing = WINDOWS_SHIMS_TEST === 'fail'
113+ const defaultSkip = process . platform === 'win32' ? null : 'test on relevant on windows'
114+
115+ const matchSpawn = ( t , cmd , bin = '' , { skip = defaultSkip , name } = { } ) => {
116+ const testName = `${ name || cmd } ${ bin } ` . trim ( )
117+ if ( skip ) {
118+ if ( failOnMissing ) {
119+ t . fail ( testName )
120+ } else {
121+ t . skip ( `${ testName } - ${ skip } ` )
122+ }
123+ return
124+ }
125+ t . test ( testName , t => {
126+ t . plan ( 1 )
127+ const isNpm = testName . includes ( 'npm' )
128+ const binArg = isNpm ? 'help' : '--version'
129+ const args = [ ]
130+ const opts = { }
131+ if ( cmd . endsWith ( '.cmd' ) ) {
132+ args . push ( binArg )
133+ } else if ( cmd === 'pwsh' ) {
134+ cmd = quoteWhich ( cmd )
135+ args . push ( `${ bin } .ps1` , binArg )
136+ opts . shell = true
137+ } else if ( cmd . endsWith ( 'bash.exe' ) ) {
138+ // only cygwin *requires* the -l, but the others are ok with it
139+ args . push ( '-l' , bin , binArg )
140+ }
141+ t . match ( spawn ( cmd , args , opts ) , {
142+ status : 0 ,
143+ signal : null ,
144+ stderr : '' ,
145+ stdout : isNpm ? `npm@${ version } ${ ROOT } ` : version ,
146+ } , 'command output is correct' )
147+ } )
148+ }
149+
150+ // ensure that all tests are either run or skipped
151+ t . plan ( 12 )
152+
153+ matchSpawn ( t , 'npm.cmd' )
154+ matchSpawn ( t , 'npx.cmd' )
155+ matchSpawn ( t , 'pwsh' , 'npm' )
156+ matchSpawn ( t , 'pwsh' , 'npx' )
157+
158+ const bashes = [
159+ { name : 'git' , cmd : join ( ProgramFiles , 'Git' , 'bin' , 'bash.exe' ) } ,
160+ { name : 'user git' , cmd : join ( ProgramFiles , 'Git' , 'usr' , 'bin' , 'bash.exe' ) } ,
161+ { name : 'wsl' , cmd : join ( SystemRoot , 'System32' , 'bash.exe' ) } ,
162+ {
163+ name : 'cygwin' ,
164+ cmd : join ( SystemRoot , '/' , 'cygwin64' , 'bin' , 'bash.exe' ) ,
165+ skip : NYC_CONFIG ? 'does not play nicely with nyc' : undefined ,
166+ } ,
167+ ] . map ( ( { name, cmd, skip = defaultSkip } ) => {
168+ if ( ! skip ) {
98169 try {
99170 // If WSL is installed, it *has* a bash.exe, but it fails if
100171 // there is no distro installed, so we need to detect that.
101- if ( spawnSync ( bash , [ '-l' , '-c' , 'exit 0' ] ) . status !== 0 ) {
172+ if ( spawnSync ( cmd , [ '-l' , '-c' , 'exit 0' ] ) . status !== 0 ) {
102173 throw new Error ( 'not installed' )
103174 }
104- } catch {
105- skip = 'not installed'
175+ } catch ( err ) {
176+ skip = err . message
106177 }
107178 }
108- return { name , bash , skip }
179+ return { cmd , skip , name : ` ${ name } bash` }
109180 } )
110181
111- for ( const { name, bash, skip } of bashes ) {
112- if ( skip ) {
113- t . skip ( name , { diagnostic : true , bin : bash , reason : skip } )
114- continue
115- }
116-
117- await t . test ( name , async t => {
118- const bins = Object . entries ( {
119- // should have loaded this instance of npm we symlinked in
120- npm : [ [ 'help' ] , `npm@${ version } ${ root } ` ] ,
121- npx : [ [ '--version' ] , version ] ,
122- } )
123-
124- for ( const [ binName , [ cmdArgs , stdout ] ] of bins ) {
125- await t . test ( binName , async t => {
126- // only cygwin *requires* the -l, but the others are ok with it
127- const args = [ '-l' , binName , ...cmdArgs ]
128- const result = await spawn ( bash , args , {
129- // don't hit the registry for the update check
130- env : { PATH : path , npm_config_update_notifier : 'false' } ,
131- cwd : path ,
132- } )
133- t . match ( result , {
134- cmd : bash ,
135- args : args ,
136- code : 0 ,
137- signal : null ,
138- stderr : String ,
139- stdout,
140- } )
141- } )
142- }
143- } )
182+ for ( const { cmd, skip, name } of bashes ) {
183+ matchSpawn ( t , cmd , 'npm' , { name, skip } )
184+ matchSpawn ( t , cmd , 'npx' , { name, skip } )
144185 }
145186} )
0 commit comments