1- import fs from 'node:fs/promises'
2- import os from 'node:os'
3- import path from 'node:path'
41import { vi } from 'vitest'
52
6- import { BuildCancelledError } from '../../src/errors/CustomError.js'
3+ import {
4+ BuildCancelledError ,
5+ InstallError ,
6+ PackageNotFoundError ,
7+ } from '../../src/errors/CustomError.js'
8+ import { exec , ProcessExecutionError } from '../../src/utils/common.utils.js'
79import InstallationUtils from '../../src/utils/installation.utils.js'
810
9- describe ( 'InstallationUtils' , ( ) => {
10- test ( 'installs a local package whose path contains shell metacharacters' , async ( ) => {
11- const temporaryDirectory = await fs . mkdtemp (
12- path . join ( os . tmpdir ( ) , 'package-build-stats-install-' ) ,
13- )
14- const packagePath = path . join ( temporaryDirectory , 'fixture; not-a-command' )
15- const installPath = await InstallationUtils . preparePath ( 'local-fixture' )
16-
17- try {
18- await fs . mkdir ( packagePath )
19- await fs . writeFile (
20- path . join ( packagePath , 'package.json' ) ,
21- JSON . stringify ( {
22- name : 'local-fixture' ,
23- version : '1.0.0' ,
24- main : 'index.js' ,
25- } ) ,
26- )
27- await fs . writeFile (
28- path . join ( packagePath , 'index.js' ) ,
29- 'module.exports = 42\n' ,
30- )
31-
32- await InstallationUtils . installWithClient (
33- packagePath ,
34- installPath ,
35- { client : 'npm' , isLocal : true , installTimeout : 10_000 } ,
36- 'npm' ,
37- )
11+ function makeProcessError ( stderr : string , stdout = '' ) {
12+ return new ProcessExecutionError ( 'install failed' , stdout , stderr , 1 )
13+ }
3814
39- const installedPackage = JSON . parse (
40- await fs . readFile (
41- path . join (
42- installPath ,
43- 'node_modules' ,
44- 'local-fixture' ,
45- 'package.json' ,
46- ) ,
47- 'utf8' ,
48- ) ,
49- )
50- expect ( installedPackage . name ) . toBe ( 'local-fixture' )
51- } finally {
52- await InstallationUtils . cleanupPath ( installPath )
53- await fs . rm ( temporaryDirectory , { recursive : true , force : true } )
54- }
55- } )
15+ // The local-package integration test (npm-pack + real install) lives in
16+ // tests/slow/local.test.ts – exec is mocked for the whole file below.
5617
18+ describe ( 'InstallationUtils' , ( ) => {
5719 test ( 'throws instead of terminating the host for an invalid client' , async ( ) => {
5820 await expect (
5921 InstallationUtils . installWithClient (
@@ -70,18 +32,129 @@ describe('InstallationUtils', () => {
7032 . spyOn ( InstallationUtils , 'installWithClient' )
7133 . mockRejectedValue ( new BuildCancelledError ( ) )
7234
73- await expect (
74- InstallationUtils . installPackage ( 'example' , '/tmp/unused' , {
75- client : [ 'bun' , 'npm' ] ,
76- } ) ,
77- ) . rejects . toBeInstanceOf ( BuildCancelledError )
35+ try {
36+ await expect (
37+ InstallationUtils . installPackage ( 'example' , '/tmp/unused' , {
38+ client : [ 'bun' , 'npm' ] ,
39+ } ) ,
40+ ) . rejects . toBeInstanceOf ( BuildCancelledError )
41+
42+ expect ( installWithClient ) . toHaveBeenCalledTimes ( 1 )
43+ expect ( installWithClient ) . toHaveBeenCalledWith (
44+ 'example' ,
45+ '/tmp/unused' ,
46+ expect . objectContaining ( { client : 'bun' } ) ,
47+ 'bun' ,
48+ )
49+ } finally {
50+ installWithClient . mockRestore ( )
51+ }
52+ } )
53+ } )
54+
55+ // vi.mock is hoisted by Vitest, so exec is replaced for the whole file.
56+ vi . mock ( '../../src/utils/common.utils.js' , async importOriginal => {
57+ const actual =
58+ await importOriginal < typeof import ( '../../src/utils/common.utils.js' ) > ( )
59+ return {
60+ ...actual ,
61+ exec : vi . fn ( ) ,
62+ }
63+ } )
64+
65+ describe ( 'installWithClient – package-not-found error classification' , ( ) => {
66+ afterEach ( ( ) => {
67+ vi . resetAllMocks( )
68+ } )
69+
70+ const cases : Array < {
71+ pm : 'npm' | 'yarn' | 'pnpm' | 'bun'
72+ label : string
73+ stderr : string
74+ stdout ?: string
75+ } > = [
76+ {
77+ pm : 'npm' ,
78+ label : 'npm E404 (unknown package)' ,
79+ stderr :
80+ 'npm error code E404\nnpm error 404 Not Found - GET https://registry.npmjs.org/no-such-pkg' ,
81+ } ,
82+ {
83+ pm : 'npm' ,
84+ label : 'npm ETARGET (version range has no match)' ,
85+ stderr :
86+ 'npm error code ETARGET\nnpm error notarget No matching version found for lodash@999.0.0' ,
87+ } ,
88+ {
89+ pm : 'npm' ,
90+ label : 'npm "No matching version found" text' ,
91+ stderr :
92+ 'npm ERR! code ETARGET\nnpm ERR! No matching version found for react@0.0.0-nonexistent.' ,
93+ } ,
94+ {
95+ pm : 'yarn' ,
96+ label : "yarn classic – Couldn't find package" ,
97+ stderr :
98+ 'error Couldn\'t find package "no-such-pkg" on the "npm" registry' ,
99+ } ,
100+ {
101+ pm : 'yarn' ,
102+ label : 'yarn berry – YN0035' ,
103+ stderr :
104+ "YN0035: │ no-such-pkg@npm:^1.0.0 couldn't be resolved to a satisfying range" ,
105+ } ,
106+ {
107+ pm : 'pnpm' ,
108+ label : 'pnpm ERR_PNPM_NO_MATCHING_VERSION' ,
109+ stderr :
110+ 'ERR_PNPM_NO_MATCHING_VERSION No matching version found for no-such-pkg@999.0.0' ,
111+ } ,
112+ {
113+ pm : 'pnpm' ,
114+ label : 'pnpm ERR_PNPM_FETCH_404' ,
115+ stderr :
116+ 'ERR_PNPM_FETCH_404 GET https://registry.npmjs.org/no-such-pkg: Not Found - 404' ,
117+ } ,
118+ {
119+ pm : 'bun' ,
120+ label : 'bun – package not found' ,
121+ stderr : 'error: bun package not found no-such-pkg' ,
122+ } ,
123+ {
124+ pm : 'bun' ,
125+ label : 'bun – 404 Not Found' ,
126+ stderr :
127+ '404 Not Found\nGET https://registry.npmjs.org/no-such-pkg/-/no-such-pkg-1.0.0.tgz' ,
128+ } ,
129+ ]
130+
131+ for ( const { pm, label, stderr, stdout = '' } of cases ) {
132+ test ( `throws PackageNotFoundError for: ${ label } ` , async ( ) => {
133+ vi . mocked ( exec ) . mockRejectedValue ( makeProcessError ( stderr , stdout ) )
134+
135+ await expect (
136+ InstallationUtils . installWithClient (
137+ 'no-such-pkg@999.0.0' ,
138+ '/tmp/unused-install-path' ,
139+ { client : pm } ,
140+ pm ,
141+ ) ,
142+ ) . rejects . toBeInstanceOf ( PackageNotFoundError )
143+ } )
144+ }
78145
79- expect ( installWithClient ) . toHaveBeenCalledTimes ( 1 )
80- expect ( installWithClient ) . toHaveBeenCalledWith (
81- 'example' ,
82- '/tmp/unused' ,
83- expect . objectContaining ( { client : 'bun' } ) ,
84- 'bun' ,
146+ test ( 'throws InstallError for generic (non-404) failures' , async ( ) => {
147+ vi . mocked ( exec ) . mockRejectedValue (
148+ makeProcessError ( 'npm ERR! network timeout' , '' ) ,
85149 )
150+
151+ await expect (
152+ InstallationUtils . installWithClient (
153+ 'some-pkg' ,
154+ '/tmp/unused-install-path' ,
155+ { client : 'npm' } ,
156+ 'npm' ,
157+ ) ,
158+ ) . rejects . toBeInstanceOf ( InstallError )
86159 } )
87160} )
0 commit comments