@@ -5,7 +5,7 @@ import {type PackageJson} from '@sanity/cli-core'
55import { moduleResolve } from 'import-meta-resolve'
66import { afterEach , describe , expect , test , vi } from 'vitest'
77
8- import { getLocalPackageVersion } from '../getLocalPackageVersion.js'
8+ import { getLocalPackageDir , getLocalPackageVersion } from '../getLocalPackageVersion.js'
99
1010const mockReadPackageJson = vi . hoisted ( ( ) => vi . fn ( ) )
1111
@@ -39,6 +39,7 @@ describe('getLocalPackageVersion', () => {
3939 const mockPackageUrl = pathToFileURL (
4040 resolve ( mockWorkDir , 'node_modules' , mockModuleId , 'package.json' ) ,
4141 )
42+ const expectedPackageDir = resolve ( mockWorkDir , 'node_modules' , mockModuleId )
4243 const mockVersion = '1.0.0'
4344
4445 mockedModuleResolve . mockReturnValueOnce ( mockPackageUrl )
@@ -53,31 +54,30 @@ describe('getLocalPackageVersion', () => {
5354 `${ mockModuleId } /package.json` ,
5455 pathToFileURL ( resolve ( mockWorkDir , 'noop.js' ) ) ,
5556 )
56- expect ( mockReadPackageJson ) . toHaveBeenCalledWith ( mockPackageUrl )
57+ expect ( mockReadPackageJson ) . toHaveBeenCalledWith ( join ( expectedPackageDir , 'package.json' ) )
5758 expect ( result ) . toBe ( mockVersion )
5859 } )
5960
6061 test ( 'returns null when readPackageJson throws' , async ( ) => {
6162 const mockPackageUrl = pathToFileURL (
6263 resolve ( mockWorkDir , 'node_modules' , mockModuleId , 'package.json' ) ,
6364 )
65+ const expectedPackageDir = resolve ( mockWorkDir , 'node_modules' , mockModuleId )
6466
6567 mockedModuleResolve . mockReturnValueOnce ( mockPackageUrl )
6668 mockReadPackageJson . mockRejectedValueOnce ( new Error ( 'Failed to read package.json' ) )
6769
6870 const result = await getLocalPackageVersion ( mockModuleId , mockWorkDir )
6971
7072 expect ( mockedModuleResolve ) . toHaveBeenCalledOnce ( )
71- expect ( mockReadPackageJson ) . toHaveBeenCalledWith ( mockPackageUrl )
73+ expect ( mockReadPackageJson ) . toHaveBeenCalledWith ( join ( expectedPackageDir , 'package.json' ) )
7274 expect ( result ) . toBeNull ( )
7375 } )
7476
7577 test ( 'returns version via fallback when package has strict exports' , async ( ) => {
7678 const mainEntryPath = resolve ( mockWorkDir , 'node_modules' , mockModuleId , 'dist' , 'index.js' )
7779 const mainEntryUrl = pathToFileURL ( mainEntryPath )
78- const expectedPackageJsonUrl = pathToFileURL (
79- join ( resolve ( mockWorkDir , 'node_modules' , mockModuleId ) , 'package.json' ) ,
80- )
80+ const expectedPackageDir = resolve ( mockWorkDir , 'node_modules' , mockModuleId )
8181 const mockVersion = '2.0.0'
8282 const dirUrl = pathToFileURL ( resolve ( mockWorkDir , 'noop.js' ) )
8383
@@ -97,7 +97,7 @@ describe('getLocalPackageVersion', () => {
9797 expect ( mockedModuleResolve ) . toHaveBeenCalledTimes ( 2 )
9898 expect ( mockedModuleResolve ) . toHaveBeenNthCalledWith ( 1 , `${ mockModuleId } /package.json` , dirUrl )
9999 expect ( mockedModuleResolve ) . toHaveBeenNthCalledWith ( 2 , mockModuleId , dirUrl )
100- expect ( mockReadPackageJson ) . toHaveBeenCalledWith ( expectedPackageJsonUrl )
100+ expect ( mockReadPackageJson ) . toHaveBeenCalledWith ( join ( expectedPackageDir , 'package.json' ) )
101101 expect ( result ) . toBe ( mockVersion )
102102 } )
103103
@@ -173,3 +173,79 @@ describe('getLocalPackageVersion', () => {
173173 expect ( result ) . toBeNull ( )
174174 } )
175175} )
176+
177+ describe ( 'getLocalPackageDir' , ( ) => {
178+ const mockWorkDir = '/mock/work/dir'
179+ const mockModuleId = '@sanity/test'
180+
181+ afterEach ( ( ) => {
182+ vi . clearAllMocks ( )
183+ } )
184+
185+ test ( 'returns package directory when package.json is resolved' , ( ) => {
186+ const mockPackageUrl = pathToFileURL (
187+ resolve ( mockWorkDir , 'node_modules' , mockModuleId , 'package.json' ) ,
188+ )
189+
190+ mockedModuleResolve . mockReturnValueOnce ( mockPackageUrl )
191+
192+ const result = getLocalPackageDir ( mockModuleId , mockWorkDir )
193+
194+ expect ( result ) . toBe ( resolve ( mockWorkDir , 'node_modules' , mockModuleId ) )
195+ expect ( mockedModuleResolve ) . toHaveBeenCalledWith (
196+ `${ mockModuleId } /package.json` ,
197+ pathToFileURL ( resolve ( mockWorkDir , 'noop.js' ) ) ,
198+ )
199+ } )
200+
201+ test ( 'resolves hoisted packages in monorepo root node_modules' , ( ) => {
202+ // Simulate a monorepo where react is hoisted to root
203+ const monorepoRoot = '/project'
204+ const workspaceDir = '/project/packages/frontend'
205+ const hoistedPackageUrl = pathToFileURL (
206+ resolve ( monorepoRoot , 'node_modules' , 'react' , 'package.json' ) ,
207+ )
208+
209+ mockedModuleResolve . mockReturnValueOnce ( hoistedPackageUrl )
210+
211+ const result = getLocalPackageDir ( 'react' , workspaceDir )
212+
213+ expect ( result ) . toBe ( resolve ( monorepoRoot , 'node_modules' , 'react' ) )
214+ } )
215+
216+ test ( 'falls back to main entry point when package.json is not exported' , ( ) => {
217+ const mainEntryPath = resolve ( mockWorkDir , 'node_modules' , mockModuleId , 'dist' , 'index.js' )
218+ const mainEntryUrl = pathToFileURL ( mainEntryPath )
219+
220+ mockedModuleResolve
221+ . mockImplementationOnce ( ( ) => {
222+ throw createNodeError ( 'ERR_PACKAGE_PATH_NOT_EXPORTED' , 'Package path not exported' )
223+ } )
224+ . mockReturnValueOnce ( mainEntryUrl )
225+
226+ const result = getLocalPackageDir ( mockModuleId , mockWorkDir )
227+
228+ expect ( result ) . toBe ( resolve ( mockWorkDir , 'node_modules' , mockModuleId ) )
229+ expect ( mockedModuleResolve ) . toHaveBeenCalledTimes ( 2 )
230+ } )
231+
232+ test ( 'throws when moduleResolve throws a non-fallback error' , ( ) => {
233+ mockedModuleResolve . mockImplementationOnce ( ( ) => {
234+ throw createNodeError ( 'ERR_MODULE_NOT_FOUND' , 'Module not found' )
235+ } )
236+
237+ expect ( ( ) => getLocalPackageDir ( mockModuleId , mockWorkDir ) ) . toThrow ( 'Module not found' )
238+ } )
239+
240+ test ( 'throws when both resolution strategies fail' , ( ) => {
241+ mockedModuleResolve
242+ . mockImplementationOnce ( ( ) => {
243+ throw createNodeError ( 'ERR_PACKAGE_PATH_NOT_EXPORTED' , 'Package path not exported' )
244+ } )
245+ . mockImplementationOnce ( ( ) => {
246+ throw createNodeError ( 'ERR_MODULE_NOT_FOUND' , 'Module not found' )
247+ } )
248+
249+ expect ( ( ) => getLocalPackageDir ( mockModuleId , mockWorkDir ) ) . toThrow ( 'Module not found' )
250+ } )
251+ } )
0 commit comments