From 85c83a1fd8934fd8370b222f865ad521fb0545fe Mon Sep 17 00:00:00 2001 From: JongYun Jeong <95991290+BellYun@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:12:05 +0900 Subject: [PATCH 1/7] fix: guard banner against unresolved vite package --- packages/nuxi/src/utils/banner.ts | 4 ++-- packages/nuxi/test/unit/utils/banner.spec.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 packages/nuxi/test/unit/utils/banner.spec.ts diff --git a/packages/nuxi/src/utils/banner.ts b/packages/nuxi/src/utils/banner.ts index 81fe65705..35c8bb463 100644 --- a/packages/nuxi/src/utils/banner.ts +++ b/packages/nuxi/src/utils/banner.ts @@ -17,8 +17,8 @@ export function getBuilder(cwd: string, builder: Exclude ({ + getPkgJSON: vi.fn(() => null), + getPkgVersion: vi.fn(() => ''), +})) + +const { getBuilder } = await import('../../../src/utils/banner') + +describe('getBuilder', () => { + // regression for banner crash under pnpm globalVirtualStore + // where vite is unreachable from cwd — getPkgJSON returns null + it('does not throw when vite package.json cannot be resolved', () => { + expect(() => getBuilder('/any', 'vite')).not.toThrow() + expect(getBuilder('/any', 'vite')).toMatchObject({ name: 'Vite', version: '' }) + }) +}) From 0bcef242fd214a896d1a2bb45a18bf389c3ee5cb Mon Sep 17 00:00:00 2001 From: JongYun Jeong <95991290+BellYun@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:02:33 +0900 Subject: [PATCH 2/7] fix: resolve vite via @nuxt/vite-builder under strict isolation --- packages/nuxi/src/utils/banner.ts | 6 ++--- packages/nuxi/src/utils/versions.ts | 24 ++++++++++++++------ packages/nuxi/test/unit/utils/banner.spec.ts | 20 ++++++++++++---- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/nuxi/src/utils/banner.ts b/packages/nuxi/src/utils/banner.ts index 35c8bb463..ac061d486 100644 --- a/packages/nuxi/src/utils/banner.ts +++ b/packages/nuxi/src/utils/banner.ts @@ -16,9 +16,9 @@ export function getBuilder(cwd: string, builder: Exclude !!v) + const searchFrom = [...roots] + + if (options?.via) { + for (const from of roots) { + const viaPath = resolveModulePath(options.via, { from, try: true }) + if (viaPath) { + searchFrom.push(viaPath) + break + } } - const p = resolveModulePath(`${pkg}/package.json`, { from: url, try: true }) + } + + for (const from of searchFrom) { + const p = resolveModulePath(`${pkg}/package.json`, { from, try: true }) if (p) { return JSON.parse(readFileSync(p, 'utf-8')) } diff --git a/packages/nuxi/test/unit/utils/banner.spec.ts b/packages/nuxi/test/unit/utils/banner.spec.ts index ea3dde2f6..a1705a203 100644 --- a/packages/nuxi/test/unit/utils/banner.spec.ts +++ b/packages/nuxi/test/unit/utils/banner.spec.ts @@ -1,7 +1,14 @@ import { describe, expect, it, vi } from 'vitest' vi.mock('../../../src/utils/versions', () => ({ - getPkgJSON: vi.fn(() => null), + getPkgJSON: vi.fn((_cwd: string, pkg: string, options?: { via?: string }) => { + // simulate pnpm globalVirtualStore: vite not reachable from cwd or nuxt, + // only reachable through @nuxt/vite-builder's isolated deps + if (pkg === 'vite' && options?.via === '@nuxt/vite-builder') { + return { name: 'vite', version: '7.3.1' } + } + return null + }), getPkgVersion: vi.fn(() => ''), })) @@ -9,9 +16,14 @@ const { getBuilder } = await import('../../../src/utils/banner') describe('getBuilder', () => { // regression for banner crash under pnpm globalVirtualStore - // where vite is unreachable from cwd — getPkgJSON returns null - it('does not throw when vite package.json cannot be resolved', () => { + it('does not throw when vite package.json cannot be resolved anywhere', async () => { + const { getPkgJSON } = await import('../../../src/utils/versions') + vi.mocked(getPkgJSON).mockReturnValueOnce(null).mockReturnValueOnce(null) expect(() => getBuilder('/any', 'vite')).not.toThrow() - expect(getBuilder('/any', 'vite')).toMatchObject({ name: 'Vite', version: '' }) + }) + + // proper fix: recover version via @nuxt/vite-builder under strict isolation + it('resolves vite version via @nuxt/vite-builder when direct lookup fails', () => { + expect(getBuilder('/any', 'vite')).toEqual({ name: 'Vite', version: '7.3.1' }) }) }) From 43d8f08e68d9cba4506f329b1de3fa4607113861 Mon Sep 17 00:00:00 2001 From: JongYun Jeong <95991290+BellYun@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:31:29 +0900 Subject: [PATCH 3/7] fix: resolve vite through @nuxt/vite-builder instead of direct lookup --- packages/nuxi/src/utils/banner.ts | 2 +- packages/nuxi/test/unit/utils/banner.spec.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/nuxi/src/utils/banner.ts b/packages/nuxi/src/utils/banner.ts index ac061d486..426a11128 100644 --- a/packages/nuxi/src/utils/banner.ts +++ b/packages/nuxi/src/utils/banner.ts @@ -16,7 +16,7 @@ export function getBuilder(cwd: string, builder: Exclude ({ getPkgJSON: vi.fn((_cwd: string, pkg: string, options?: { via?: string }) => { - // simulate pnpm globalVirtualStore: vite not reachable from cwd or nuxt, - // only reachable through @nuxt/vite-builder's isolated deps if (pkg === 'vite' && options?.via === '@nuxt/vite-builder') { return { name: 'vite', version: '7.3.1' } } @@ -15,15 +13,13 @@ vi.mock('../../../src/utils/versions', () => ({ const { getBuilder } = await import('../../../src/utils/banner') describe('getBuilder', () => { - // regression for banner crash under pnpm globalVirtualStore - it('does not throw when vite package.json cannot be resolved anywhere', async () => { + it('does not throw when vite package.json cannot be resolved', async () => { const { getPkgJSON } = await import('../../../src/utils/versions') - vi.mocked(getPkgJSON).mockReturnValueOnce(null).mockReturnValueOnce(null) + vi.mocked(getPkgJSON).mockReturnValueOnce(null) expect(() => getBuilder('/any', 'vite')).not.toThrow() }) - // proper fix: recover version via @nuxt/vite-builder under strict isolation - it('resolves vite version via @nuxt/vite-builder when direct lookup fails', () => { + it('resolves vite version via @nuxt/vite-builder', () => { expect(getBuilder('/any', 'vite')).toEqual({ name: 'Vite', version: '7.3.1' }) }) }) From 012d0f63ef737a7b3216890cb6a067f388474f34 Mon Sep 17 00:00:00 2001 From: JongYun Jeong <95991290+BellYun@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:48:43 +0900 Subject: [PATCH 4/7] fix: prioritize via resolution path over generic roots --- packages/nuxi/src/utils/versions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nuxi/src/utils/versions.ts b/packages/nuxi/src/utils/versions.ts index 17c1dcbe4..3d8269c56 100644 --- a/packages/nuxi/src/utils/versions.ts +++ b/packages/nuxi/src/utils/versions.ts @@ -22,7 +22,7 @@ export function getPkgVersion(cwd: string, pkg: string, options?: { via?: string export function getPkgJSON(cwd: string, pkg: string, options?: { via?: string }) { const roots = [cwd, tryResolveNuxt(cwd)].filter((v): v is string => !!v) - const searchFrom = [...roots] + const searchFrom: string[] = [] if (options?.via) { for (const from of roots) { @@ -34,6 +34,8 @@ export function getPkgJSON(cwd: string, pkg: string, options?: { via?: string }) } } + searchFrom.push(...roots) + for (const from of searchFrom) { const p = resolveModulePath(`${pkg}/package.json`, { from, try: true }) if (p) { From c98f287e9b869eea152ddb3bf6c6ea5375dcd020 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 21 Apr 2026 13:10:55 +0100 Subject: [PATCH 5/7] fix: also walk dependencies for other deps --- packages/nuxi/src/utils/banner.ts | 11 +++-- packages/nuxi/src/utils/versions.ts | 49 +++++++++++++++----- packages/nuxi/test/unit/utils/banner.spec.ts | 24 ++++++++-- 3 files changed, 63 insertions(+), 21 deletions(-) diff --git a/packages/nuxi/src/utils/banner.ts b/packages/nuxi/src/utils/banner.ts index 426a11128..1d0268e5a 100644 --- a/packages/nuxi/src/utils/banner.ts +++ b/packages/nuxi/src/utils/banner.ts @@ -9,14 +9,14 @@ export function getBuilder(cwd: string, builder: Exclude !!v) - const searchFrom: string[] = [] +/** + * Resolve a package.json, optionally walking a dependency chain. + * + * `via` is an array of `[startingPoint, ...intermediates]` describing + * the dependency path to walk before resolving `pkg`. For example: + * + * // vite is a dep of @nuxt/vite-builder, which is a dep of nuxt + * getPkgJSON(cwd, 'vite', { via: ['nuxt', '@nuxt/vite-builder'] }) + * + * // webpack is a dep of @nuxt/webpack-builder, which the user installs + * getPkgJSON(cwd, 'webpack', { via: ['@nuxt/webpack-builder'] }) + * + * Each entry is resolved from the location of the previous one, + * starting from cwd. Falls back to direct resolution from cwd/nuxt. + */ +export function getPkgJSON(cwd: string, pkg: string, options?: { via?: string[] }) { + // Build list of locations to try resolving pkg from. + // When `via` is provided, walk the chain first; then fall back to cwd/nuxt. + const roots: string[] = [] - if (options?.via) { - for (const from of roots) { - const viaPath = resolveModulePath(options.via, { from, try: true }) - if (viaPath) { - searchFrom.push(viaPath) + if (options?.via && options.via.length > 0) { + let from: string | undefined = cwd + for (const step of options.via) { + from = resolveModulePath(step, { from, try: true }) ?? undefined + if (!from) { break } } + if (from) { + roots.push(from) + } } - searchFrom.push(...roots) + // Fallback: direct resolution from cwd or nuxt's location + roots.push(cwd) + const nuxtPath = tryResolveNuxt(cwd) + if (nuxtPath) { + roots.push(nuxtPath) + } - for (const from of searchFrom) { - const p = resolveModulePath(`${pkg}/package.json`, { from, try: true }) + for (const root of roots) { + const p = resolveModulePath(`${pkg}/package.json`, { from: root, try: true }) if (p) { return JSON.parse(readFileSync(p, 'utf-8')) } } + return null } diff --git a/packages/nuxi/test/unit/utils/banner.spec.ts b/packages/nuxi/test/unit/utils/banner.spec.ts index 677601eb9..7feac83d3 100644 --- a/packages/nuxi/test/unit/utils/banner.spec.ts +++ b/packages/nuxi/test/unit/utils/banner.spec.ts @@ -1,13 +1,21 @@ import { describe, expect, it, vi } from 'vitest' vi.mock('../../../src/utils/versions', () => ({ - getPkgJSON: vi.fn((_cwd: string, pkg: string, options?: { via?: string }) => { - if (pkg === 'vite' && options?.via === '@nuxt/vite-builder') { + getPkgJSON: vi.fn((_cwd: string, pkg: string, options?: { via?: string[] }) => { + if (pkg === 'vite' && options?.via?.includes('@nuxt/vite-builder')) { return { name: 'vite', version: '7.3.1' } } return null }), - getPkgVersion: vi.fn(() => ''), + getPkgVersion: vi.fn((_cwd: string, pkg: string, options?: { via?: string[] }) => { + if (pkg === 'webpack' && options?.via?.includes('@nuxt/webpack-builder')) { + return '5.99.0' + } + if (pkg === '@rspack/core' && options?.via?.includes('@nuxt/rspack-builder')) { + return '1.3.0' + } + return '' + }), })) const { getBuilder } = await import('../../../src/utils/banner') @@ -19,7 +27,15 @@ describe('getBuilder', () => { expect(() => getBuilder('/any', 'vite')).not.toThrow() }) - it('resolves vite version via @nuxt/vite-builder', () => { + it('resolves vite version via nuxt -> @nuxt/vite-builder', () => { expect(getBuilder('/any', 'vite')).toEqual({ name: 'Vite', version: '7.3.1' }) }) + + it('resolves webpack version via @nuxt/webpack-builder', () => { + expect(getBuilder('/any', 'webpack')).toEqual({ name: 'Webpack', version: '5.99.0' }) + }) + + it('resolves rspack version via @nuxt/rspack-builder', () => { + expect(getBuilder('/any', 'rspack')).toEqual({ name: 'Rspack', version: '1.3.0' }) + }) }) From 04ac99eca63088b686697b254ed837401792ae12 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 21 Apr 2026 13:15:16 +0100 Subject: [PATCH 6/7] chore: remove optional chaining --- packages/nuxi/src/utils/banner.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxi/src/utils/banner.ts b/packages/nuxi/src/utils/banner.ts index 1d0268e5a..6e026592d 100644 --- a/packages/nuxi/src/utils/banner.ts +++ b/packages/nuxi/src/utils/banner.ts @@ -17,8 +17,8 @@ export function getBuilder(cwd: string, builder: Exclude Date: Tue, 21 Apr 2026 13:16:13 +0100 Subject: [PATCH 7/7] test: remove test about throwing --- packages/nuxi/test/unit/utils/banner.spec.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/nuxi/test/unit/utils/banner.spec.ts b/packages/nuxi/test/unit/utils/banner.spec.ts index 7feac83d3..6e5d4cb42 100644 --- a/packages/nuxi/test/unit/utils/banner.spec.ts +++ b/packages/nuxi/test/unit/utils/banner.spec.ts @@ -21,12 +21,6 @@ vi.mock('../../../src/utils/versions', () => ({ const { getBuilder } = await import('../../../src/utils/banner') describe('getBuilder', () => { - it('does not throw when vite package.json cannot be resolved', async () => { - const { getPkgJSON } = await import('../../../src/utils/versions') - vi.mocked(getPkgJSON).mockReturnValueOnce(null) - expect(() => getBuilder('/any', 'vite')).not.toThrow() - }) - it('resolves vite version via nuxt -> @nuxt/vite-builder', () => { expect(getBuilder('/any', 'vite')).toEqual({ name: 'Vite', version: '7.3.1' }) })