Skip to content

Commit 9022816

Browse files
committed
fix(info): make project reporting resilient
1 parent b6f2690 commit 9022816

6 files changed

Lines changed: 186 additions & 85 deletions

File tree

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

Lines changed: 94 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
importtype{NuxtConfig,NuxtModule}from'@nuxt/schema'
1+
importtype{NuxtModule}from'@nuxt/schema'
22
importtype{PackageJson}from'pkg-types'
33

44
importosfrom'node:os'
@@ -36,66 +36,52 @@ export default defineCommand({
3636
...rootDirArgs,
3737
},
3838
asyncrun(ctx){
39-
// Resolve rootDir
4039
constcwd=resolveRootDir(ctx.args)
41-
42-
// Load Nuxt config
43-
constnuxtConfig=awaitgetNuxtConfig(cwd)
44-
45-
// Find nearest package.json
46-
constprojectPkg=awaitreadPackageJSON(cwd).catch(()=>({}asPackageJson))
40+
const[nuxtConfig,projectPkg,detectedPackageManager]=awaitPromise.all([
41+
getNuxtConfig(cwd),
42+
readPackageJSON(cwd).catch(()=>({}asPackageJson)),
43+
detectPackageManager(cwd),
44+
])
4745
const{ dependencies ={}, devDependencies ={}}=projectPkg
48-
49-
// Utils to query a dependency version
5046
constnuxtPath=tryResolveNuxt(cwd)
51-
asyncfunctiongetDepVersion(name: string){
52-
for(consturlof[cwd,nuxtPath]){
53-
if(!url){
54-
continue
55-
}
56-
constpkg=awaitreadDependencyPackageJson(name,url)
57-
if(pkg){
58-
returnpkg.version!
59-
}
47+
constversions=newMap<string,Promise<string|undefined>>()
48+
constgetDepVersion=(name: string)=>{
49+
letversion=versions.get(name)
50+
if(!version){
51+
version=resolveDependencyVersion(name,[cwd,nuxtPath],cwd,projectPkg,dependencies,devDependencies)
52+
versions.set(name,version)
6053
}
61-
returnresolveCatalogEntry(cwd,projectPkg,name)?.specifier
62-
??(dependencies[name]||devDependencies[name])
54+
returnversion
6355
}
6456

65-
asyncfunctionlistModules(arr: NonNullable<NuxtConfig['modules']>=[]){
66-
constinfo: string[]=[]
67-
for(letmofarr){
68-
if(Array.isArray(m)){
69-
m=m[0]
70-
}
71-
constname=normalizeConfigModule(m,cwd)
72-
if(name){
73-
constnpmName=name!.split('/').splice(0,2).join('/')// @foo/bar/baz => @foo/bar
74-
constv=awaitgetDepVersion(npmName)
75-
info.push(`\`${v ? `${name}@${v}` : name}\``)
76-
}
57+
constmodulesPromise=Promise.all((nuxtConfig.modules||[]).map(async(module)=>{
58+
constname=normalizeConfigModule(module,cwd)
59+
if(!name){
60+
returnnull
7761
}
78-
returninfo.join(', ')
79-
}
80-
81-
// Check Nuxt version
82-
constnuxtVersion=awaitgetDepVersion('nuxt')||awaitgetDepVersion('nuxt-nightly')||'-'
62+
constspecifier=Array.isArray(module) ? module[0] : module
63+
constpackageName=typeofspecifier==='string'&&getPackageName(specifier)
64+
constversion=packageName&&awaitgetDepVersion(packageName)
65+
return`\`${version ? `${name}@${version}` : name}\``
66+
}))
67+
const[modules,nuxtVersion='-',nitroVersion]=awaitPromise.all([
68+
modulesPromise,
69+
getDepVersion('nuxt').then(version=>version||getDepVersion('nuxt-nightly')),
70+
getDepVersion('nitropack').then(version=>version||getDepVersion('nitro')),
71+
])
8372
constbuilder=nuxtConfig.builder||'vite'
84-
85-
letpackageManager=(awaitdetectPackageManager(cwd))?.name
86-
87-
if(packageManager){
88-
packageManager+=`@${getPackageManagerVersion(packageManager)}`
89-
}
90-
73+
constpackageManager=detectedPackageManager
74+
? `${detectedPackageManager.name}@${getPackageManagerVersion(detectedPackageManager.command)}`
75+
: 'unknown'
9176
constosType=os.type()
92-
constbuilderInfo=typeofbuilder==='string'
77+
constcpus=os.cpus()
78+
constbuilderInfo=typeofbuilder==='string'&&['vite','@nuxt/vite-builder','webpack','@nuxt/webpack-builder','rspack','@nuxt/rspack-builder'].includes(builder)
9379
? getBuilder(cwd,builder)
9480
: {name: 'custom',version: '0.0.0'}
9581

9682
constinfoObj={
9783
'Operating system': osType==='Darwin' ? `macOS ${os.release()}` : osType==='Windows_NT' ? `Windows ${os.release()}` : `${osType}${os.release()}`,
98-
'CPU': `${os.cpus()[0]?.model||'unknown'} (${os.cpus().length} cores)`,
84+
'CPU': `${cpus[0]?.model||'unknown'} (${cpus.length} cores)`,
9985
...isBun
10086
// @ts-expect-error Bun global
10187
? {'Bun version': Bun?.versionasstring}
@@ -104,41 +90,22 @@ export default defineCommand({
10490
? {'Deno version': Deno?.version.denoasstring}
10591
: {'Node.js version': process.versionasstring},
10692
'nuxt/cli version': nuxiVersion,
107-
'Package manager': packageManager??'unknown',
93+
'Package manager': packageManager,
10894
'Nuxt version': nuxtVersion,
109-
'Nitro version': awaitgetDepVersion('nitropack')||awaitgetDepVersion('nitro'),
95+
'Nitro version': nitroVersion,
11096
'Builder': builderInfo.name==='custom' ? 'custom' : `${builderInfo.name.toLowerCase()}@${builderInfo.version}`,
11197
'Config': Object.keys(nuxtConfig)
11298
.map(key=>`\`${key}\``)
11399
.sort()
114100
.join(', '),
115-
'Modules': awaitlistModules(nuxtConfig.modules),
101+
'Modules': modules.filter(module=>module!==null).join(', '),
116102
}
117103

118104
logger.info(`Nuxt root directory: ${styleText('cyan',nuxtConfig.rootDir||cwd)}\n`)
119105

120106
constboxStr=formatInfoBox(infoObj)
121107

122-
letfirstColumnLength=0
123-
letsecondColumnLength=0
124-
constentries=Object.entries(infoObj).map(([label,val])=>{
125-
if(label.length>firstColumnLength){
126-
firstColumnLength=label.length+4
127-
}
128-
if((val||'').length>secondColumnLength){
129-
secondColumnLength=(val||'').length+2
130-
}
131-
return[label,val||'-']asconst
132-
})
133-
134-
// formatted for copy-pasting into an issue
135-
letcopyStr=`| ${' '.repeat(firstColumnLength)} | ${' '.repeat(secondColumnLength)} |\n| ${'-'.repeat(firstColumnLength)} | ${'-'.repeat(secondColumnLength)} |\n`
136-
for(const[label,value]ofentries){
137-
if(!isMinimal){
138-
copyStr+=`| ${`**${label}**`.padEnd(firstColumnLength)} | ${(value.includes('`') ? value : `\`${value}\``).padEnd(secondColumnLength)} |\n`
139-
}
140-
}
141-
108+
constcopyStr=formatMarkdownTable(infoObj)
142109
constcopied=!isMinimal&&awaitwriteText(copyStr).then(()=>true).catch(()=>false)
143110

144111
if(copied){
@@ -169,20 +136,69 @@ export default defineCommand({
169136
},
170137
})
171138

172-
functionnormalizeConfigModule(
173-
module: NuxtModule<any,any>|string|false|null|undefined,
139+
asyncfunctionresolveDependencyVersion(
140+
name: string,
141+
roots: Array<string|null>,
142+
cwd: string,
143+
projectPkg: PackageJson,
144+
dependencies: Record<string,string>,
145+
devDependencies: Record<string,string>,
146+
): Promise<string|undefined>{
147+
for(constrootofroots){
148+
if(!root){
149+
continue
150+
}
151+
constpkg=awaitreadDependencyPackageJson(name,root)
152+
if(pkg?.version){
153+
returnpkg.version
154+
}
155+
}
156+
returnresolveCatalogEntry(cwd,projectPkg,name)?.specifier
157+
??dependencies[name]
158+
??devDependencies[name]
159+
}
160+
161+
exportfunctionformatMarkdownTable(info: Record<string,string|undefined>): string{
162+
constentries=Object.entries(info).map(([label,value])=>[label,value||'-']asconst)
163+
constlabelWidth=Math.max(...entries.map(([label])=>label.length+4))
164+
constvalueWidth=Math.max(...entries.map(([,value])=>value.length+(value.includes('`') ? 0 : 2)))
165+
constrows=entries.map(([label,value])=>{
166+
constformattedValue=value.includes('`') ? value : `\`${value}\``
167+
return`| ${`**${label}**`.padEnd(labelWidth)} | ${formattedValue.padEnd(valueWidth)} |`
168+
})
169+
return[
170+
`| ${' '.repeat(labelWidth)} | ${' '.repeat(valueWidth)} |`,
171+
`| ${'-'.repeat(labelWidth)} | ${'-'.repeat(valueWidth)} |`,
172+
...rows,
173+
'',
174+
].join('\n')
175+
}
176+
177+
exportfunctiongetPackageName(name: string): string|undefined{
178+
if(name.startsWith('.')||name.startsWith('/')||/^[a-z]:[\\/]/i.test(name)||name.endsWith('()')){
179+
returnundefined
180+
}
181+
constparts=name.split('/')
182+
returnname.startsWith('@') ? parts.slice(0,2).join('/') : parts[0]
183+
}
184+
185+
exportfunctionnormalizeConfigModule(
186+
module: NuxtModule<any,any>|string|false|null|undefined|readonly[(NuxtModule<any,any>|string|undefined)?,unknown?],
174187
rootDir: string,
175188
): string|null{
176189
if(!module){
177190
returnnull
178191
}
179192
if(typeofmodule==='string'){
180-
returnmodule
181-
.split(rootDir)
182-
.pop()!// Strip rootDir
183-
.split('node_modules')
184-
.pop()!// Strip node_modules
185-
.replace(LEADING_SLASH_RE,'')
193+
constnormalized=module.replaceAll('\\','/')
194+
constnormalizedRoot=rootDir.replaceAll('\\','/').replace(/\/$/,'')
195+
constnodeModulesIndex=normalized.lastIndexOf('/node_modules/')
196+
if(nodeModulesIndex!==-1){
197+
returnnormalized.slice(nodeModulesIndex+'/node_modules/'.length)
198+
}
199+
returnnormalized.startsWith(`${normalizedRoot}/`)
200+
? normalized.slice(normalizedRoot.length+1)
201+
: normalized.replace(LEADING_SLASH_RE,'')
186202
}
187203
if(typeofmodule==='function'){
188204
return`${module.name}()`

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ export function getBuilder(cwd: string, builder: Exclude<NuxtOptions['builder']
1717
case'@nuxt/vite-builder':
1818
default: {
1919
constpkgJSON=getPkgJSON(cwd,'vite',{via: ['nuxt','@nuxt/vite-builder']})
20-
constisRolldown=pkgJSON.name.includes('rolldown')
21-
constisVitePlus=pkgJSON.name==='@voidzero-dev/vite-plus-core'
20+
constisRolldown=pkgJSON?.name.includes('rolldown')
21+
constisVitePlus=pkgJSON?.name==='@voidzero-dev/vite-plus-core'
2222
return{
2323
name: isRolldown ? 'Rolldown-Vite' : 'Vite',
24-
version: (isVitePlus ? pkgJSON.bundledVersions?.vite : pkgJSON.version)||'unknown',
25-
provider: isVitePlus ? {name: 'Vite+',version: pkgJSON.version||'unknown'} : undefined,
24+
version: (isVitePlus ? pkgJSON?.bundledVersions?.vite : pkgJSON?.version)||'unknown',
25+
provider: isVitePlus ? {name: 'Vite+',version: pkgJSON?.version||'unknown'} : undefined,
2626
}
2727
}
2828
}
Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import{execSync}from'node:child_process'
1+
import{execFileSync}from'node:child_process'
2+
importprocessfrom'node:process'
23

3-
exportfunctiongetPackageManagerVersion(name: string){
4-
returnexecSync(`${name} --version`).toString('utf8').trim()
4+
exportfunctiongetPackageManagerVersion(command: string){
5+
// Package managers are `.cmd` shims on Windows, which cannot be spawned without a shell.
6+
constisWindows=process.platform==='win32'
7+
try{
8+
returnexecFileSync(isWindows ? `"${command}"` : command,['--version'],{shell: isWindows,stdio: ['ignore','pipe','ignore']}).toString('utf8').trim()
9+
}
10+
catch{
11+
return'unknown'
12+
}
513
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import{describe,expect,it}from'vitest'
2+
3+
import{formatMarkdownTable,getPackageName,normalizeConfigModule}from'../../../src/commands/info'
4+
5+
describe('info',()=>{
6+
describe('formatMarkdownTable',()=>{
7+
it('includes rows in minimal environments',()=>{
8+
expect(formatMarkdownTable({
9+
'Nuxt version': '4.0.0',
10+
'Modules': '`@nuxt/image@1.0.0`',
11+
'Config': '',
12+
})).toMatchInlineSnapshot(`
13+
"| | |
14+
| ---------------- | ------------------- |
15+
| **Nuxt version** | \`4.0.0\` |
16+
| **Modules** | \`@nuxt/image@1.0.0\` |
17+
| **Config** | \`-\` |
18+
"
19+
`)
20+
})
21+
})
22+
23+
describe('getPackageName',()=>{
24+
it.each([
25+
['@nuxt/image','@nuxt/image'],
26+
['@nuxt/image/module','@nuxt/image'],
27+
['example/module','example'],
28+
['example','example'],
29+
['./modules/example',undefined],
30+
['/project/modules/example',undefined],
31+
['C:\\project\\modules\\example',undefined],
32+
['exampleModule()',undefined],
33+
])('gets the package name for %s',(module,expected)=>{
34+
expect(getPackageName(module)).toBe(expected)
35+
})
36+
})
37+
38+
describe('normalizeConfigModule',()=>{
39+
it.each([
40+
['/project/modules/example','/project','modules/example'],
41+
['/project/node_modules/@nuxt/image/dist/module.mjs','/project','@nuxt/image/dist/module.mjs'],
42+
['/project/node_modules/foo/node_modules/bar/index.mjs','/project','bar/index.mjs'],
43+
['C:\\project\\modules\\example','C:\\project','modules/example'],
44+
['@nuxt/image','/project','@nuxt/image'],
45+
])('normalizes %s',(module,rootDir,expected)=>{
46+
expect(normalizeConfigModule(module,rootDir)).toBe(expected)
47+
expect(normalizeConfigModule([module,{}],rootDir)).toBe(expected)
48+
})
49+
50+
it('formats function modules',()=>{
51+
functionexampleModule(){}
52+
expect(normalizeConfigModule(exampleModule,'/project')).toBe('exampleModule()')
53+
})
54+
})
55+
})

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ const VERSIONS: Record<string, string> = {
1515
vi.mock('../../../src/utils/pkg',()=>({
1616
getPkgJSON: vi.fn((_cwd: string,pkg: string,options?: {via?: string[]})=>{
1717
if(pkg==='vite'&&options?.via?.includes('@nuxt/vite-builder')){
18+
if(_cwd==='/missing'){
19+
returnnull
20+
}
1821
if(_cwd==='/vite-plus'){
1922
return{name: '@voidzero-dev/vite-plus-core',version: '0.2.6',bundledVersions: {vite: '8.1.5'}}
2023
}
@@ -40,6 +43,10 @@ describe('getBuilder', () => {
4043
expect(getBuilder('/any','vite')).toEqual({name: 'Vite',version: '7.3.1'})
4144
})
4245

46+
it('reports an unknown vite version when vite is unavailable',()=>{
47+
expect(getBuilder('/missing','vite')).toEqual({name: 'Vite',version: 'unknown',provider: undefined})
48+
})
49+
4350
it('resolves the bundled vite version from Vite+',()=>{
4451
expect(getBuilder('/vite-plus','vite')).toEqual({
4552
name: 'Vite',
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
importprocessfrom'node:process'
2+
3+
import{describe,expect,it}from'vitest'
4+
5+
import{getPackageManagerVersion}from'../../../src/utils/packageManagers'
6+
7+
describe('getPackageManagerVersion',()=>{
8+
it('returns the command version',()=>{
9+
expect(getPackageManagerVersion(process.execPath)).toBe(process.version)
10+
})
11+
12+
it('does not fail when the package manager is unavailable',()=>{
13+
expect(getPackageManagerVersion('nuxt-cli-missing-package-manager')).toBe('unknown')
14+
})
15+
})

0 commit comments

Comments
 (0)