@@ -118,7 +118,11 @@ export class InstallRecipes {
118118
119119 let recipeModule ;
120120 try {
121- setupSharedDependencies ( resolvedPath ) ;
121+ // Pre-load core modules that are used by recipes but loaded lazily
122+ // This ensures they're in require.cache before setupSharedDependencies runs
123+ preloadCoreModules ( logger ) ;
124+
125+ setupSharedDependencies ( resolvedPath , logger ) ;
122126 recipeModule = require ( resolvedPath ) ;
123127 } catch ( e : any ) {
124128 throw new Error ( `Failed to load recipe module from ${ resolvedPath } : ${ e . stack } ` ) ;
@@ -142,38 +146,137 @@ export class InstallRecipes {
142146 }
143147}
144148
149+ /**
150+ * Pre-loads core modules that are typically loaded lazily by recipes.
151+ * This ensures they're in require.cache before setupSharedDependencies runs,
152+ * so they can be properly mapped to avoid instanceof failures.
153+ */
154+ function preloadCoreModules ( logger ?: rpc . Logger ) {
155+ const modulesToPreload = [
156+ '../..' ,
157+ '../../java' ,
158+ '../../javascript' ,
159+ '../../json' ,
160+ '../../rpc' ,
161+ '../../search' ,
162+ '../../text' ,
163+ ] ;
164+
165+ modulesToPreload . forEach ( modulePath => {
166+ try {
167+ require ( modulePath ) ;
168+ if ( logger ) {
169+ logger . info ( `[preloadCoreModules] Loaded ${ modulePath } ` ) ;
170+ }
171+ } catch ( e ) {
172+ if ( logger ) {
173+ logger . warn ( `[preloadCoreModules] Failed to load ${ modulePath } : ${ e } ` ) ;
174+ }
175+ }
176+ } ) ;
177+ }
178+
145179/**
146180 * Ensures dynamically loaded modules share the same class instances as the host
147181 * by mapping require.cache entries. This prevents instanceof failures when the
148182 * same package is installed in multiple node_modules directories.
149183 */
150- function setupSharedDependencies ( targetModulePath : string ) {
184+ function setupSharedDependencies ( targetModulePath : string , logger ?: rpc . Logger ) {
151185 const sharedDeps = [ '@openrewrite/rewrite' , 'vscode-jsonrpc' ] ;
152186 const targetDir = path . dirname ( targetModulePath ) ;
153187
154188 sharedDeps . forEach ( depName => {
155- const depPattern = path . sep + 'node_modules' + path . sep + depName . replace ( '/' , path . sep ) ;
156-
157- for ( const cachedPath of Object . keys ( require . cache ) ) {
158- if ( ! cachedPath . includes ( depPattern ) ) continue ;
159-
160- try {
161- // Extract subpath: /path/node_modules/@pkg /dist/tree.js -> dist/tree.js
162- const pkgIndex = cachedPath . indexOf ( depPattern ) ;
163- let subpath = cachedPath . substring ( pkgIndex + depPattern . length )
164- . replace ( / ^ [ / \\ ] / , '' ) // Remove leading slash
165- . replace ( / \. ( j s | t s ) $ / , '' ) // Remove extension
166- . replace ( / ^ d i s t [ / \\ ] / , '' ) // Remove dist/ prefix if present
167- . replace ( / [ / \\ ] i n d e x $ / , '' ) ; // Remove /index suffix
168-
169- // Build require path: @pkg or @pkg/subpath
170- const requirePath = subpath ? `${ depName } /${ subpath } ` : depName ;
171-
172- // Resolve from target's perspective and map cache
173- const targetDepPath = require . resolve ( requirePath , { paths : [ targetDir ] } ) ;
174- require . cache [ targetDepPath ] = require . cache [ cachedPath ] ;
175- } catch ( e ) {
176- // Target can't resolve this path, skip
189+ try {
190+ // Step 1: Find where this package is currently loaded from (host)
191+ const hostPackageEntry = require . resolve ( depName ) ;
192+
193+ // Step 2: Find the package root by looking for package.json
194+ let hostPackageRoot = path . dirname ( hostPackageEntry ) ;
195+ while ( hostPackageRoot !== path . dirname ( hostPackageRoot ) ) {
196+ const packageJsonPath = path . join ( hostPackageRoot , 'package.json' ) ;
197+ if ( fs . existsSync ( packageJsonPath ) ) {
198+ try {
199+ const pkg = JSON . parse ( fs . readFileSync ( packageJsonPath , 'utf8' ) ) ;
200+ if ( pkg . name === depName ) {
201+ break ; // Found the package root
202+ }
203+ } catch ( e ) {
204+ // Not a valid package.json, continue
205+ }
206+ }
207+ hostPackageRoot = path . dirname ( hostPackageRoot ) ;
208+ }
209+
210+ if ( logger ) {
211+ logger . info ( `[setupSharedDependencies] Host package root for ${ depName } : ${ hostPackageRoot } ` ) ;
212+ }
213+
214+ // Step 3: Find where the target's node_modules has this package
215+ // We explicitly look in node_modules to avoid finding npm-linked global packages
216+ let targetPackageRoot : string | undefined ;
217+
218+ // Walk up from targetDir looking for node_modules containing this package
219+ let searchDir = targetDir ;
220+ while ( searchDir !== path . dirname ( searchDir ) ) {
221+ const nodeModulesPath = path . join ( searchDir , 'node_modules' , ...depName . split ( '/' ) ) ;
222+ if ( fs . existsSync ( nodeModulesPath ) ) {
223+ const packageJsonPath = path . join ( nodeModulesPath , 'package.json' ) ;
224+ if ( fs . existsSync ( packageJsonPath ) ) {
225+ try {
226+ const pkg = JSON . parse ( fs . readFileSync ( packageJsonPath , 'utf8' ) ) ;
227+ if ( pkg . name === depName ) {
228+ targetPackageRoot = nodeModulesPath ;
229+ break ;
230+ }
231+ } catch ( e ) {
232+ // Not a valid package.json, continue
233+ }
234+ }
235+ }
236+ searchDir = path . dirname ( searchDir ) ;
237+ }
238+
239+ if ( ! targetPackageRoot ) {
240+ if ( logger ) {
241+ logger . warn ( `[setupSharedDependencies] Could not find ${ depName } in target's node_modules` ) ;
242+ }
243+ return ; // Can't map this package
244+ }
245+
246+ if ( logger ) {
247+ logger . info ( `[setupSharedDependencies] Target package root for ${ depName } : ${ targetPackageRoot } ` ) ;
248+ }
249+
250+ // If they're the same, no mapping needed
251+ if ( hostPackageRoot === targetPackageRoot ) {
252+ if ( logger ) {
253+ logger . info ( `[setupSharedDependencies] Same package root, no mapping needed for ${ depName } ` ) ;
254+ }
255+ return ;
256+ }
257+
258+ // Step 4: Map all cached modules from host package to target package
259+ const hostPrefix = hostPackageRoot + path . sep ;
260+
261+ let mappedCount = 0 ;
262+ for ( const cachedPath of Object . keys ( require . cache ) ) {
263+ if ( cachedPath . startsWith ( hostPrefix ) ) {
264+ // This module belongs to the host package
265+ const relativePath = cachedPath . substring ( hostPrefix . length ) ;
266+ const targetPath = path . join ( targetPackageRoot , relativePath ) ;
267+
268+ // Map the target path to use the host's cached module
269+ require . cache [ targetPath ] = require . cache [ cachedPath ] ;
270+ mappedCount ++ ;
271+ }
272+ }
273+
274+ if ( logger ) {
275+ logger . info ( `[setupSharedDependencies] Mapped ${ mappedCount } modules for ${ depName } ` ) ;
276+ }
277+ } catch ( e ) {
278+ if ( logger ) {
279+ logger . error ( `[setupSharedDependencies] Failed to setup ${ depName } : ${ e } ` ) ;
177280 }
178281 }
179282 } ) ;
0 commit comments