@@ -57,13 +57,20 @@ type BucketState struct {
5757 // the bucket was built. If changed, the bucket should be rebuilt.
5858 fileExcludePatterns []string
5959 // dirtyPackages is the set of package names that need to be re-indexed.
60- // This is used for granular updates: when a file in a project reference package
60+ // This is used for granular updates: when a file in a local workspace package
6161 // changes, only that package needs to be re-extracted rather than rebuilding
62- // the entire node_modules bucket. If nil, no granular updates are pending.
62+ // the entire node_modules bucket.
63+ // If nil, no granular updates are pending.
6364 // If set but multipleFilesDirty is true, the entire bucket needs to be rebuilt.
6465 dirtyPackages * collections.Set [string ]
6566}
6667
68+ func (b BucketState ) Clone () BucketState {
69+ b .fileExcludePatterns = slices .Clone (b .fileExcludePatterns )
70+ b .dirtyPackages = b .dirtyPackages .Clone ()
71+ return b
72+ }
73+
6774func (b BucketState ) Dirty () bool {
6875 return b .multipleFilesDirty || b .dirtyFile != "" || b .newProgramStructure > 0 || b .dirtyPackages .Len () > 0
6976}
@@ -98,31 +105,50 @@ type RegistryBucket struct {
98105
99106 // Paths maps file paths to package names. For project buckets, the package name
100107 // is always empty string. For node_modules buckets, this enables reverse lookup
101- // from path to package for granular updates. Only paths eligible for granular
102- // update (project reference packages) have entries here.
108+ // from path to package for granular updates. Only paths for local workspace
109+ // packages (symlinked and within the workspace root) have entries here, since
110+ // their realpaths are outside node_modules and need reverse lookup for dirty
111+ // detection.
112+ //
113+ // Paths is considered immutable after the bucket is finalized.
114+ // It should be fully replaced rather than mutated while changing a bucket.
103115 Paths map [tspath.Path ]string
104116 // PackageFiles maps package names to their file paths and file names.
105117 // All package directory names in node_modules are keys; indexed packages have
106118 // non-nil maps with path→fileName entries, unindexed packages have nil maps.
107119 // This enables efficient removal of a package's files during granular updates
108120 // without iterating through all entries. Only defined for node_modules buckets.
121+ //
122+ // PackageFiles is considered immutable after the bucket is finalized.
123+ // It should be fully replaced rather than mutated while changing a bucket.
109124 PackageFiles map [string ]map [tspath.Path ]string
110125 // ResolvedPackageNames is only defined for project buckets. It is the set of
111126 // package names that were resolved from imports in the project's program files.
112127 // This is passed to node_modules buckets so they include packages that are
113128 // directly imported even if not listed in package.json dependencies.
129+ //
130+ // ResolvedPackageNames is considered immutable after the bucket is finalized.
131+ // It should be fully replaced rather than mutated while changing a bucket.
114132 ResolvedPackageNames * collections.Set [string ]
115133 // DependencyNames is only defined for node_modules buckets. It is the set of
116134 // package names that will be included in the bucket if present in the directory,
117135 // computed from package.json dependencies plus resolved package names from
118136 // active programs. If nil, all packages are included because at least one open
119137 // file has access to this node_modules directory without being filtered by a
120138 // package.json.
139+ //
140+ // DependencyNames is considered immutable after the bucket is finalized.
141+ // It should be fully replaced rather than mutated while changing a bucket.
121142 DependencyNames * collections.Set [string ]
122143 // AmbientModuleNames is only defined for node_modules buckets. It is the set of
123144 // ambient module names found while extracting exports in the bucket.
145+ //
146+ // AmbientModuleNames is considered immutable after the bucket is finalized.
147+ // It should be fully replaced rather than mutated while changing a bucket.
124148 AmbientModuleNames map [string ][]string
125- Index * Index [* Export ]
149+ // Index is considered immutable after the bucket is finalized.
150+ // It should be cloned and replaced rather than mutated while changing a bucket.
151+ Index * Index [* Export ]
126152}
127153
128154func newRegistryBucket () * RegistryBucket {
@@ -136,7 +162,7 @@ func newRegistryBucket() *RegistryBucket {
136162
137163func (b * RegistryBucket ) Clone () * RegistryBucket {
138164 return & RegistryBucket {
139- state : b .state ,
165+ state : b .state . Clone () ,
140166 Paths : b .Paths ,
141167 PackageFiles : b .PackageFiles ,
142168 ResolvedPackageNames : b .ResolvedPackageNames ,
@@ -615,8 +641,8 @@ func (b *registryBuilder) markBucketsDirty(change RegistryChange, logger *loggin
615641 }
616642 }
617643 } else {
618- // Check if this path (possibly a realpath of a symlinked package) is in any bucket's Paths.
619- // This handles symlinked packages where the realpath doesn't contain /node_modules/.
644+ // Check if this path (possibly a realpath of a workspace package) is in any bucket's Paths.
645+ // This handles local workspace packages where the realpath doesn't contain /node_modules/.
620646 for bucketDirPath := range cleanNodeModulesBuckets {
621647 entry := core .FirstResult (b .nodeModules .Get (bucketDirPath ))
622648 if packageName , ok := entry .Value ().Paths [path ]; ok {
@@ -1184,6 +1210,7 @@ type discoveredPackage struct {
11841210 typesPackageJson * packagejson.InfoCacheEntry
11851211 typesRealpath string
11861212 dirPath tspath.Path // bucket directory path (used as extraction context)
1213+ isLocal bool // true if realpath is within the workspace root
11871214}
11881215
11891216// perPackageExtractionResult holds the extraction output for one physical package.
@@ -1197,7 +1224,7 @@ type perPackageExtractionResult struct {
11971224 statsExports int
11981225 statsUsedChecker int
11991226 skippedEntrypoints int
1200- isProjectReference bool
1227+ isSymlinked bool
12011228 failedAmbientModuleLookupSources map [tspath.Path ]* failedAmbientModuleLookupSource
12021229 failedAmbientModuleLookupTargets * collections.Set [string ]
12031230}
@@ -1208,7 +1235,7 @@ type packageExtractionResult struct {
12081235 packageFiles map [string ]map [tspath.Path ]string
12091236 ambientModuleNames map [string ][]string
12101237 entrypoints [][]* module.ResolvedEntrypoint
1211- projectReferencePackages * collections.Set [string ]
1238+ workspacePackages * collections.Set [string ]
12121239 possibleFailedAmbientModuleLookupSources * collections.SyncMap [tspath.Path , * failedAmbientModuleLookupSource ]
12131240 possibleFailedAmbientModuleLookupTargets * collections.SyncSet [string ]
12141241 stats extractorStats
@@ -1241,13 +1268,21 @@ func (b *registryBuilder) discoverBucketPackages(
12411268 if typesPackageJson != nil {
12421269 typesRealpath = b .host .FS ().Realpath (typesPackageJson .PackageDirectory )
12431270 }
1271+ isLocal := realpath != "" &&
1272+ ! strings .Contains (realpath , "/node_modules/" ) &&
1273+ tspath .ContainsPath (
1274+ b .host .GetCurrentDirectory (),
1275+ realpath ,
1276+ tspath.ComparePathsOptions {UseCaseSensitiveFileNames : b .host .FS ().UseCaseSensitiveFileNames ()},
1277+ )
12441278 result = append (result , & discoveredPackage {
12451279 packageName : packageName ,
12461280 packageJson : packageJson ,
12471281 realpath : realpath ,
12481282 typesPackageJson : typesPackageJson ,
12491283 typesRealpath : typesRealpath ,
12501284 dirPath : dirPath ,
1285+ isLocal : isLocal ,
12511286 })
12521287 }
12531288 return result
@@ -1309,7 +1344,6 @@ func (b *registryBuilder) extractPackage(
13091344 fileName = toSymlink (inputFileName )
13101345 realpathFileName = inputFileName
13111346 realpathPath = b .base .toPath (realpathFileName )
1312- result .isProjectReference = true
13131347 }
13141348
13151349 if ! seenFiles .AddIfAbsent (realpathPath ) {
@@ -1318,6 +1352,7 @@ func (b *registryBuilder) extractPackage(
13181352 if fileName != realpathFileName {
13191353 symlinkPath := b .base .toPath (fileName )
13201354 symlinks [realpathPath ] = pathAndFileName {path : symlinkPath , fileName : fileName }
1355+ result .isSymlinked = true
13211356 }
13221357 wg .Go (func () {
13231358 file := b .host .GetSourceFile (realpathFileName , realpathPath )
@@ -1377,7 +1412,7 @@ func installExtractions(
13771412 exports : make (map [tspath.Path ][]* Export ),
13781413 packageFiles : make (map [string ]map [tspath.Path ]string ),
13791414 ambientModuleNames : make (map [string ][]string ),
1380- projectReferencePackages : & collections.Set [string ]{},
1415+ workspacePackages : & collections.Set [string ]{},
13811416 possibleFailedAmbientModuleLookupSources : & collections.SyncMap [tspath.Path , * failedAmbientModuleLookupSource ]{},
13821417 possibleFailedAmbientModuleLookupTargets : & collections.SyncSet [string ]{},
13831418 }
@@ -1407,8 +1442,8 @@ func installExtractions(
14071442 for target := range extraction .failedAmbientModuleLookupTargets .Keys () {
14081443 result .possibleFailedAmbientModuleLookupTargets .Add (target )
14091444 }
1410- if extraction .isProjectReference {
1411- result .projectReferencePackages .Add (pkg .packageName )
1445+ if extraction .isSymlinked && pkg . isLocal {
1446+ result .workspacePackages .Add (pkg .packageName )
14121447 }
14131448 result .stats .exports .Add (int32 (extraction .statsExports ))
14141449 result .stats .usedChecker .Add (int32 (extraction .statsUsedChecker ))
@@ -1444,9 +1479,9 @@ func (b *registryBuilder) buildNodeModulesBucket(
14441479 }
14451480
14461481 // Build Paths as reverse mapping from path to package name.
1447- // Only include paths for packages that are project references (eligible for granular updates).
1482+ // Only include paths for local workspace packages (eligible for granular updates).
14481483 paths := make (map [tspath.Path ]string )
1449- for pkgName := range extraction .projectReferencePackages .Keys () {
1484+ for pkgName := range extraction .workspacePackages .Keys () {
14501485 if files , ok := extraction .packageFiles [pkgName ]; ok {
14511486 for path := range files {
14521487 paths [path ] = pkgName
@@ -1545,8 +1580,8 @@ func (b *registryBuilder) updateNodeModulesBucket(
15451580 }
15461581 newPaths [path ] = pkgName
15471582 }
1548- // Add paths for newly extracted project reference packages
1549- for pkgName := range extraction .projectReferencePackages .Keys () {
1583+ // Add paths for newly extracted workspace packages
1584+ for pkgName := range extraction .workspacePackages .Keys () {
15501585 if files , ok := extraction .packageFiles [pkgName ]; ok {
15511586 for path := range files {
15521587 newPaths [path ] = pkgName
0 commit comments