@@ -228,6 +228,127 @@ export const bar = 2;`,
228228 assert .Equal (t , len (stats .ProjectBuckets ), 1 )
229229 })
230230
231+ t .Run ("deleting node_modules leaves the registry prepared for importing" , func (t * testing.T ) {
232+ t .Parallel ()
233+ fixture := autoimporttestutil .SetupLifecycleSession (t , lifecycleProjectRoot , 1 )
234+ session := fixture .Session ()
235+ sessionUtils := fixture .Utils ()
236+ project := fixture .SingleProject ()
237+ mainFile := project .File (0 )
238+ ctx := context .Background ()
239+
240+ preferences := lsutil .NewDefaultUserPreferences ()
241+ preferences .IncludeCompletionsForModuleExports = core .TSTrue
242+ preferences .IncludeCompletionsForImportStatements = core .TSTrue
243+
244+ // Build auto-imports once so both buckets are clean and prepared.
245+ session .DidOpenFile (ctx , mainFile .URI (), 1 , mainFile .Content (), lsproto .LanguageKindTypeScript )
246+ _ , err := session .GetCurrentLanguageServiceWithAutoImports (ctx , mainFile .URI ())
247+ assert .NilError (t , err )
248+
249+ snapshot := session .Snapshot ()
250+ defaultProject := snapshot .GetDefaultProject (mainFile .URI ())
251+ assert .Assert (t , defaultProject != nil )
252+ projectPath := defaultProject .ConfigFilePath ()
253+ assert .Assert (t , snapshot .AutoImportRegistry ().IsPreparedForImportingFile (mainFile .FileName (), projectPath , preferences ))
254+ assert .Equal (t , len (autoImportStats (t , session ).NodeModulesBuckets ), 1 )
255+
256+ // Simulate the user deleting node_modules: remove the directory from disk
257+ // and notify the session of the deletion, which marks the node_modules
258+ // bucket dirty.
259+ nodeModulesDir := tspath .CombinePaths (project .Root (), "node_modules" )
260+ assert .NilError (t , sessionUtils .FS ().Remove (nodeModulesDir ))
261+ session .DidChangeWatchedFiles (ctx , []* lsproto.FileEvent {
262+ {Type : lsproto .FileChangeTypeDeleted , Uri : lsconv .FileNameToDocumentURI (nodeModulesDir )},
263+ })
264+
265+ // Re-preparing auto-imports must succeed and leave the registry prepared.
266+ // Before the fix, the node_modules bucket's rebuild bailed out with
267+ // vfs.ErrNotExist, leaving the stale dirty bucket in place so the registry
268+ // reported "needs rebuild" forever (which crashed the request handler).
269+ _ , err = session .GetCurrentLanguageServiceWithAutoImports (ctx , mainFile .URI ())
270+ assert .NilError (t , err )
271+
272+ snapshot = session .Snapshot ()
273+ assert .Assert (t , snapshot .AutoImportRegistry ().IsPreparedForImportingFile (mainFile .FileName (), projectPath , preferences ),
274+ "registry should be prepared after node_modules is deleted" )
275+ // The node_modules bucket should be removed entirely, not left behind as an
276+ // empty bucket.
277+ assert .Equal (t , len (autoImportStats (t , session ).NodeModulesBuckets ), 0 )
278+ })
279+
280+ t .Run ("deleting node_modules alongside a package.json change removes the bucket" , func (t * testing.T ) {
281+ t .Parallel ()
282+ fixture := autoimporttestutil .SetupLifecycleSession (t , lifecycleProjectRoot , 1 )
283+ session := fixture .Session ()
284+ sessionUtils := fixture .Utils ()
285+ project := fixture .SingleProject ()
286+ mainFile := project .File (0 )
287+ packageJSON := project .PackageJSONFile ()
288+ ctx := context .Background ()
289+
290+ preferences := lsutil .NewDefaultUserPreferences ()
291+ preferences .IncludeCompletionsForModuleExports = core .TSTrue
292+ preferences .IncludeCompletionsForImportStatements = core .TSTrue
293+
294+ session .DidOpenFile (ctx , mainFile .URI (), 1 , mainFile .Content (), lsproto .LanguageKindTypeScript )
295+ _ , err := session .GetCurrentLanguageServiceWithAutoImports (ctx , mainFile .URI ())
296+ assert .NilError (t , err )
297+
298+ snapshot := session .Snapshot ()
299+ defaultProject := snapshot .GetDefaultProject (mainFile .URI ())
300+ assert .Assert (t , defaultProject != nil )
301+ projectPath := defaultProject .ConfigFilePath ()
302+ assert .Equal (t , len (autoImportStats (t , session ).NodeModulesBuckets ), 1 )
303+
304+ // In a single changeset, edit package.json AND delete node_modules. The
305+ // package.json change must not prevent the now-missing node_modules bucket
306+ // from being removed.
307+ assert .NilError (t , sessionUtils .FS ().WriteFile (packageJSON .FileName (), `{"name": "app", "dependencies": {}}` ))
308+ nodeModulesDir := tspath .CombinePaths (project .Root (), "node_modules" )
309+ assert .NilError (t , sessionUtils .FS ().Remove (nodeModulesDir ))
310+ session .DidChangeWatchedFiles (ctx , []* lsproto.FileEvent {
311+ {Type : lsproto .FileChangeTypeChanged , Uri : packageJSON .URI ()},
312+ {Type : lsproto .FileChangeTypeDeleted , Uri : lsconv .FileNameToDocumentURI (nodeModulesDir )},
313+ })
314+
315+ _ , err = session .GetCurrentLanguageServiceWithAutoImports (ctx , mainFile .URI ())
316+ assert .NilError (t , err )
317+
318+ snapshot = session .Snapshot ()
319+ assert .Assert (t , snapshot .AutoImportRegistry ().IsPreparedForImportingFile (mainFile .FileName (), projectPath , preferences ))
320+ assert .Equal (t , len (autoImportStats (t , session ).NodeModulesBuckets ), 0 )
321+ })
322+
323+ t .Run ("deleting a package directory inside node_modules invalidates the bucket" , func (t * testing.T ) {
324+ t .Parallel ()
325+ fixture := autoimporttestutil .SetupLifecycleSession (t , lifecycleProjectRoot , 1 )
326+ session := fixture .Session ()
327+ sessionUtils := fixture .Utils ()
328+ project := fixture .SingleProject ()
329+ mainFile := project .File (0 )
330+ nodePackage := project .NodeModules ()[0 ]
331+ ctx := context .Background ()
332+
333+ session .DidOpenFile (ctx , mainFile .URI (), 1 , mainFile .Content (), lsproto .LanguageKindTypeScript )
334+ _ , err := session .GetCurrentLanguageServiceWithAutoImports (ctx , mainFile .URI ())
335+ assert .NilError (t , err )
336+ assert .Assert (t , singleBucket (t , autoImportStats (t , session ).NodeModulesBuckets ).ExportCount > 0 )
337+
338+ // Delete just the package directory, leaving node_modules itself in place. The
339+ // package's files are read transiently by the registry, so they are never tracked
340+ // in diskFiles/diskDirectories; only the directory deletion event is reported, and
341+ // it must survive snapshotfs filtering to invalidate the bucket.
342+ assert .NilError (t , sessionUtils .FS ().Remove (nodePackage .Directory ))
343+ session .DidChangeWatchedFiles (ctx , []* lsproto.FileEvent {
344+ {Type : lsproto .FileChangeTypeDeleted , Uri : lsconv .FileNameToDocumentURI (nodePackage .Directory )},
345+ })
346+
347+ _ , err = session .GetCurrentLanguageServiceWithAutoImports (ctx , mainFile .URI ())
348+ assert .NilError (t , err )
349+ assert .Equal (t , singleBucket (t , autoImportStats (t , session ).NodeModulesBuckets ).ExportCount , 0 )
350+ })
351+
231352 t .Run ("node_modules bucket dependency selection changes with open files" , func (t * testing.T ) {
232353 t .Parallel ()
233354 monorepoRoot := "/home/src/monorepo"
0 commit comments