Skip to content

Commit aeb022e

Browse files
committed
fix: do not read the project manifest when a dependency is missing
1 parent 1b910a9 commit aeb022e

9 files changed

Lines changed: 125 additions & 64 deletions

File tree

‎packages/nuxt-cli/src/commands/info.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { formatInfoBox } from '../utils/formatting'
2020
import{tryResolveNuxt}from'../utils/kit'
2121
import{logger}from'../utils/logger'
2222
import{getNuxtConfig}from'../utils/nuxt-config'
23+
import{readDependencyPackageJson}from'../utils/package-json'
2324
import{getPackageManagerVersion}from'../utils/packageManagers'
2425
import{resolveRootDir}from'../utils/paths'
2526
import{rootDirArgs}from'./_shared'
@@ -52,7 +53,7 @@ export default defineCommand({
5253
if(!url){
5354
continue
5455
}
55-
constpkg=awaitreadPackageJSON(name,{url}).catch(()=>null)
56+
constpkg=awaitreadDependencyPackageJson(name,url)
5657
if(pkg){
5758
returnpkg.version!
5859
}

‎packages/nuxt-cli/src/commands/module/remove.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { readNuxtConfig, removeNuxtConfigEntries } from '../../utils/config'
1717
import{CONFIG_KEYS}from'../../utils/config-parse'
1818
import{logger}from'../../utils/logger'
1919
import{logNetworkError}from'../../utils/network'
20+
import{readDependencyPackageJson}from'../../utils/package-json'
2021
import{relativeToProcess}from'../../utils/paths'
2122
import{cwdArgs,logLevelArgs}from'../_shared'
2223
importprepareCommandfrom'../prepare'
@@ -258,7 +259,7 @@ async function findOrphanedPeers(removing: string[], projectPkg: PackageJson, cw
258259
// peer name -> first removed module that declares it
259260
constcandidates=newMap<string,string>()
260261
for(constmofremoving){
261-
constpkg=awaitreadPackageJSON(m,{from: cwd}).catch(()=>null)
262+
constpkg=awaitreadDependencyPackageJson(m,cwd)
262263
if(!pkg?.peerDependencies){
263264
continue
264265
}
@@ -280,7 +281,7 @@ async function findOrphanedPeers(removing: string[], projectPkg: PackageJson, cw
280281
if(removingSet.has(dep)||candidates.has(dep)){
281282
continue
282283
}
283-
constdepPkg=awaitreadPackageJSON(dep,{from: cwd}).catch(()=>null)
284+
constdepPkg=awaitreadDependencyPackageJson(dep,cwd)
284285
if(!depPkg){
285286
continue
286287
}

‎packages/nuxt-cli/src/commands/upgrade.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ export default defineCommand({
187187
logger.step(`Package manager: ${colors.cyan(packageManagerName)}${packageManagerVersion}`)
188188

189189
// Check currently installed Nuxt version
190-
constcurrentVersion=(awaitgetNuxtVersion(cwd,false))||'[unknown]'
190+
constcurrentVersion=(awaitgetNuxtVersion(cwd))||'[unknown]'
191191
logger.step(`Current Nuxt version: ${colors.cyan(currentVersion)}`)
192192

193193
constpkg=awaitreadPackageJSON(cwd).catch(()=>null)
@@ -374,7 +374,7 @@ export default defineCommand({
374374
}
375375

376376
// Check installed Nuxt version again
377-
constupgradedVersion=(awaitgetNuxtVersion(cwd,false))||'[unknown]'
377+
constupgradedVersion=(awaitgetNuxtVersion(cwd))||'[unknown]'
378378

379379
if(upgradedVersion==='[unknown]'){
380380
return
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
importtype{PackageJson}from'pkg-types'
2+
3+
import{resolveModulePath}from'exsolve'
4+
import{join}from'pathe'
5+
import{readPackageJSON}from'pkg-types'
6+
7+
/**
8+
* Read the `package.json` of an installed dependency.
9+
*
10+
* `readPackageJSON(name, { try: true })` falls back to the nearest `package.json`
11+
* above the current working directory when the package cannot be resolved, which
12+
* silently returns the consuming project's own manifest in place of the one that
13+
* was asked for. Resolution happens here instead so a dependency that is not
14+
* installed reports as missing.
15+
*
16+
* The manifest is asked for directly first, then located from the package's entry
17+
* point, because `exports` may withhold either one of the two.
18+
*/
19+
exportasyncfunctionreadDependencyPackageJson(name: string,from: string): Promise<PackageJson|undefined>{
20+
constoptions={from: join(from,'/'),try: true}
21+
constpath=resolveModulePath(`${name}/package.json`,options)??resolveModulePath(name,options)
22+
23+
returnpath ? awaitreadPackageJSON(path).catch(()=>undefined) : undefined
24+
}

‎packages/nuxt-cli/src/utils/versions.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,25 @@ import { resolveCatalogEntry } from './catalog'
88
import{fetchJson}from'./fetch'
99
import{tryResolveNuxt}from'./kit'
1010
import{debug}from'./logger'
11+
import{readDependencyPackageJson}from'./package-json'
1112
import{detectNpmRegistry}from'./registry'
1213

1314
/** How long to wait on the registry before giving up on a version lookup. */
1415
constFETCH_TIMEOUT=10_000
1516

1617
/**
1718
* Names a resolved `nuxt` dependency can legitimately have, so that a
18-
* `package.json` reached through `pkg-types`' nearest-file fallback (which
19-
* happens when `nuxt` cannot be resolved at all) is not mistaken for Nuxt's own.
19+
* `package.json` reached by walking up from a resolved entry point (which happens
20+
* when the package exposes no `./package.json` export) is not mistaken for Nuxt's
21+
* own.
2022
*/
2123
constNUXT_PACKAGE_NAMES=newSet(['nuxt','nuxt-nightly','nuxt3','nuxt-edge'])
2224

2325
/** Assumed Nuxt version when the project declares no resolvable one. */
2426
exportconstDEFAULT_NUXT_VERSION='3.0.0'
2527

26-
exportasyncfunctiongetNuxtVersion(cwd: string,cache=true){
27-
constnuxtPkg=awaitreadPackageJSON('nuxt',{url: cwd,try: true, cache }).catch(()=>null)
28+
exportasyncfunctiongetNuxtVersion(cwd: string){
29+
constnuxtPkg=awaitreadDependencyPackageJson('nuxt',cwd).catch(()=>null)
2830
if(nuxtPkg?.version&&NUXT_PACKAGE_NAMES.has(nuxtPkg.name!)){
2931
returnnuxtPkg.version
3032
}

‎packages/nuxt-cli/test/unit/commands/module/remove.spec.ts‎

Lines changed: 39 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ const defaultProjectPkg = {
1818
dependencies: {'@nuxt/content': '^3.0.0'},
1919
}
2020

21-
constreadPackageJSON=vi.fn(()=>Promise.resolve(defaultProjectPkg))
21+
constreadPackageJSON=vi.fn((): Promise<Record<string,unknown>>=>Promise.resolve(defaultProjectPkg))
22+
constreadDependencyPackageJson=vi.fn((_name?: string): Promise<Record<string,unknown>|undefined>=>Promise.resolve(undefined))
2223

2324
interfaceCommandsType{
2425
subCommands: {
@@ -29,6 +30,7 @@ interface CommandsType {
2930
vi.mock('../../../../src/utils/config',()=>({ readNuxtConfig, removeNuxtConfigEntries }))
3031
vi.mock('nypm',()=>({ removeDependency, detectPackageManager }))
3132
vi.mock('pkg-types',()=>({ readPackageJSON }))
33+
vi.mock('../../../../src/utils/package-json',()=>({ readDependencyPackageJson }))
3234
vi.mock('@clack/prompts',asyncimportOriginal=>({
3335
...awaitimportOriginal<typeofimport('@clack/prompts')>(),
3436
confirm: (...args: unknown[])=>confirm(...(argsas[])),
@@ -71,6 +73,7 @@ describe('module remove', () => {
7173
confirm.mockReset().mockResolvedValue(false)
7274
multiselect.mockReset().mockResolvedValue([])
7375
readPackageJSON.mockReset().mockImplementation(()=>Promise.resolve(defaultProjectPkg))
76+
readDependencyPackageJson.mockReset().mockImplementation(()=>Promise.resolve(undefined))
7477
})
7578

7679
it('should remove a Nuxt module by alias',async()=>{
@@ -163,10 +166,10 @@ describe('module remove', () => {
163166
})
164167

165168
it('should not uninstall a module that is not in dependencies',async()=>{
166-
readPackageJSON.mockImplementation((()=>Promise.resolve({
169+
readPackageJSON.mockImplementation(()=>Promise.resolve({
167170
devDependencies: {nuxt: '3.0.0'},
168171
dependencies: {},
169-
}))astypeofreadPackageJSON)
172+
}))
170173

171174
constremoveCommand=await(commandsasCommandsType).subCommands.remove()
172175
awaitremoveCommand.setup({
@@ -181,21 +184,16 @@ describe('module remove', () => {
181184

182185
it('should remove orphaned peer dependencies when confirmed',async()=>{
183186
confirm.mockResolvedValueOnce(true)
184-
readPackageJSON.mockImplementation(((id?: string)=>{
185-
if(id==='@vee-validate/nuxt'){
186-
returnPromise.resolve({peerDependencies: {'vee-validate': '^4.0.0'}})
187-
}
188-
if(id==='vee-validate'||id==='nuxt'){
189-
returnPromise.resolve({})
190-
}
191-
returnPromise.resolve({
192-
devDependencies: {nuxt: '3.0.0'},
193-
dependencies: {
194-
'@vee-validate/nuxt': '1.0.0',
195-
'vee-validate': '4.0.0',
196-
},
197-
})
198-
})astypeofreadPackageJSON)
187+
readPackageJSON.mockImplementation(()=>Promise.resolve({
188+
devDependencies: {nuxt: '3.0.0'},
189+
dependencies: {
190+
'@vee-validate/nuxt': '1.0.0',
191+
'vee-validate': '4.0.0',
192+
},
193+
}))
194+
readDependencyPackageJson.mockImplementation((name?: string)=>Promise.resolve(
195+
name==='@vee-validate/nuxt' ? {peerDependencies: {'vee-validate': '^4.0.0'}} : {},
196+
))
199197

200198
constremoveCommand=await(commandsasCommandsType).subCommands.remove()
201199
awaitremoveCommand.setup({
@@ -214,21 +212,16 @@ describe('module remove', () => {
214212

215213
it('should keep orphaned peer dependencies when declined',async()=>{
216214
confirm.mockResolvedValueOnce(false)
217-
readPackageJSON.mockImplementation(((id?: string)=>{
218-
if(id==='@vee-validate/nuxt'){
219-
returnPromise.resolve({peerDependencies: {'vee-validate': '^4.0.0'}})
220-
}
221-
if(id==='vee-validate'||id==='nuxt'){
222-
returnPromise.resolve({})
223-
}
224-
returnPromise.resolve({
225-
devDependencies: {nuxt: '3.0.0'},
226-
dependencies: {
227-
'@vee-validate/nuxt': '1.0.0',
228-
'vee-validate': '4.0.0',
229-
},
230-
})
231-
})astypeofreadPackageJSON)
215+
readPackageJSON.mockImplementation(()=>Promise.resolve({
216+
devDependencies: {nuxt: '3.0.0'},
217+
dependencies: {
218+
'@vee-validate/nuxt': '1.0.0',
219+
'vee-validate': '4.0.0',
220+
},
221+
}))
222+
readDependencyPackageJson.mockImplementation((name?: string)=>Promise.resolve(
223+
name==='@vee-validate/nuxt' ? {peerDependencies: {'vee-validate': '^4.0.0'}} : {},
224+
))
232225

233226
constremoveCommand=await(commandsasCommandsType).subCommands.remove()
234227
awaitremoveCommand.setup({
@@ -246,25 +239,23 @@ describe('module remove', () => {
246239
})
247240

248241
it('should not treat a peer still required by another dependency as orphaned',async()=>{
249-
readPackageJSON.mockImplementation(((id?: string)=>{
250-
if(id==='@vee-validate/nuxt'){
242+
readPackageJSON.mockImplementation(()=>Promise.resolve({
243+
devDependencies: {nuxt: '3.0.0'},
244+
dependencies: {
245+
'@vee-validate/nuxt': '1.0.0',
246+
'some-other-dep': '1.0.0',
247+
'vee-validate': '4.0.0',
248+
},
249+
}))
250+
readDependencyPackageJson.mockImplementation((name?: string)=>{
251+
if(name==='@vee-validate/nuxt'){
251252
returnPromise.resolve({peerDependencies: {'vee-validate': '^4.0.0'}})
252253
}
253-
if(id==='some-other-dep'){
254+
if(name==='some-other-dep'){
254255
returnPromise.resolve({dependencies: {'vee-validate': '^4.0.0'}})
255256
}
256-
if(id==='vee-validate'||id==='nuxt'){
257-
returnPromise.resolve({})
258-
}
259-
returnPromise.resolve({
260-
devDependencies: {nuxt: '3.0.0'},
261-
dependencies: {
262-
'@vee-validate/nuxt': '1.0.0',
263-
'some-other-dep': '1.0.0',
264-
'vee-validate': '4.0.0',
265-
},
266-
})
267-
})astypeofreadPackageJSON)
257+
returnPromise.resolve({})
258+
})
268259

269260
constremoveCommand=await(commandsasCommandsType).subCommands.remove()
270261
awaitremoveCommand.setup({
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import{mkdtemp,realpath,writeFile}from'node:fs/promises'
2+
import{tmpdir}from'node:os'
3+
import{fileURLToPath}from'node:url'
4+
5+
import{join}from'pathe'
6+
import{describe,expect,it}from'vitest'
7+
8+
import{readDependencyPackageJson}from'../../../src/utils/package-json'
9+
10+
/** This package's own directory, where its dependencies resolve from. */
11+
constpackageDir=fileURLToPath(newURL('../../../',import.meta.url))
12+
13+
describe('readDependencyPackageJson',()=>{
14+
it('should read the manifest of an installed dependency',async()=>{
15+
constpkg=awaitreadDependencyPackageJson('pathe',packageDir)
16+
17+
expect(pkg?.name).toBe('pathe')
18+
})
19+
20+
it('should read the manifest of a dependency that does not export it',async()=>{
21+
constpkg=awaitreadDependencyPackageJson('scule',packageDir)
22+
23+
expect(pkg?.name).toBe('scule')
24+
})
25+
26+
it('should return `undefined` for a package that is not installed',async()=>{
27+
awaitexpect(readDependencyPackageJson('not-a-real-package-xyz',packageDir)).resolves.toBeUndefined()
28+
})
29+
30+
it('should not fall back to the manifest of the consuming project',async()=>{
31+
constcwd=awaitrealpath(awaitmkdtemp(join(tmpdir(),'nuxi-pkg-')))
32+
awaitwriteFile(join(cwd,'package.json'),'{"name":"my-project","version":"1.2.3"}','utf8')
33+
34+
awaitexpect(readDependencyPackageJson('nuxt',cwd)).resolves.toBeUndefined()
35+
})
36+
})

‎packages/nuxt-cli/test/unit/utils/update.spec.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,17 @@ vi.mock('../../../src/utils/registry', () => ({
3939
detectNpmRegistry: async()=>registry.current,
4040
}))
4141

42-
vi.mock('pkg-types',()=>({
43-
readPackageJSON: async(id?: string)=>{
44-
if(id==='nuxt'){
42+
vi.mock('../../../src/utils/package-json',()=>({
43+
readDependencyPackageJson: async(name?: string)=>{
44+
if(name==='nuxt'){
4545
returnproject.nuxtVersion ? {name: 'nuxt',version: project.nuxtVersion} : undefined
4646
}
47+
returnundefined
48+
},
49+
}))
50+
51+
vi.mock('pkg-types',()=>({
52+
readPackageJSON: async()=>{
4753
thrownewError('package.json not found')
4854
},
4955
}))

‎packages/nuxt-cli/test/unit/utils/versions.spec.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,28 @@ describe('getNuxtVersion', () => {
2525
it('should read the declared version when nuxt is not installed',async()=>{
2626
awaitwriteFile(join(tempDir,'package.json'),JSON.stringify({devDependencies: {nuxt: '^4.2.0'}}))
2727

28-
expect(awaitgetNuxtVersion(tempDir,false)).toBe('4.2.0')
28+
expect(awaitgetNuxtVersion(tempDir)).toBe('4.2.0')
2929
})
3030

3131
it('should resolve a catalog reference through pnpm-workspace.yaml',async()=>{
3232
awaitwriteFile(join(tempDir,'package.json'),JSON.stringify({devDependencies: {nuxt: 'catalog:'}}))
3333
awaitwriteFile(join(tempDir,'pnpm-workspace.yaml'),'catalog:\n nuxt: ^4.2.0\n')
3434

35-
expect(awaitgetNuxtVersion(tempDir,false)).toBe('4.2.0')
35+
expect(awaitgetNuxtVersion(tempDir)).toBe('4.2.0')
3636
})
3737

3838
it('should resolve a named catalog reference',async()=>{
3939
awaitwriteFile(join(tempDir,'package.json'),JSON.stringify({dependencies: {nuxt: 'catalog:prod'}}))
4040
awaitwriteFile(join(tempDir,'pnpm-workspace.yaml'),'catalogs:\n prod:\n nuxt: 4.1.2\n')
4141

42-
expect(awaitgetNuxtVersion(tempDir,false)).toBe('4.1.2')
42+
expect(awaitgetNuxtVersion(tempDir)).toBe('4.1.2')
4343
})
4444

4545
it('should fall back when the catalog has no entry for nuxt',async()=>{
4646
awaitwriteFile(join(tempDir,'package.json'),JSON.stringify({dependencies: {nuxt: 'catalog:'}}))
4747
awaitwriteFile(join(tempDir,'pnpm-workspace.yaml'),'catalog:\n vue: ^3.6.0\n')
4848

49-
expect(awaitgetNuxtVersion(tempDir,false)).toBe('3.0.0')
49+
expect(awaitgetNuxtVersion(tempDir)).toBe('3.0.0')
5050
})
5151
})
5252

0 commit comments

Comments
 (0)