@@ -76,8 +76,9 @@ export class TypingsLoader {
7676
7777 for ( const library of this . librariesToGetTypesFrom ) {
7878 // The library's package.json is where the type definitions are defined
79+ const packageJsonFsPath = `./node_modules/${ library } /package.json` ;
7980 const packageJsonContent = await this . webContainer . fs
80- . readFile ( `./node_modules/ ${ library } /package.json` , 'utf-8' )
81+ . readFile ( packageJsonFsPath , 'utf-8' )
8182 . catch ( ( error ) => {
8283 // Note: "ENOENT" errors occurs:
8384 // - While resetting the NodeRuntimeSandbox.
@@ -94,15 +95,31 @@ export class TypingsLoader {
9495 // if the package.json content is empty, skip this library
9596 if ( ! packageJsonContent ) continue ;
9697
98+ // Ensure the worker VFS also receives the package.json file so NodeNext resolution
99+ // can read "exports"/"types" information when resolving imports like '@angular/core'.
100+ filesToRead . push ( `/node_modules/${ library } /package.json` ) ;
101+
97102 const packageJson = JSON . parse ( packageJsonContent ) ;
98103
99- // If the package.json doesn't have `exports`, skip this library
104+ // If the package exposes a top-level types entry, include that directory as a fallback
105+ const topLevelTypes : string | undefined = packageJson . types ?? packageJson . typings ;
106+ if ( ! packageJson ?. exports && topLevelTypes ) {
107+ const path = `/node_modules/${ library } /${ this . normalizePath ( topLevelTypes ) } ` ;
108+ const directory = path . substring ( 0 , path . lastIndexOf ( '/' ) ) ;
109+ directoriesToRead . push ( directory ) ;
110+ continue ;
111+ }
112+
100113 if ( ! packageJson ?. exports ) continue ;
101114
102115 // Based on `exports` we can identify paths to the types definition files
103116 for ( const exportKey of Object . keys ( packageJson . exports ) ) {
104117 const exportEntry = packageJson . exports [ exportKey ] ;
105- const types : string | undefined = exportEntry . typings ?? exportEntry . types ;
118+ // Handle both object and string entries; for strings we can't infer types, so skip
119+ const types : string | undefined =
120+ exportEntry && typeof exportEntry === 'object'
121+ ? ( exportEntry . typings ?? exportEntry . types )
122+ : undefined ;
106123
107124 if ( types ) {
108125 const path = `/node_modules/${ library } /${ this . normalizePath ( types ) } ` ;
@@ -131,15 +148,73 @@ export class TypingsLoader {
131148 private async getTypeDefinitionFilesFromDirectory ( directory : string ) : Promise < string [ ] > {
132149 if ( ! this . webContainer ) throw new Error ( 'this.webContainer is not defined' ) ;
133150
134- const files = await this . webContainer . fs . readdir ( directory ) ;
151+ // Use a `visited` set to avoid loops/duplicates between recurses.
152+ return this . getTypeDefinitionFilesRecursively ( directory , new Set ( ) ) ;
153+ }
154+
155+ private async getTypeDefinitionFilesRecursively (
156+ directory : string ,
157+ visited : Set < string > ,
158+ ) : Promise < string [ ] > {
159+ if ( ! this . webContainer ) throw new Error ( 'this.webContainer is not defined' ) ;
160+
161+ // Normalize and deduplicate the current directory
162+ const dir = directory . replace ( / \/ + $ / , '' ) ;
163+ if ( visited . has ( dir ) ) return [ ] ;
164+ visited . add ( dir ) ;
165+
166+ const results : string [ ] = [ ] ;
167+
168+ // Read entries; if directory doesn't exist, ignore (optional exports)
169+ const entries = await this . webContainer . fs . readdir ( dir ) . catch ( ( error ) => {
170+ if ( error ?. message ?. startsWith ( 'ENOENT' ) ) return [ ] as string [ ] ;
171+ throw error ;
172+ } ) ;
173+
174+ // Deterministic sort: sort alfab.
175+ entries . sort ( ) ;
176+
177+ for ( const entry of entries ) {
178+ // Some FS (or test fakes) already return full paths.
179+ // Normalize to avoid `dir/dir/file`.
180+ const fullPath = entry . startsWith ( dir + '/' ) ? entry : `${ dir } /${ entry } ` ;
135181
136- return files . filter ( this . isTypeDefinitionFile ) . map ( ( file ) => `${ directory } /${ file } ` ) ;
182+ // If it's a `.d.ts`, add it and move on (don't try `readdir` on file)
183+ if ( this . isTypeDefinitionFile ( fullPath ) ) {
184+ results . push ( fullPath ) ;
185+ continue ;
186+ }
187+
188+ // Avoid recursively going down paths that are likely files (e.g. .js/.mjs)
189+ if ( this . isProbablyAFile ( fullPath ) ) {
190+ continue ;
191+ }
192+
193+ // Try reading it as a directory.
194+ const children = await this . webContainer . fs . readdir ( fullPath ) . catch ( ( ) => null ) ;
195+
196+ // Only if `children` is a non-empty array do we consider it a directory and descend.
197+ if ( Array . isArray ( children ) && children . length > 0 ) {
198+ const nested = await this . getTypeDefinitionFilesRecursively ( fullPath , visited ) ;
199+ results . push ( ...nested ) ;
200+ }
201+ }
202+
203+ // Dedup and deterministic order
204+ const uniqueSorted = Array . from ( new Set ( results ) ) . sort ( ) ;
205+ return uniqueSorted ;
137206 }
138207
139208 private isTypeDefinitionFile ( path : string ) : boolean {
140209 return path . endsWith ( '.d.ts' ) ;
141210 }
142211
212+ private isProbablyAFile ( path : string ) : boolean {
213+ // Consider any path whose last segment contains a period to be a "file" (e.g., index.js, index.mjs)
214+ // Example regex: '/something/index.js' -> true; '/something/nested' -> false
215+ return / \/ [ ^ \/ ] + \. [ ^ \/ ] + $ / . test ( path ) ;
216+ }
217+
143218 private normalizePath ( path : string ) : string {
144219 if ( path . startsWith ( './' ) ) {
145220 return path . substring ( 2 ) ;
0 commit comments