From 0498de8f9f4db0baa397be1d5ebf513a35c8385a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 03:43:31 +0000 Subject: [PATCH 1/4] chore(tools): pin patch index abbreviation and route every saved patch through savePatch git's default --abbrev scales with the clone's object count, so saving from a shallow submodule rewrote every index line (the committed tsgo patches carried 9-char hashes, the TypeScript ones 10). savePatch now passes --abbrev=10 and takes pathspecs, so the five hand-rolled diff-and-write blocks in the save scripts are gone; the tsgo patches take a one-time index-line-only rewrite. patch-typescript no longer syncs vendor/native-preview: applying the TypeScript delta does not read it, build:js already syncs it, and the coupling made patch:ts fail without the typescript-go submodule. Co-authored-by: Johnson Chu --- .../typescript-go/0001-bridge-inplace.patch | 96 +++++++++---------- .../0002-osvfs-executable-fallback.patch | 2 +- patches/typescript-go/0004-api-surface.patch | 10 +- .../typescript-go/0005-noembed-lib-path.patch | 2 +- tools/patch-common.js | 12 +-- tools/patch-typescript.js | 6 -- tools/save-tsgo-patches.js | 42 ++------ tools/save-typescript-patches.js | 33 ++----- 8 files changed, 74 insertions(+), 129 deletions(-) diff --git a/patches/typescript-go/0001-bridge-inplace.patch b/patches/typescript-go/0001-bridge-inplace.patch index f5de85f..4b2ed2f 100644 --- a/patches/typescript-go/0001-bridge-inplace.patch +++ b/patches/typescript-go/0001-bridge-inplace.patch @@ -1,5 +1,5 @@ diff --git a/_packages/native-preview/src/api/async/client.ts b/_packages/native-preview/src/api/async/client.ts -index b4a123d53..6d3b9b8f3 100644 +index b4a123d537..6d3b9b8f35 100644 --- a/_packages/native-preview/src/api/async/client.ts +++ b/_packages/native-preview/src/api/async/client.ts @@ -90,7 +90,7 @@ export class Client { @@ -24,7 +24,7 @@ index b4a123d53..6d3b9b8f3 100644 resolve(); }); diff --git a/_packages/native-preview/src/api/node/node.generated.ts b/_packages/native-preview/src/api/node/node.generated.ts -index 60ee9cbf3..9ffce1295 100644 +index 60ee9cbf30..9ffce12957 100644 --- a/_packages/native-preview/src/api/node/node.generated.ts +++ b/_packages/native-preview/src/api/node/node.generated.ts @@ -43,7 +43,7 @@ export class RemoteNodeList extends Array implements NodeArray extends ReadonlyArray, ReadonlyTex @@ -754,7 +754,7 @@ index 3f5fa2c43..4f9f73076 100644 readonly jsDoc?: readonly Node[] | undefined; forEachChild(visitor: (node: Node) => T, visitArray?: (nodes: NodeArray) => T): T | undefined; diff --git a/_packages/native-preview/src/ast/astnav.ts b/_packages/native-preview/src/ast/astnav.ts -index 8005023f5..b2128fdc2 100644 +index 8005023f50..b2128fdc28 100644 --- a/_packages/native-preview/src/ast/astnav.ts +++ b/_packages/native-preview/src/ast/astnav.ts @@ -48,7 +48,7 @@ export function findNextToken(previousToken: Node, parent: Node, sourceFile: Sou @@ -895,7 +895,7 @@ index 8005023f5..b2128fdc2 100644 continue; } diff --git a/_packages/native-preview/src/ast/factory.generated.ts b/_packages/native-preview/src/ast/factory.generated.ts -index f29cfc3d1..d3f48c5c4 100644 +index f29cfc3d10..d3f48c5c4e 100644 --- a/_packages/native-preview/src/ast/factory.generated.ts +++ b/_packages/native-preview/src/ast/factory.generated.ts @@ -267,6 +267,8 @@ import { @@ -908,7 +908,7 @@ index f29cfc3d1..d3f48c5c4 100644 readonly end: number = -1; parent: Node = undefined!; diff --git a/_packages/native-preview/src/ast/is.ts b/_packages/native-preview/src/ast/is.ts -index 613ed0a0c..8b11b26ed 100644 +index 613ed0a0cc..8b11b26edf 100644 --- a/_packages/native-preview/src/ast/is.ts +++ b/_packages/native-preview/src/ast/is.ts @@ -257,7 +257,7 @@ function isJSDocTypeAssertion(node: ParenthesizedExpression): boolean { @@ -921,7 +921,7 @@ index 613ed0a0c..8b11b26ed 100644 export function isBindingPattern(node: Node): node is BindingPattern { diff --git a/internal/api/session_apistate_test.go b/internal/api/session_apistate_test.go -index 91aab0904..48b7cabb1 100644 +index 91aab09042..48b7cabb11 100644 --- a/internal/api/session_apistate_test.go +++ b/internal/api/session_apistate_test.go @@ -33,14 +33,14 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) { @@ -960,7 +960,7 @@ index 91aab0904..48b7cabb1 100644 assert.NilError(t, err) diff --git a/internal/checker/checker.go b/internal/checker/checker.go -index b639d488e..f46b47fef 100644 +index b639d488e4..f46b47fef6 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -786,6 +786,7 @@ type Checker struct { @@ -1368,7 +1368,7 @@ index b639d488e..f46b47fef 100644 case ast.KindVariableDeclaration, ast.KindParameter, ast.KindPropertyDeclaration, ast.KindPropertySignature, ast.KindBindingElement: return c.getContextualTypeForInitializerExpression(node, contextFlags) diff --git a/internal/checker/exports.go b/internal/checker/exports.go -index 90162bdae..4fdcc2a9f 100644 +index 90162bdaeb..4fdcc2a9fb 100644 --- a/internal/checker/exports.go +++ b/internal/checker/exports.go @@ -3,7 +3,9 @@ package checker @@ -1771,7 +1771,7 @@ index 90162bdae..4fdcc2a9f 100644 +// GetSymbolOfExpando — expando.go +// CollectVisitedTypeParameters — symbolwalker.go diff --git a/internal/checker/nodebuilderimpl.go b/internal/checker/nodebuilderimpl.go -index 2c64872a6..b5aecd4eb 100644 +index 2c64872a69..b5aecd4eb9 100644 --- a/internal/checker/nodebuilderimpl.go +++ b/internal/checker/nodebuilderimpl.go @@ -1617,7 +1617,10 @@ func (b *NodeBuilderImpl) symbolToTypeParameterDeclarations(symbol *ast.Symbol) @@ -1787,7 +1787,7 @@ index 2c64872a6..b5aecd4eb 100644 params := b.ch.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol) for _, param := range params { diff --git a/internal/checker/printer.go b/internal/checker/printer.go -index 21704cc7d..2c1c23145 100644 +index 21704cc7da..2c1c23145a 100644 --- a/internal/checker/printer.go +++ b/internal/checker/printer.go @@ -426,6 +426,24 @@ func (c *Checker) SignatureToSignatureDeclaration(signature *Signature, kind ast @@ -1856,7 +1856,7 @@ index 21704cc7d..2c1c23145 100644 + return nodeBuilder.IndexInfoToIndexSignatureDeclaration(info, enclosingDeclaration, flags, nodebuilder.InternalFlagsNone, nil) +} diff --git a/internal/checker/services.go b/internal/checker/services.go -index 12b9d4079..db9750b1a 100644 +index 12b9d40792..db9750b1af 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -222,6 +222,14 @@ func (c *Checker) IsUnknownSymbol(symbol *ast.Symbol) bool { @@ -1875,7 +1875,7 @@ index 12b9d4079..db9750b1a 100644 return symbol == c.undefinedSymbol } diff --git a/internal/checker/types.go b/internal/checker/types.go -index bd1342092..4d7865fd0 100644 +index bd13420922..4d7865fd00 100644 --- a/internal/checker/types.go +++ b/internal/checker/types.go @@ -746,6 +746,18 @@ func (t *Type) Target() *Type { @@ -1910,7 +1910,7 @@ index bd1342092..4d7865fd0 100644 type ConstrainedType struct { diff --git a/internal/compiler/fileloader.go b/internal/compiler/fileloader.go -index f8d006e92..82e128ca2 100644 +index f8d006e925..82e128ca2e 100644 --- a/internal/compiler/fileloader.go +++ b/internal/compiler/fileloader.go @@ -125,7 +125,7 @@ func processAllProgramFiles( @@ -2002,7 +2002,7 @@ index f8d006e92..82e128ca2 100644 // add file to program only if: diff --git a/internal/compiler/filesparser.go b/internal/compiler/filesparser.go -index 5817fc6bb..698abd084 100644 +index 5817fc6bb8..698abd084a 100644 --- a/internal/compiler/filesparser.go +++ b/internal/compiler/filesparser.go @@ -29,13 +29,16 @@ type parseTask struct { @@ -2053,7 +2053,7 @@ index 5817fc6bb..698abd084 100644 missingFiles = append(missingFiles, task.normalizedFilePath) continue diff --git a/internal/compiler/program.go b/internal/compiler/program.go -index 74b2504d9..d6a714c18 100644 +index 74b2504d9e..d6a714c18b 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -116,6 +116,20 @@ type Program struct { @@ -2196,7 +2196,7 @@ index 74b2504d9..d6a714c18 100644 return diagnostics } diff --git a/internal/execute/incremental/programtosnapshot.go b/internal/execute/incremental/programtosnapshot.go -index 44b9b4cbc..0a8aafd0d 100644 +index 44b9b4cbc6..0a8aafd0d4 100644 --- a/internal/execute/incremental/programtosnapshot.go +++ b/internal/execute/incremental/programtosnapshot.go @@ -14,13 +14,22 @@ import ( @@ -2225,7 +2225,7 @@ index 44b9b4cbc..0a8aafd0d 100644 to := &toProgramSnapshot{ program: program, diff --git a/internal/ls/autoimport/export.go b/internal/ls/autoimport/export.go -index cb5278fab..bf5b19e51 100644 +index cb5278fab0..bf5b19e515 100644 --- a/internal/ls/autoimport/export.go +++ b/internal/ls/autoimport/export.go @@ -62,6 +62,10 @@ type Export struct { @@ -2240,7 +2240,7 @@ index cb5278fab..bf5b19e51 100644 // The file where the export was found. Path tspath.Path diff --git a/internal/ls/autoimport/extract.go b/internal/ls/autoimport/extract.go -index dc0a1db7e..56820b267 100644 +index dc0a1db7e4..56820b2676 100644 --- a/internal/ls/autoimport/extract.go +++ b/internal/ls/autoimport/extract.go @@ -297,8 +297,15 @@ func (e *symbolExtractor) createExport(symbol *ast.Symbol, moduleID ModuleID, mo @@ -2270,7 +2270,7 @@ index dc0a1db7e..56820b267 100644 if symbol.Name == ast.InternalSymbolNameDefault || symbol.Name == ast.InternalSymbolNameExportEquals { diff --git a/internal/ls/autoimport/registry.go b/internal/ls/autoimport/registry.go -index 6c6468095..489622b9a 100644 +index 6c6468095c..489622b9ab 100644 --- a/internal/ls/autoimport/registry.go +++ b/internal/ls/autoimport/registry.go @@ -244,6 +244,12 @@ type RegistryBucket struct { @@ -2545,7 +2545,7 @@ index 6c6468095..489622b9a 100644 buildPreferences: bucketBuildPreferencesFromUserPreferences(b.userPreferences), recursiveSearchPackages: recursiveSearchPackages.Clone(), diff --git a/internal/ls/autoimport/view.go b/internal/ls/autoimport/view.go -index 9f8b144fa..4baa9e6e2 100644 +index 9f8b144fa5..4baa9e6e2e 100644 --- a/internal/ls/autoimport/view.go +++ b/internal/ls/autoimport/view.go @@ -32,6 +32,13 @@ type View struct { @@ -2625,7 +2625,7 @@ index 9f8b144fa..4baa9e6e2 100644 continue outer } diff --git a/internal/ls/completions.go b/internal/ls/completions.go -index 5e62661fc..9d58f85c6 100644 +index 5e62661fce..9d58f85c6e 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -26,6 +26,7 @@ import ( @@ -2905,7 +2905,7 @@ index 5e62661fc..9d58f85c6 100644 } current = current.Parent diff --git a/internal/ls/inlay_hints.go b/internal/ls/inlay_hints.go -index a1923826c..aa562b80d 100644 +index a1923826cb..aa562b80d1 100644 --- a/internal/ls/inlay_hints.go +++ b/internal/ls/inlay_hints.go @@ -177,15 +177,15 @@ func (s *inlayHintState) visitCallOrNewExpression(expr *ast.CallOrNewExpression) @@ -3014,7 +3014,7 @@ index a1923826c..aa562b80d 100644 if ast.IsIdentifier(expr) { return expr.Text() == parameterName diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go -index 7c7e1aced..b6d587aba 100644 +index 7c7e1aced7..b6d587aba9 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -97,7 +97,8 @@ func (l *LanguageService) getPreparedAutoImportView(fromFile *ast.SourceFile) (* @@ -3028,7 +3028,7 @@ index 7c7e1aced..b6d587aba 100644 } diff --git a/internal/ls/lsutil/symbol_display.go b/internal/ls/lsutil/symbol_display.go -index 8ee6bd52b..1a94babdb 100644 +index 8ee6bd52b5..1a94babdb6 100644 --- a/internal/ls/lsutil/symbol_display.go +++ b/internal/ls/lsutil/symbol_display.go @@ -1,6 +1,8 @@ @@ -3137,7 +3137,7 @@ index 8ee6bd52b..1a94babdb 100644 var modifierSet ScriptElementKindModifier if len(symbol.Declarations) > 0 { diff --git a/internal/ls/signaturehelp.go b/internal/ls/signaturehelp.go -index c83765666..5e3df8463 100644 +index c837656666..5e3df84635 100644 --- a/internal/ls/signaturehelp.go +++ b/internal/ls/signaturehelp.go @@ -470,8 +470,9 @@ func (l *LanguageService) getSignatureHelpItem(candidate *checker.Signature, isT @@ -3153,7 +3153,7 @@ index c83765666..5e3df8463 100644 predicate := c.GetTypePredicateOfSignature(candidateSignature) if predicate != nil { diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go -index f5240bcf3..ba4a1873f 100644 +index f5240bcf32..ba4a1873fe 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -173,9 +173,7 @@ func (l *LanguageService) convertStringLiteralCompletions( @@ -3207,7 +3207,7 @@ index f5240bcf3..ba4a1873f 100644 extensions = append(extensions, ext...) } diff --git a/internal/module/resolver.go b/internal/module/resolver.go -index 52400dc2b..29afc1957 100644 +index 52400dc2b9..29afc19574 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -158,9 +158,31 @@ type Resolver struct { @@ -3288,7 +3288,7 @@ index 52400dc2b..29afc1957 100644 } } diff --git a/internal/project/checkerpool.go b/internal/project/checkerpool.go -index 32f68d413..2fe5c76b1 100644 +index 32f68d413f..2fe5c76b1b 100644 --- a/internal/project/checkerpool.go +++ b/internal/project/checkerpool.go @@ -215,6 +215,34 @@ func (p *checkerPool) tryReacquireForRequest(requestID string, sem chan<- struct @@ -3327,7 +3327,7 @@ index 32f68d413..2fe5c76b1 100644 // Creates it on first use. Blocks on diagSem if it's currently in use. func (p *checkerPool) getDiagnosticsChecker(ctx context.Context, requestID string) (*checker.Checker, func()) { diff --git a/internal/project/compilerhost.go b/internal/project/compilerhost.go -index 212601d4b..abb4d611d 100644 +index 212601d4b8..abb4d611de 100644 --- a/internal/project/compilerhost.go +++ b/internal/project/compilerhost.go @@ -3,6 +3,7 @@ package project @@ -3360,7 +3360,7 @@ index 212601d4b..abb4d611d 100644 } return nil diff --git a/internal/project/configfileregistrybuilder.go b/internal/project/configfileregistrybuilder.go -index 1d62a1188..59a6a8cdc 100644 +index 1d62a11880..59a6a8cdc6 100644 --- a/internal/project/configfileregistrybuilder.go +++ b/internal/project/configfileregistrybuilder.go @@ -31,6 +31,7 @@ type configFileRegistryBuilder struct { @@ -3427,7 +3427,7 @@ index 1d62a1188..59a6a8cdc 100644 c.updateRootFilesWatch(fileName, entry) logger.Log("Finished loading config file") diff --git a/internal/project/filechange.go b/internal/project/filechange.go -index edc209b39..21928e66e 100644 +index edc209b397..21928e66ef 100644 --- a/internal/project/filechange.go +++ b/internal/project/filechange.go @@ -30,13 +30,18 @@ type FileChange struct { @@ -3463,7 +3463,7 @@ index edc209b39..21928e66e 100644 func (f FileChangeSummary) HasExcessiveWatchEvents() bool { diff --git a/internal/project/overlayfs.go b/internal/project/overlayfs.go -index 1b300a013..de14d03a0 100644 +index 1b300a0131..de14d03a09 100644 --- a/internal/project/overlayfs.go +++ b/internal/project/overlayfs.go @@ -300,15 +300,20 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma @@ -3496,7 +3496,7 @@ index 1b300a013..de14d03a0 100644 scriptKind := lsconv.LanguageKindToScriptKind(events.openChange.LanguageKind) if scriptKind == core.ScriptKindUnknown { diff --git a/internal/project/overlayfs_test.go b/internal/project/overlayfs_test.go -index 4659530ff..dfcd64020 100644 +index 4659530ff7..dfcd640209 100644 --- a/internal/project/overlayfs_test.go +++ b/internal/project/overlayfs_test.go @@ -34,7 +34,9 @@ func TestProcessChanges(t *testing.T) { @@ -3540,7 +3540,7 @@ index 4659530ff..dfcd64020 100644 assert.Assert(t, result.Changed.Has(testURI1), "close then open should mark as changed") // Should have the new content diff --git a/internal/project/project.go b/internal/project/project.go -index 39c77aa78..a7c1805b3 100644 +index 39c77aa78d..a7c1805b3c 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -85,6 +85,28 @@ type Project struct { @@ -3651,7 +3651,7 @@ index 39c77aa78..a7c1805b3 100644 if p.potentialProjectReferences == nil { p.potentialProjectReferences = &collections.Set[tspath.Path]{} diff --git a/internal/project/projectcollection.go b/internal/project/projectcollection.go -index e38812bc4..ed707447f 100644 +index e38812bc45..ed707447fb 100644 --- a/internal/project/projectcollection.go +++ b/internal/project/projectcollection.go @@ -50,17 +50,50 @@ type APIState struct { @@ -3709,7 +3709,7 @@ index e38812bc4..ed707447f 100644 // apiOpenedFile tracks a file kept open by API clients along with its ref count. diff --git a/internal/project/projectcollectionbuilder.go b/internal/project/projectcollectionbuilder.go -index 3355b6887..c21b4d670 100644 +index 3355b68878..c21b4d6704 100644 --- a/internal/project/projectcollectionbuilder.go +++ b/internal/project/projectcollectionbuilder.go @@ -4,6 +4,7 @@ import ( @@ -3996,7 +3996,7 @@ index 3355b6887..c21b4d670 100644 project.ProgramLastUpdate = b.newSnapshotID if result.UpdateKind == ProgramUpdateKindCloned { diff --git a/internal/project/refcountcache.go b/internal/project/refcountcache.go -index 909b5aa4b..2237d958c 100644 +index 909b5aa4b1..2237d958c0 100644 --- a/internal/project/refcountcache.go +++ b/internal/project/refcountcache.go @@ -23,6 +23,13 @@ type RefCountCache[K comparable, V any, AcquireArgs any] struct { @@ -4044,7 +4044,7 @@ index 909b5aa4b..2237d958c 100644 existing, loaded := c.entries.LoadOrStore(key, entry) if loaded { diff --git a/internal/project/session.go b/internal/project/session.go -index 00f4056e5..6003b3773 100644 +index 00f4056e58..6003b37730 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -26,6 +26,7 @@ import ( @@ -4172,7 +4172,7 @@ index 00f4056e5..6003b3773 100644 // snapshot so future requests benefit from the work already done. If the // session has moved on, the snapshot is discarded; the next request needing diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go -index 61af433d3..77240f672 100644 +index 61af433d3c..77240f6725 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -8,6 +8,7 @@ import ( @@ -4249,7 +4249,7 @@ index 61af433d3..77240f672 100644 change.fileChanges.Deleted.Len() > 0 if shouldCleanDiskCache { diff --git a/internal/project/snapshotfs.go b/internal/project/snapshotfs.go -index 0a9f391fa..582bb0de1 100644 +index 0a9f391fa2..582bb0de1d 100644 --- a/internal/project/snapshotfs.go +++ b/internal/project/snapshotfs.go @@ -494,6 +494,45 @@ func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) { @@ -4324,7 +4324,7 @@ index 0a9f391fa..582bb0de1 100644 } } diff --git a/internal/tsoptions/parsedcommandline.go b/internal/tsoptions/parsedcommandline.go -index 9f0229165..0c1abcbe2 100644 +index 9f0229165b..0c1abcbe26 100644 --- a/internal/tsoptions/parsedcommandline.go +++ b/internal/tsoptions/parsedcommandline.go @@ -89,6 +89,31 @@ func (p *ParsedCommandLine) ConfigName() string { @@ -4415,7 +4415,7 @@ index 9f0229165..0c1abcbe2 100644 parsedConfig := *p.ParsedConfig fileNames, literalFileNamesLen := getFileNamesFromConfigSpecs( diff --git a/internal/tsoptions/tsconfigparsing.go b/internal/tsoptions/tsconfigparsing.go -index 4f8578275..07524b629 100644 +index 4f85782753..07524b6290 100644 --- a/internal/tsoptions/tsconfigparsing.go +++ b/internal/tsoptions/tsconfigparsing.go @@ -1673,7 +1673,7 @@ func getFileNamesFromConfigSpecs( diff --git a/patches/typescript-go/0002-osvfs-executable-fallback.patch b/patches/typescript-go/0002-osvfs-executable-fallback.patch index a0aaefc..c54e659 100644 --- a/patches/typescript-go/0002-osvfs-executable-fallback.patch +++ b/patches/typescript-go/0002-osvfs-executable-fallback.patch @@ -1,5 +1,5 @@ diff --git a/internal/vfs/osvfs/os.go b/internal/vfs/osvfs/os.go -index 7db6f338b..a2b8c5bb9 100644 +index 7db6f338b8..a2b8c5bb98 100644 --- a/internal/vfs/osvfs/os.go +++ b/internal/vfs/osvfs/os.go @@ -60,7 +60,12 @@ var isFileSystemCaseSensitive = func() bool { diff --git a/patches/typescript-go/0004-api-surface.patch b/patches/typescript-go/0004-api-surface.patch index b651987..95538eb 100644 --- a/patches/typescript-go/0004-api-surface.patch +++ b/patches/typescript-go/0004-api-surface.patch @@ -1,5 +1,5 @@ diff --git a/_packages/native-preview/src/api/async/api.ts b/_packages/native-preview/src/api/async/api.ts -index 1c8b5d7f0..a131f9ea7 100644 +index 1c8b5d7f0f..a131f9ea70 100644 --- a/_packages/native-preview/src/api/async/api.ts +++ b/_packages/native-preview/src/api/async/api.ts @@ -49,6 +49,7 @@ import type { @@ -1815,7 +1815,7 @@ index 1c8b5d7f0..a131f9ea7 100644 get hasRestParameter(): boolean { diff --git a/_packages/native-preview/src/api/sync/api.ts b/_packages/native-preview/src/api/sync/api.ts -index 1a0296ac7..25b87f6ad 100644 +index 1a0296ac77..25b87f6ad0 100644 --- a/_packages/native-preview/src/api/sync/api.ts +++ b/_packages/native-preview/src/api/sync/api.ts @@ -62,6 +62,8 @@ import type { @@ -4381,7 +4381,7 @@ index 1a0296ac7..25b87f6ad 100644 get hasRestParameter(): boolean { diff --git a/_packages/native-preview/test/sync/api.test.ts b/_packages/native-preview/test/sync/api.test.ts -index 6a90030eb..fc58b512a 100644 +index 6a90030eb2..fc58b512a8 100644 --- a/_packages/native-preview/test/sync/api.test.ts +++ b/_packages/native-preview/test/sync/api.test.ts @@ -3459,6 +3459,38 @@ export * from "./inner"; @@ -4424,7 +4424,7 @@ index 6a90030eb..fc58b512a 100644 const docFiles = { "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), diff --git a/internal/api/proto.go b/internal/api/proto.go -index 0917c385d..8f640c977 100644 +index 0917c385d6..8f640c9775 100644 --- a/internal/api/proto.go +++ b/internal/api/proto.go @@ -67,37 +67,81 @@ const ( @@ -6195,7 +6195,7 @@ index 0917c385d..8f640c977 100644 type GetProjectDiagnosticsParams struct { Snapshot SnapshotID `json:"snapshot"` diff --git a/internal/api/session.go b/internal/api/session.go -index 7e29c44b6..eb237f5b4 100644 +index 7e29c44b6f..eb237f5b4f 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -3,6 +3,7 @@ package api diff --git a/patches/typescript-go/0005-noembed-lib-path.patch b/patches/typescript-go/0005-noembed-lib-path.patch index 4206222..4aaeb60 100644 --- a/patches/typescript-go/0005-noembed-lib-path.patch +++ b/patches/typescript-go/0005-noembed-lib-path.patch @@ -1,5 +1,5 @@ diff --git a/internal/bundled/noembed.go b/internal/bundled/noembed.go -index 39b204b45..5f0052499 100644 +index 39b204b450..5f0052499f 100644 --- a/internal/bundled/noembed.go +++ b/internal/bundled/noembed.go @@ -6,6 +6,7 @@ import ( diff --git a/tools/patch-common.js b/tools/patch-common.js index 4c863b4..d003e3d 100644 --- a/tools/patch-common.js +++ b/tools/patch-common.js @@ -146,14 +146,14 @@ function realignEolToHead(subDir) { } } -// Save: in-place edits to tracked files (git diff HEAD) -> single patch. -// Optional exclude globs keep named files in separate patch files. -function savePatch(subDir, patchPath, exclude = []) { +// Save: in-place edits to tracked files (git diff HEAD) -> single patch, +// restricted to `pathspecs` (`:(exclude)` carves files out into their +// own patch). --abbrev pins the `index` lines: git's default scales with the +// object count, so a shallow submodule clone would rewrite every hash line. +function savePatch(subDir, patchPath, pathspecs) { guardNoStaged(subDir); realignEolToHead(subDir); - const args = ["diff", "HEAD", "--", "."]; - for (const ex of exclude) args.push(`:(exclude)${ex}`); - const diff = git(subDir, args); + const diff = git(subDir, ["diff", "--abbrev=10", "HEAD", "--", ...pathspecs]); if (diff.status !== 0) { console.error("save: git diff failed\n" + diff.stderr); process.exit(1); diff --git a/tools/patch-typescript.js b/tools/patch-typescript.js index e4b4316..2c59d30 100644 --- a/tools/patch-typescript.js +++ b/tools/patch-typescript.js @@ -9,7 +9,6 @@ const path = require("path"); const fs = require("fs"); -const { spawnSync } = require("child_process"); const { applyOverlay, applyPatchFiles } = require("./patch-common.js"); const repoRoot = path.resolve(__dirname, ".."); @@ -22,10 +21,5 @@ if (!fs.existsSync(path.join(subDir, ".git"))) { process.exit(1); } -if (!checkOnly) { - const result = spawnSync(process.execPath, [path.join(__dirname, "sync-vendor-native-preview.js")], { stdio: "inherit" }); - if (result.status !== 0) process.exit(result.status ?? 1); -} - applyOverlay(subDir, path.join(patchDir, "overlay"), checkOnly); applyPatchFiles(subDir, patchDir, checkOnly); diff --git a/tools/save-tsgo-patches.js b/tools/save-tsgo-patches.js index f68573b..47eea4a 100644 --- a/tools/save-tsgo-patches.js +++ b/tools/save-tsgo-patches.js @@ -5,17 +5,13 @@ const path = require("path"); const { saveOverlay, savePatch } = require("./patch-common.js"); -const { spawnSync } = require("child_process"); const repoRoot = path.resolve(__dirname, ".."); const subDir = path.join(repoRoot, "typescript-go"); const patchDir = path.join(repoRoot, "patches", "typescript-go"); -const git = (args) => spawnSync("git", ["-C", subDir, ...args], { encoding: "utf8" }); - // Files carved out of 0001 into their own focused patch (each rebases -// independently). savePatch realigns EOL to HEAD first, so the per-file -// diffs below inherit the same normalization. +// independently). const osvfsRel = "internal/vfs/osvfs/os.go"; const apiSurfaceRel = [ "internal/api/proto.go", @@ -27,34 +23,8 @@ const apiSurfaceRel = [ const noembedRel = "internal/bundled/noembed.go"; saveOverlay(subDir, path.join(patchDir, "overlay")); -savePatch(subDir, path.join(patchDir, "0001-bridge-inplace.patch"), [ - osvfsRel, - ...apiSurfaceRel, - noembedRel, -]); - -function saveSingleFilePatch(rel, patchName) { - const diff = git(["diff", "HEAD", "--", rel]); - if (diff.status !== 0) { - console.error(`save: git diff ${rel} failed\n` + diff.stderr); - process.exit(1); - } - const patchPath = path.join(patchDir, patchName); - require("fs").writeFileSync(patchPath, diff.stdout); - console.log(`save: patch <- ${diff.stdout.length} bytes (${patchName})`); -} - -function saveFilesPatch(rels, patchName) { - const diff = git(["diff", "HEAD", "--", ...rels]); - if (diff.status !== 0) { - console.error(`save: git diff ${rels.join(" ")} failed\n` + diff.stderr); - process.exit(1); - } - const patchPath = path.join(patchDir, patchName); - require("fs").writeFileSync(patchPath, diff.stdout); - console.log(`save: patch <- ${diff.stdout.length} bytes (${patchName})`); -} - -saveSingleFilePatch(osvfsRel, "0002-osvfs-executable-fallback.patch"); -saveFilesPatch(apiSurfaceRel, "0004-api-surface.patch"); -saveSingleFilePatch(noembedRel, "0005-noembed-lib-path.patch"); +const carved = [osvfsRel, ...apiSurfaceRel, noembedRel]; +savePatch(subDir, path.join(patchDir, "0001-bridge-inplace.patch"), [".", ...carved.map(rel => `:(exclude)${rel}`)]); +savePatch(subDir, path.join(patchDir, "0002-osvfs-executable-fallback.patch"), [osvfsRel]); +savePatch(subDir, path.join(patchDir, "0004-api-surface.patch"), apiSurfaceRel); +savePatch(subDir, path.join(patchDir, "0005-noembed-lib-path.patch"), [noembedRel]); diff --git a/tools/save-typescript-patches.js b/tools/save-typescript-patches.js index 98dcfca..6d6844f 100644 --- a/tools/save-typescript-patches.js +++ b/tools/save-typescript-patches.js @@ -4,37 +4,18 @@ // node tools/save-typescript-patches.js const path = require("path"); -const fs = require("fs"); -const { spawnSync } = require("child_process"); const { saveOverlay, savePatch } = require("./patch-common.js"); const repoRoot = path.resolve(__dirname, ".."); const subDir = path.join(repoRoot, "typescript"); const patchDir = path.join(repoRoot, "patches", "typescript"); -const git = (dir, args) => spawnSync("git", ["-C", dir, ...args], { encoding: "utf8" }); - -saveOverlay(subDir, path.join(patchDir, "overlay")); -savePatch(subDir, path.join(patchDir, "0001-tsgo-hooks.patch"), ["scripts/produceLKG.mjs", "src/server/project.ts"]); - -// produceLKG.mjs is a separate tracked edit — keep it in its own patch so -// overlay/hook changes can be rebased independently. +// Separate tracked edits, each in its own patch so overlay/hook changes can be +// rebased independently. const lkgRel = "scripts/produceLKG.mjs"; -const lkgDiff = git(subDir, ["diff", "HEAD", "--", lkgRel]); -if (lkgDiff.status !== 0) { - console.error("save: git diff produceLKG failed\n" + lkgDiff.stderr); - process.exit(1); -} -const lkgPatch = path.join(patchDir, "0002-lkg-output-to-parent.patch"); -fs.writeFileSync(lkgPatch, lkgDiff.stdout); -console.log(`save: patch <- ${lkgDiff.stdout.length} bytes (${path.basename(lkgPatch)})`); - const projectRel = "src/server/project.ts"; -const projectDiff = git(subDir, ["diff", "HEAD", "--", projectRel]); -if (projectDiff.status !== 0) { - console.error("save: git diff project.ts failed\n" + projectDiff.stderr); - process.exit(1); -} -const projectPatch = path.join(patchDir, "0003-tsgo-tsserver-scriptinfo.patch"); -fs.writeFileSync(projectPatch, projectDiff.stdout); -console.log(`save: patch <- ${projectDiff.stdout.length} bytes (${path.basename(projectPatch)})`); + +saveOverlay(subDir, path.join(patchDir, "overlay")); +savePatch(subDir, path.join(patchDir, "0001-tsgo-hooks.patch"), [".", `:(exclude)${lkgRel}`, `:(exclude)${projectRel}`]); +savePatch(subDir, path.join(patchDir, "0002-lkg-output-to-parent.patch"), [lkgRel]); +savePatch(subDir, path.join(patchDir, "0003-tsgo-tsserver-scriptinfo.patch"), [projectRel]); From 0f0f4a0ebb16004e93e9ee8a9ceff44f14970f9a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 03:51:29 +0000 Subject: [PATCH 2/4] fix(bridge): refuse a bridge.node built from a different package version build-bridge.js stamps the package.json version into the binary (-ldflags -X main.bridgeVersion); the shim exports it as `version` and the loader throws before setLibPath when it differs from the bundle's own package.json. A stale platform package in a pnpm store paired with a newer bundle previously surfaced as an ArenaClient.readHandle error far from the cause. Co-authored-by: Johnson Chu --- README.md | 11 ++++++++--- patches/typescript-go/overlay/bridge/bridge.go | 14 ++++++++++++++ patches/typescript-go/overlay/bridge/napi_shim.c | 5 +++++ .../typescript/overlay/src/compiler/tsgoChecker.ts | 14 +++++++++++++- tools/build-bridge.js | 7 +++++-- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a543792..2fd6ca6 100644 --- a/README.md +++ b/README.md @@ -328,9 +328,14 @@ differs from tsgo itself — file an issue with a minimal repro. ### Missing native bridge -Error mentioning `bridge.dylib` / `bridge.so` / `bridge.dll` / "unsupported platform" → -see [Platform support](#platform-support) (build from source, or use a `link:` install -built with `npm run setup`). +"bridge shared library not found" → see [Platform support](#platform-support) (build +from source, or use a `link:` install built with `npm run setup`). + +"bridge.node was built for typescript-native-bridge X, this bundle is Y" → the loaded +native bridge comes from another release (typically a stale +`@typescript-native-bridge/-` left in a package-manager store). Reinstall +so the platform package matches the main package; from a source checkout, run +`npm run build:bridge`. ### Debug a slow run diff --git a/patches/typescript-go/overlay/bridge/bridge.go b/patches/typescript-go/overlay/bridge/bridge.go index f9fbd01..8f9894b 100644 --- a/patches/typescript-go/overlay/bridge/bridge.go +++ b/patches/typescript-go/overlay/bridge/bridge.go @@ -184,6 +184,20 @@ func startOrphanWatchdog() { }) } +// bridgeVersion is the typescript-native-bridge package version this binary +// was built from, stamped by tools/build-bridge.js (-ldflags -X). The JS +// loader refuses a bridge whose stamp differs from its own package.json: a +// stale platform package would otherwise pair one release's JS with +// another's wire format and fail far from the cause. +var bridgeVersion string + +// BridgeVersion returns bridgeVersion as a malloc'd string the shim frees. +// +//export BridgeVersion +func BridgeVersion() *C.char { + return C.CString(bridgeVersion) +} + // BridgeSetLibPath hands the JS host's bundled-lib dir to Go — the only // channel that reaches Go from every host thread (issue #37: worker // process.env writes never touch the environ Go reads, so an env-var diff --git a/patches/typescript-go/overlay/bridge/napi_shim.c b/patches/typescript-go/overlay/bridge/napi_shim.c index 4325a78..1934d53 100644 --- a/patches/typescript-go/overlay/bridge/napi_shim.c +++ b/patches/typescript-go/overlay/bridge/napi_shim.c @@ -70,6 +70,7 @@ static void pin_module_in_process(void) { // cgo-exported Go entry points (bridge.go). struct BridgeText { char* data; long long kind; }; struct BridgeBinary { void* data; long long len; unsigned long long handle; long long kind; }; +extern char* BridgeVersion(void); extern char* BridgeSetLibPath(char* dir); extern long long BridgeNewSession(char* cwd); extern struct BridgeText BridgeCall(int64_t session, char* method, char* paramsJson); @@ -529,6 +530,10 @@ NAPI_MODULE_INIT() { napi_throw_error(env, NULL, "tnb bridge: out of memory"); return NULL; } + // `version`: the build stamp the JS loader checks before any other call. + char* version = BridgeVersion(); + napi_set_named_property(env, exports, "version", js_string(env, version)); + free(version); set_fn(env, exports, "setLibPath", fn_set_lib_path); set_fn(env, exports, "newSession", fn_new_session); set_fn(env, exports, "call", fn_call); diff --git a/patches/typescript/overlay/src/compiler/tsgoChecker.ts b/patches/typescript/overlay/src/compiler/tsgoChecker.ts index 196815a..ebb725a 100644 --- a/patches/typescript/overlay/src/compiler/tsgoChecker.ts +++ b/patches/typescript/overlay/src/compiler/tsgoChecker.ts @@ -417,7 +417,19 @@ function loadBridgeDeps(): void { // The bridge is a NAPI addon (bridge.node) — Node dlopens it directly via // require(); no FFI library. napi_shim.c exposes plain JS functions // (strings/Buffer/bool/null/int64 in and out; errors are thrown). - _bridgeAddon = require(resolvedBridge); + const addon = require(resolvedBridge); + // A bridge from another release speaks another wire format and fails far + // from the cause (a stale platform package in a pnpm store paired with a + // newer bundle surfaced as an ArenaClient.readHandle error). + const expectedVersion: string = require(path.join(packageRoot, "package.json")).version; + if (addon.version !== expectedVersion) { + throw new Error( + `tsgoChecker: ${resolvedBridge} was built for typescript-native-bridge ${addon.version || "(unstamped)"}, ` + + `this bundle is ${expectedVersion}\n` + + ` Reinstall so the platform package matches, or from a source checkout: npm run build:bridge`, + ); + } + _bridgeAddon = addon; // Hand the bundled lib dir to Go over NAPI — the only channel that // reaches Go from every host thread (issue #37: worker_threads never // propagate process.env writes to the environ Go reads, so an env-var diff --git a/tools/build-bridge.js b/tools/build-bridge.js index ed10888..9713ef1 100644 --- a/tools/build-bridge.js +++ b/tools/build-bridge.js @@ -27,9 +27,12 @@ if (!fs.existsSync(path.join(nodeInclude, "node_api.h"))) { fs.mkdirSync(nativeDir, { recursive: true }); -const args = ["build", "-tags=noembed", "-buildmode=c-shared"]; +// main.bridgeVersion: the stamp tsgoChecker's loader matches against its own +// package.json before touching the bridge. +const ldflags = [`-X main.bridgeVersion=${require(path.join(repoRoot, "package.json")).version}`]; // TNB_STRIP=1: drop debug symbols for shipped binaries (release matrix). -if (process.env.TNB_STRIP === "1") args.push("-ldflags=-s -w"); +if (process.env.TNB_STRIP === "1") ldflags.push("-s", "-w"); +const args = ["build", "-tags=noembed", "-buildmode=c-shared", `-ldflags=${ldflags.join(" ")}`]; // Build the package (not just bridge.go) so platform-split files // (killself_unix.go / killself_windows.go) and the NAPI shim are included. args.push("-o", outPath, "."); From 35e3f7e7c4a2635c462046095a3d5dfcee1ed25f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 04:04:41 +0000 Subject: [PATCH 3/4] refactor(bridge): one overlay-sync path for every updateSnapshot site ensureProject, its late extra-extension push and pushHostOverlayToTsgo each re-implemented the per-file overlay decision and the send/commit/dedupe bookkeeping, with two fingerprints (_lastUpdateParamsByConfig for the rebuild dedupe, _lastOverlayPushKeyByConfig for the query fast path) that had to be kept in step by hand. planOverlayPush is now the only per-file rule (diverging text -> mirror skip / edit delta / full content; text back at disk -> one re-push, then forget), and syncSnapshot the only send: steady params (options, open set, sticky extras and additional roots) plus a one-shot delta, skipped against the last landed steady params when the delta is empty. Both sites now build the same steady params, so a rebuild and the following query sync compare equal by construction. The push walk drops its hostOnly/inTsgo special cases from before the openFiles channel existed: files absent on disk dedupe against the mirror like everywhere else, and a host==disk file not yet in Go's program is opened through openFiles instead of being re-sent as an overlay. Co-authored-by: Johnson Chu --- .../overlay/src/compiler/tsgoChecker.ts | 327 +++++++----------- tools/triage-overlay-delta-sync.mjs | 2 +- 2 files changed, 127 insertions(+), 202 deletions(-) diff --git a/patches/typescript/overlay/src/compiler/tsgoChecker.ts b/patches/typescript/overlay/src/compiler/tsgoChecker.ts index ebb725a..76ca073 100644 --- a/patches/typescript/overlay/src/compiler/tsgoChecker.ts +++ b/patches/typescript/overlay/src/compiler/tsgoChecker.ts @@ -900,37 +900,50 @@ function openProjectParam(configFilePath: string, options: any): { fileName: str return { fileName: configFilePath, compilerOptions: toWireCompilerOptions(options) }; } -/** Last updateSnapshot params + project per config — identical repeat calls are skipped (watch-lint generations reissue the same params ~1000×). The params OBJECT is the fingerprint: retaining a JSON copy duplicated every overlay text (~15MB per config at hoppscotch scale, issue #41 B-2), while the content strings here are shared references with hostContentByFile, so equality below is O(1) per unchanged file. */ -const _lastUpdateParamsByConfig = new Map(); /** Per-generation getCompilerOptionsForFile memo, keyed by the live vendored project wrapper — see optionsForFile (no-project-references fast path). */ const _optionsForFileMemo = new WeakMap(); -/** Structural equality for updateSnapshot params (see _lastUpdateParamsByConfig). Small fields (compilerOptions, extraFileExtensions) compare via JSON; bulk texts compare by shared-reference string equality. */ -const sameUpdateParams = (a: any, b: any): boolean => { +/** The state-bearing half of every updateSnapshot: what Go holds for a config + * once a send lands. Every field is idempotent on the Go side (options + * replace, open refs are a set, extras/additional roots are sticky), so + * re-sending an identical steady half changes nothing. */ +type SteadySyncParams = { + openProject: { fileName: string; compilerOptions: Record }; + openFiles?: string[]; + extraFileExtensions?: any[]; + additionalFiles?: string[]; +}; +/** The one-shot half: consumed by the send that carries it. */ +type SyncDelta = { + openFilesWithContent?: any[]; + fileChanges?: { changed: string[] }; + closeFiles?: string[]; + closeProjects?: string[]; + prefetchDiagnostics?: boolean; +}; + +function steadySyncParams(configFilePath: string, options: any, openFiles: string[]): SteadySyncParams { + const additionalFiles = _lastAdditionalFilesByConfig.get(configFilePath); + return { + openProject: openProjectParam(configFilePath, options), + ...(openFiles.length > 0 ? { openFiles } : {}), + ...(_lastExtraFileExtensions?.length ? { extraFileExtensions: _lastExtraFileExtensions } : {}), + ...(additionalFiles?.length ? { additionalFiles } : {}), + }; +} + +/** Last landed steady params + the project they yielded, per config. */ +const _lastSyncByConfig = new Map(); + +function sameSteadySyncParams(a: SteadySyncParams, b: SteadySyncParams): boolean { const names = (x: readonly string[] | undefined, y: readonly string[] | undefined): boolean => x === y || (!!x && !!y && x.length === y.length && x.every((v, i) => v === y[i])); - const filesWithContent = (x: readonly any[] | undefined, y: readonly any[] | undefined): boolean => - x === y || (!!x && !!y && x.length === y.length && x.every((f, i) => f.fileName === y[i].fileName && f.scriptKind === y[i].scriptKind && f.content === y[i].content - // Edit pushes carry content:"" plus the real change in edits — comparing content alone treats two different edits as equal and drops the push (sim-xfile s1 revert). - && f.baseVersion === y[i].baseVersion && JSON.stringify(f.edits) === JSON.stringify(y[i].edits))); - // Same-shape fileChanges must compare itemwise: a params object carrying - // changed entries vs one without (or with different ones) is a REAL state - // difference — treating them as equal would swallow the send (issue #49). - const fileChanges = (x: any, y: any): boolean => - x === y || (!!x && !!y - && !!x.invalidateAll === !!y.invalidateAll - && names(x.changed, y.changed) && names(x.created, y.created) && names(x.deleted, y.deleted)); - return names(a.openFiles, b.openFiles) - && names(a.closeFiles, b.closeFiles) - && names(a.closeProjects, b.closeProjects) + return a.openProject.fileName === b.openProject.fileName + && JSON.stringify(a.openProject.compilerOptions) === JSON.stringify(b.openProject.compilerOptions) + && names(a.openFiles, b.openFiles) && names(a.additionalFiles, b.additionalFiles) - && filesWithContent(a.openFilesWithContent, b.openFilesWithContent) - && fileChanges(a.fileChanges, b.fileChanges) - && !!a.prefetchDiagnostics === !!b.prefetchDiagnostics - && a.openProject?.fileName === b.openProject?.fileName - && JSON.stringify(a.openProject?.compilerOptions) === JSON.stringify(b.openProject?.compilerOptions) && JSON.stringify(a.extraFileExtensions) === JSON.stringify(b.extraFileExtensions); -}; +} /** Open client tabs mirrored to the per-process tsgo session. tsgo OpenFiles * is additive-only (ref-counted, session.go): a tab closed in the editor @@ -2471,33 +2484,76 @@ function forgetSyncedOverlay(fileName: string): void { _pendingOverlayEditsByFile.delete(fileName); } -/** Per-file overlay push decision: undefined = Go already holds exactly the - * host text (nothing to send); an `{edits, baseVersion}` delta when splicing - * the recorded host edits into the synced base reproduces the host text - * (plain TS only — .vue-class virtual TS is regenerated per host change, - * never edit-shaped, so it always takes the full-content path); full content - * otherwise. Consumes the file's pending edits either way. */ -function decideOverlayPush(fileName: string, hostText: string, scriptKind: number): any | undefined { +/** The per-file overlay rule every sync site shares. Returns the wire entry + * (undefined = Go already holds the host text) and whether host text + * diverges from disk (or disk lacks the file). + * - Diverging: nothing when the mirror already equals the host text; an + * `{edits, baseVersion}` delta when splicing the recorded host edits into + * the synced base reproduces it (plain TS only — .vue-class virtual TS is + * regenerated per host change, never edit-shaped); full content otherwise. + * - Back at disk after a prior overlay: re-push the text once so Go stops + * checking the stale overlay, and the mirror forgets the file. Host text + * equal to CURRENT disk is not necessarily what Go saw — SnapshotFS froze + * the first disk read; that case rides fileChanges.changed via the #49 + * pending set, not this rule. + * Consumes the file's pending edits either way. */ +function planOverlayPush(fileName: string, hostText: string, scriptKind: number): { entry: any | undefined; diverges: boolean } { const synced = _syncedOverlayContentByFile.get(fileName); - if (synced === hostText) { - _pendingOverlayEditsByFile.delete(fileName); - return undefined; + if (!shouldSendHostOverlay(fileName, hostText)) { + if (synced === undefined) return { entry: undefined, diverges: false }; + forgetSyncedOverlay(fileName); + return { entry: synced === hostText ? undefined : { fileName, content: hostText, scriptKind }, diverges: false }; } const pendingEdits = _pendingOverlayEditsByFile.get(fileName); _pendingOverlayEditsByFile.delete(fileName); + if (synced === hostText) return { entry: undefined, diverges: true }; if (synced !== undefined && pendingEdits?.length && !isExtraExtensionFileName(fileName) && applyOverlayEdits(synced, pendingEdits) === hostText) { - return { fileName, content: "", scriptKind, edits: pendingEdits, baseVersion: _syncedOverlayVersionByFile.get(fileName) ?? 0 }; + return { entry: { fileName, content: "", scriptKind, edits: pendingEdits, baseVersion: _syncedOverlayVersionByFile.get(fileName) ?? 0 }, diverges: true }; } - return { fileName, content: hostText, scriptKind }; + return { entry: { fileName, content: hostText, scriptKind }, diverges: true }; } /** overlay.sync trace line for a send batch (witness + perf A/B read this). */ -function traceOverlaySync(openFilesWithContent: readonly any[], deduped: boolean): void { +function traceOverlaySync(openFilesWithContent: readonly any[]): void { if (openFilesWithContent.length === 0) return; const deltaFiles = openFilesWithContent.filter(f => f.edits?.length); const edits = deltaFiles.reduce((n, f) => n + f.edits.length, 0); - _rpcTraceEvent("overlay.sync", `files=${openFilesWithContent.length} deltaFiles=${deltaFiles.length} edits=${edits} bytes=${JSON.stringify(openFilesWithContent).length} deduped=${deduped} names=${openFilesWithContent.map(f => f.fileName.split("/").slice(-2).join("/") + (f.edits?.length ? "(d)" : "")).join(",")}`); + _rpcTraceEvent("overlay.sync", `files=${openFilesWithContent.length} deltaFiles=${deltaFiles.length} edits=${edits} bytes=${JSON.stringify(openFilesWithContent).length} names=${openFilesWithContent.map(f => f.fileName.split("/").slice(-2).join("/") + (f.edits?.length ? "(d)" : "")).join(",")}`); +} + +/** + * The one updateSnapshot path. A send with an empty delta against the last + * landed steady params is skipped and the recorded project reused: every + * updateSnapshot REPLACES the Go snapshot and disposes the previous + * generation's handle registry, so a no-op send strands the reused thin + * program's cached handles ("symbol handle N not found in snapshot + * registry", sim-nav #5986 class), and watch-lint reissues identical + * params ~1000× (issue #11 perf). `snapshot` is undefined when skipped. + * The overlay mirror commits here, right after Go accepted the content. + */ +function syncSnapshot(configFilePath: string, steady: SteadySyncParams, delta: SyncDelta): { project: any; snapshot: any } { + const content = delta.openFilesWithContent ?? []; + const prev = _lastSyncByConfig.get(configFilePath); + if (content.length === 0 && !delta.fileChanges && !delta.closeFiles?.length && !delta.closeProjects?.length + && !delta.prefetchDiagnostics && prev && sameSteadySyncParams(prev.steady, steady)) { + return { project: prev.project, snapshot: undefined }; + } + traceOverlaySync(content); + const snapshot: any = _api.updateSnapshot({ + ...steady, + ...(content.length > 0 ? { openFilesWithContent: content } : {}), + ...(delta.fileChanges ? { fileChanges: delta.fileChanges } : {}), + ...(delta.closeFiles?.length ? { closeFiles: delta.closeFiles } : {}), + ...(delta.closeProjects?.length ? { closeProjects: delta.closeProjects } : {}), + ...(delta.prefetchDiagnostics ? { prefetchDiagnostics: true } : {}), + }); + for (const f of content) commitSyncedOverlay(f); + trackBuildProjectSnapshot(configFilePath, snapshot, [...(steady.openFiles ?? []), ...content.map(f => f.fileName)]); + const project = snapshot.getProject(configFilePath); + if (project) _lastSyncByConfig.set(configFilePath, { steady, project }); + else _lastSyncByConfig.delete(configFilePath); + return { project, snapshot }; } // Overlay-path cache: only files missing on disk are fed to tsgo as overlays @@ -2560,6 +2616,7 @@ function beginBuildProject(configFilePath: string): { closeParams: any; staleSna _buildModeRef.active = { configFilePath, openedFiles: new Set(), snapshots: [] }; if (!prev) return { closeParams: undefined, staleSnapshots: undefined }; _projectCache.delete(prev.configFilePath); + _lastSyncByConfig.delete(prev.configFilePath); // The overlays are being closed in tsgo — forget the synced-content memo // so a later re-push of identical content is not skipped. for (const f of prev.openedFiles) forgetSyncedOverlay(f); @@ -2742,8 +2799,8 @@ function collectTsgoOpenFileNames(syncHost: any, extra?: Iterable): stri }; // Extra (query-requested) names go LAST: hoisting them first makes the // collected order depend on the query target, which alternates the - // overlay push key between sibling queries and re-fires an empty - // updateSnapshot on every other request (see _lastOverlayPushKeyByConfig). + // steady openFiles between sibling queries and re-fires an empty + // updateSnapshot on every other request (see syncSnapshot). const scriptNames = syncHost?.getScriptFileNames?.(); if (scriptNames) { for (const fn of scriptNames) add(fn); @@ -5879,10 +5936,7 @@ function resolveLanguageServiceScriptKind( } return inferScriptKind(hostFileName); } -/** Overlay when host snapshot text differs from disk (or file is absent on disk). - * Same-as-current-disk host text is not necessarily same-as-Go-saw — SnapshotFS - * froze the first disk read; that case rides fileChanges.changed via the #49 - * pending set, not this gate. */ +/** Host text differs from disk (or disk lacks the file) — see planOverlayPush. */ function shouldSendHostOverlay(fileName: string, hostText: string): boolean { if (!isOverlayCandidatePath(fileName)) return false; if (!fileExistsOnDisk(fileName)) return true; @@ -6833,19 +6887,6 @@ function tnbComputeNamedDeclarations(sourceFile: any): Map { * module-level lsnav wire entry (tsgoLsApiRequest) can sync on demand. */ const _overlaySyncByConfig = new Map void>(); -/** - * Last synced open-file set per config (join of the collected open names), - * for the no-change fast path in pushHostOverlayToTsgo. Every updateSnapshot - * REPLACES the Go snapshot and disposes the previous one's handle registry — - * a query-only sync with the same open set and no content to push must be a - * no-op, or every replayed lsnav query strands the reused thin program's - * cached handles in a disposed snapshot ("symbol handle N not found in - * snapshot registry", sim-nav #5986 class). ensureProject records the same - * key after its own snapshot so the first query sync after a rebuild is a - * no-op too. - */ -const _lastOverlayPushKeyByConfig = new Map(); - export function tsgoLsApiRequest(program: any, method: string, params: any): any { const configFilePath = program?.getCompilerOptions?.()?.configFilePath; let proj = configFilePath && _projectCache.get(configFilePath); @@ -7142,22 +7183,9 @@ export function createTsgoProgram( // from the same host snapshot on first access), so program // creation pays text only, not one JS AST per virtual file (B-1). // Pure disk lint skips this and uses tsgo-backed single-parse. - if (!shouldSendHostOverlay(resolvedFn, content.text)) { - // Host matches disk again after a prior overlay — re-push on-disk text - // so tsgo does not keep checking stale overlay content. - const synced = _syncedOverlayContentByFile.get(resolvedFn); - if (synced !== undefined && synced !== content.text) { - forgetSyncedOverlay(resolvedFn); - overlays.push({ fileName: resolvedFn, content: content.text, scriptKind: content.scriptKind }); - } - else if (synced !== undefined) { - forgetSyncedOverlay(resolvedFn); - } - continue; - } - const entry = decideOverlayPush(resolvedFn, content.text, content.scriptKind); + const { entry, diverges } = planOverlayPush(resolvedFn, content.text, content.scriptKind); if (!entry) continue; - hostContentByFile.set(resolvedFn, content); + if (diverges) hostContentByFile.set(resolvedFn, content); overlays.push(entry); } } @@ -9676,9 +9704,8 @@ export function createTsgoChecker(program: any): any { programCtx.pendingOverlays = undefined; } - const extraFileExtensions = programCtx?.pendingExtraFileExtensions; + if (programCtx?.pendingExtraFileExtensions) _lastExtraFileExtensions = programCtx.pendingExtraFileExtensions; if (programCtx) programCtx.pendingExtraFileExtensions = undefined; - if (extraFileExtensions) _lastExtraFileExtensions = extraFileExtensions; // Host-computed root set the tsconfig expansion may miss (LS // getScriptFileNames shims, glint readDirectory extras) — Go adds them @@ -9688,7 +9715,6 @@ export function createTsgoChecker(program: any): any { _lastAdditionalFilesByConfig.set(configFilePath!, programCtx.pendingAdditionalFiles); programCtx.pendingAdditionalFiles = undefined; } - const additionalFiles = _lastAdditionalFilesByConfig.get(configFilePath!); // #49: files the host told us were rewritten on disk (or where a // host read diverged from disk at materialization). They are NOT @@ -9734,55 +9760,20 @@ export function createTsgoChecker(program: any): any { // then joins the in-flight pass (Go-side singleflight). Never set for // interactive hosts: a full check per keystroke would be pure waste. const prefetchDiagnostics = !!(options as any).tscBuild; - // Watch-mode lint rebuilds the thin program per linted file with - // byte-identical updateSnapshot params (same open files, no new - // overlays): Go's answer is deterministic and already cached here — - // skip the round trip. Interactive-only: build mode's beginBuildProject - // / prefetch side effects must always fire. The fingerprint covers - // every value that can alter the response (open files, overlay - // contents, extras, close params, prefetch, fileChanges). - const updateParams = { - openProject: openProjectParam(configFilePath!, options), - ...(openFiles.length > 0 ? { openFiles } : {}), - ...(closedTabs?.length ? { closeFiles: closedTabs } : {}), - ...(openFilesWithContent.length > 0 ? { openFilesWithContent } : {}), - ...(extraFileExtensions ? { extraFileExtensions } : {}), - ...(additionalFiles?.length ? { additionalFiles } : {}), + const steady = steadySyncParams(configFilePath!, options, openFiles); + const synced = syncSnapshot(configFilePath!, steady, { + openFilesWithContent, ...(externalChanged?.length ? { fileChanges: { changed: externalChanged } } : {}), - ...(buildClose.closeParams ?? {}), - ...(prefetchDiagnostics ? { prefetchDiagnostics: true } : {}), - }; - const prevUpdate = (options as any).tscBuild ? undefined : _lastUpdateParamsByConfig.get(configFilePath!); - const deduped = prevUpdate !== undefined && sameUpdateParams(prevUpdate.params, updateParams); - traceOverlaySync(openFilesWithContent, deduped); - const snapshot: any = deduped ? undefined : _api.updateSnapshot(updateParams); + ...(closedTabs?.length ? { closeFiles: closedTabs } : {}), + ...buildClose.closeParams, + prefetchDiagnostics, + }); if (programCtx) programCtx.pendingReferencedProjects = undefined; - project = deduped ? prevUpdate!.project : snapshot.getProject(configFilePath!); + project = synced.project; if (!project) { throw new Error(`tsgoChecker: project not found for ${configFilePath}`); } - if (!deduped && !(options as any).tscBuild) { - // Fingerprint WITHOUT fileChanges (#49): external-change entries - // are one-shot — a later snapshot with identical steady-state - // params and no fileChanges must still dedupe against this one. - _lastUpdateParamsByConfig.set(configFilePath!, { params: { ...updateParams, fileChanges: undefined }, project }); - } - if (snapshot) { - trackBuildProjectSnapshot(configFilePath!, snapshot, [ - ...openFiles, - ...openFilesWithContent.map(f => f.fileName), - ]); - // Go now holds exactly this open set with all host content - // synced — record the query-path no-change key so the first - // pushHostOverlayToTsgo after a rebuild is a no-op instead of - // one empty updateSnapshot (which would dispose the handle - // registry the fresh program's caches just warmed). - _lastOverlayPushKeyByConfig.set(configFilePath!, openFiles.join("\n")); - } releaseStaleBuildSnapshots(buildClose.staleSnapshots); - for (const f of openFilesWithContent) { - commitSyncedOverlay(f); - } // Cross-project extra-extension imports (e.g. ../other/foo.vue): the // program can include host-virtual files that were not in this // project's root set, so no overlay was pushed for them and tsgo @@ -9793,7 +9784,7 @@ export function createTsgoChecker(program: any): any { // host.getSourceFile. Only files not already overlaid are sent. { const sentOverlayFiles = new Set(openFilesWithContent.map(f => f.fileName)); - const lateOverlays: { fileName: string; content: string; scriptKind: number }[] = []; + const lateOverlays: any[] = []; // names are host-form already (decode-boundary normalized in // tsgoSourceFileNames) — no per-entry re-normalization here. for (const hostFileName of tsgoSourceFileNames(configFilePath!, project).names) { @@ -9801,33 +9792,18 @@ export function createTsgoChecker(program: any): any { if (sentOverlayFiles.has(hostFileName) || _syncedOverlayContentByFile.has(hostFileName)) continue; const content = getHostScriptContent(syncHost ?? programCtx?.overlayHostCtx?.host, hostFileName, options); if (!content?.text || !content.fromHost) continue; - // Same-as-disk host content adds nothing (tsgo already parsed - // the disk text) — only genuine virtual content is pushed. - if (!shouldSendHostOverlay(hostFileName, content.text)) continue; - lateOverlays.push({ fileName: hostFileName, content: content.text, scriptKind: content.scriptKind }); + const { entry } = planOverlayPush(hostFileName, content.text, content.scriptKind); + if (entry) lateOverlays.push(entry); } if (lateOverlays.length > 0) { - const lateSnapshot: any = _api.updateSnapshot({ - openProject: openProjectParam(configFilePath!, options), - openFilesWithContent: lateOverlays, - ...(extraFileExtensions ? { extraFileExtensions } : {}), - // This push supersedes the snapshot that carried the - // build-mode prefetch, and Go cancels a superseded - // snapshot's in-flight pass — re-request it here so the - // whole-program check restarts on the virtual-content - // state and overlaps the remaining builder work instead - // of running synchronously inside getGlobalDiagnostics. - ...(prefetchDiagnostics ? { prefetchDiagnostics: true } : {}), - }); - // The push advanced Go state — the params dedupe must not - // skip the next ensureProject's refresh for this config. - _lastUpdateParamsByConfig.delete(configFilePath!); - const refreshed = lateSnapshot.getProject(configFilePath!); - if (refreshed) { - project = refreshed; - trackBuildProjectSnapshot(configFilePath!, lateSnapshot, lateOverlays.map(f => f.fileName)); - for (const f of lateOverlays) commitSyncedOverlay(f); - } + // This push supersedes the snapshot that carried the + // build-mode prefetch, and Go cancels a superseded + // snapshot's in-flight pass — re-request it here so the + // whole-program check restarts on the virtual-content + // state and overlaps the remaining builder work instead + // of running synchronously inside getGlobalDiagnostics. + const late = syncSnapshot(configFilePath!, steady, { openFilesWithContent: lateOverlays, prefetchDiagnostics }); + if (late.project) project = late.project; } } _projectCache.set(configFilePath!, project); @@ -10153,70 +10129,20 @@ export function createTsgoChecker(program: any): any { // mirror the walk is about to reconcile — the same order as the collect. const externalChanged = drainExternalFileChanges()?.changed; const openFiles = collectTsgoOpenFileNames(syncHost, requestedFileName ? [requestedFileName] : undefined); - const openFilesWithContent: { fileName: string; content: string; scriptKind: number }[] = []; + const openFilesWithContent: any[] = []; for (const hostFileName of openFiles) { if (!isOverlayCandidatePath(hostFileName)) continue; const content = getHostScriptContent(syncHost, hostFileName, ctx.options); if (!content?.text) continue; - const hostOnly = !fileExistsOnDisk(hostFileName); - const inTsgo = !!project?.program?.getSourceFile?.(toTsgoFileName(hostFileName)); - if (!hostOnly && inTsgo && !shouldSendHostOverlay(hostFileName, content.text)) { - const synced = _syncedOverlayContentByFile.get(hostFileName); - if (synced !== undefined && synced !== content.text) { - forgetSyncedOverlay(hostFileName); - openFilesWithContent.push({ fileName: hostFileName, content: content.text, scriptKind: content.scriptKind }); - } - else if (synced !== undefined) { - forgetSyncedOverlay(hostFileName); - } - continue; - } - if (!hostOnly) { - const entry = decideOverlayPush(hostFileName, content.text, content.scriptKind); - if (!entry) continue; - openFilesWithContent.push(entry); - continue; - } - openFilesWithContent.push({ fileName: hostFileName, content: content.text, scriptKind: content.scriptKind }); + const { entry } = planOverlayPush(hostFileName, content.text, content.scriptKind); + if (entry) openFilesWithContent.push(entry); } if (!openFiles.length && !openFilesWithContent.length && !externalChanged) return; - - // No-change fast path: bumping the snapshot here would dispose the - // handle registry the reused thin program's caches still reference - // (see _lastOverlayPushKeyByConfig). Content changes are detected - // upstream (decideOverlayPush / shouldSendHostOverlay), so identical - // open set + nothing to push means Go state already matches host. - // The key covers the open set only: the fast path requires empty - // content, and after any successful push the mirror holds whatever - // was pushed — recording the no-content form lets a content push and - // a following query-only sync compare equal instead of churning one - // empty snapshot. ensureProject records the same key after its own - // snapshot so the first query sync after a rebuild is a no-op too. - const pushKey = openFiles.join("\n"); - if (openFilesWithContent.length === 0 && !externalChanged && _lastOverlayPushKeyByConfig.get(ctx.configFilePath) === pushKey) return; - - traceOverlaySync(openFilesWithContent, /*deduped*/ false); - const snapshot: any = _api.updateSnapshot({ - openProject: openProjectParam(ctx.configFilePath, ctx.options), - ...(openFiles.length > 0 ? { openFiles } : {}), + const { project: refreshed, snapshot } = syncSnapshot(ctx.configFilePath, steadySyncParams(ctx.configFilePath, ctx.options, openFiles), { openFilesWithContent, ...(externalChanged ? { fileChanges: { changed: externalChanged } } : {}), - ...(_lastExtraFileExtensions ? { extraFileExtensions: _lastExtraFileExtensions } : {}), - // Host-injected extra roots (svelte2tsx/glint shims) — without them - // every hook-driven rebuild drops the ambient shim files from the - // program (#5847 svelteHTML false positive). - ...(_lastAdditionalFilesByConfig.get(ctx.configFilePath)?.length ? { additionalFiles: _lastAdditionalFilesByConfig.get(ctx.configFilePath) } : {}), }); - _lastOverlayPushKeyByConfig.set(ctx.configFilePath, pushKey); - // The push advanced Go state — the params dedupe must not skip the - // next ensureProject's refresh for this config. - _lastUpdateParamsByConfig.delete(ctx.configFilePath); - trackBuildProjectSnapshot(ctx.configFilePath, snapshot, [ - ...openFiles, - ...openFilesWithContent.map(f => f.fileName), - ]); - const refreshed = snapshot.getProject(ctx.configFilePath); - if (!refreshed) return; + if (!snapshot || !refreshed) return; project = refreshed; // Wire objects route prototype API calls through their registry's // project, so a replacement generation must own the live checker @@ -10225,7 +10151,6 @@ export function createTsgoChecker(program: any): any { _projectCache.set(ctx.configFilePath, refreshed); _currentProjectRef.project = refreshed; installTsgoBackedSourceFileLoader(() => project); - for (const f of openFilesWithContent) commitSyncedOverlay(f); for (const fileName of [...openFilesWithContent.map(f => f.fileName), ...(externalChanged ?? [])]) { tsgoSfCache.delete(fileName); nodeIndexCache.delete(fileName); @@ -10242,7 +10167,7 @@ export function createTsgoChecker(program: any): any { // change or not. (The former "content-only" guard kept stale handles // across openFiles-only bumps, which is how replayed queries faulted // with "symbol handle N not found in snapshot registry", sim-nav - // #5986 class. The no-change fast path above is what keeps this from + // #5986 class. syncSnapshot's no-op skip is what keeps this from // costing re-resolution on every query.) _rpcTraceEvent( "overlay.refresh.clearCaches", diff --git a/tools/triage-overlay-delta-sync.mjs b/tools/triage-overlay-delta-sync.mjs index ca95ea6..8b5019e 100644 --- a/tools/triage-overlay-delta-sync.mjs +++ b/tools/triage-overlay-delta-sync.mjs @@ -111,7 +111,7 @@ const events = fs.existsSync(traceFile) : []; if (events.length === 0) fail('no overlay.sync trace events (TNB_TRACE_RPC instrumentation missing?)'); for (const e of events) console.log(`trace ${e}`); -const sends = events.filter(e => /deduped=false/.test(e)); +const sends = events; if (!sends.some(e => /deltaFiles=0/.test(e))) fail(`expected an initial full push (deltaFiles=0): ${JSON.stringify(sends)}`); if (!sends.some(e => /deltaFiles=[1-9]/.test(e))) fail(`no delta-shaped sync after the second edit round: ${JSON.stringify(sends)}`); const byteLines = sends.map(e => +(e.match(/bytes=(\d+)/)?.[1] ?? Infinity)); From c1116c9c9eed2d6ead2ccb45e8e8ecdf430a2a6a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 04:04:41 +0000 Subject: [PATCH 4/4] refactor(bridge): snapshot-bound checker memos live in one generation object pushHostOverlayToTsgo reset 18 snapshot-bound memos through a hand-kept clear list, and a new memo had to remember to join it. They are now fields of newSnapshotCaches(), and a snapshot bump replaces the whole object; the per-file scan caches that deliberately reuse across generations stay outside. Co-authored-by: Johnson Chu --- .../overlay/src/compiler/tsgoChecker.ts | 280 +++++++++--------- 1 file changed, 132 insertions(+), 148 deletions(-) diff --git a/patches/typescript/overlay/src/compiler/tsgoChecker.ts b/patches/typescript/overlay/src/compiler/tsgoChecker.ts index 76ca073..128f2b3 100644 --- a/patches/typescript/overlay/src/compiler/tsgoChecker.ts +++ b/patches/typescript/overlay/src/compiler/tsgoChecker.ts @@ -9837,20 +9837,82 @@ export function createTsgoChecker(program: any): any { // ── Position → tsgo Node finder (cached per file) ──────────────── const tsgoSfCache = new Map(); const nodeAtPosCache = new Map>(); - // Symbol cache keyed by (fileName, end-offset) — the fast path for - // getSymbolAtLocation, which resolves the vast majority of the ~30k - // scope-manager identifier queries without touching the node index. - const symByPos = new Map>(); + // ── Snapshot-generation caches ─────────────────────────────────── + // Every memo here holds handles bound to one Go snapshot generation. + // Each updateSnapshot disposes the previous generation's handle registry, + // content change or not, so a snapshot bump inside this checker's + // lifetime (pushHostOverlayToTsgo) replaces the whole object — a new + // snapshot-bound memo cannot be forgotten by a clear list. The per-file + // scan caches (tsgoSfCache, nodeIndexCache, nodeAtPosCache) stay outside: + // they reuse across generations and invalidate per changed file. + const newSnapshotCaches = () => ({ + // Symbol cache keyed by (fileName, end-offset) — the fast path for + // getSymbolAtLocation, which resolves the vast majority of the ~30k + // scope-manager identifier queries without touching the node index. + symByPos: new Map>(), + // refineNavSymbol memo: the same symbol is refined once even when + // queried from many reference sites (e.g. 100 refs to `foo`). Keyed by + // symbol object identity (tsgo symbols are id-keyed singletons via + // objectRegistry; host-bound symbols are real TS symbol objects). + refinedSymBySym: new WeakMap(), + // Files whose import/export module-specifier symbols were + // batch-resolved in one getSymbolsAtLocations RPC over the tsgo + // SourceFile's `imports` list (see + // ensureModuleSpecifierSymbolsPrefetched). Multi-project lint runners + // resolve every import literal of every program file (BuilderState + // dependency walks, cache hashing); per-literal RPC dominates without this. + moduleSpecPrefetched: new Set(), + /** getSymbolsInScope memo — keyed (file:start:end:meaning); see adapter. */ + symbolsInScopeCache: new Map(), + /** tryFindAmbientModule memo — keyed unquoted module name; null = miss. */ + ambientModuleByNameCache: new Map(), + // Light ambient-module batch for builder referenced-files (module names + + // declaration merge only) via Checker.getAmbientModules. null = fetched + // and empty; undefined = not fetched. + ambientModuleBatchCache: undefined as any, + // Checker-quality ambient symbols for tryFindAmbientModule / export-info + // rehydrate. Binder-only light symbols lack merged exports and can hang + // getAliasedSymbol when auto-import walks ambient modules. + ambientModuleExportBatchCache: undefined as any, + /** Host→tsgo resolution memo (see rpc()). */ + rpcSymbolCache: new Map(), + nodeTypeCache: new Map(), + typeOfSymbolCache: new Map(), + propertiesCache: new Map(), + // Per-type name→property map, built lazily from the memoized + // getPropertiesOfType result. Collapses N getPropertyOfType(name) + // RPCs into 1 getPropertiesOfType RPC + JS lookup per type. + propertyByNameCache: new Map>(), + // Types for which getPropertiesOfType was used to bulk-fill propertyByNameCache. + propertyBulkLoaded: new Set(), + // Per-type signature cache keyed by SignatureKind. Unifies the proto + // (type.getCallSignatures/getConstructSignatures) and adapter + // (checker.getSignaturesOfType) paths onto one RPC per (type, kind). + signaturesByKindCache: new Map>(), + // Per-type base-types cache. Unifies proto (type.getBaseTypes) and adapter + // (checker.getBaseTypes) onto one RPC per type. + baseTypesCache: new Map(), + // Object-literal completion batch (see ObjCompletionBatch). + objCompletionPending: undefined as { hostNode: any; tsgoNode: any; results: any[] } | undefined, + objCompletionBatch: undefined as ObjCompletionBatch | undefined, + // getHostBoundSf memo: refineNavSymbol resolves the module parent of + // ~2k scope symbols per completion, each walking declarations and + // calling getHostBoundSf per declaration file. Repeating the + // resolveHostFileName + getScriptVersion + sfCache probe per call + // burned most of the completion wall. null = confirmed not host-parsed. + hostBoundSfMemo: new Map(), + }); + let gen = newSnapshotCaches(); const symCacheFileName = (fileName: string): string => { const h = hostForOverlaySyncLocal(); return resolveHostFileName(fileName, h); }; const getSymFileCache = (fileName: string, create = false): Map | undefined => { const key = symCacheFileName(fileName); - let fc = symByPos.get(key); + let fc = gen.symByPos.get(key); if (!fc && create) { fc = new Map(); - symByPos.set(key, fc); + gen.symByPos.set(key, fc); } return fc; }; @@ -9894,33 +9956,9 @@ export function createTsgoChecker(program: any): any { const fc = getSymFileCache(fileName, true)!; fc.set(`${start}:${end}`, sym); }; - // refineNavSymbol memo: the same symbol is refined once even when queried - // from many reference sites (e.g. 100 refs to `foo`). Keyed by symbol - // object identity (tsgo symbols are id-keyed singletons via objectRegistry; - // host-bound symbols are real TS symbol objects). Cleared on snapshot - // refresh alongside symByPos. - let refinedSymBySym = new WeakMap(); - // Files whose import/export module-specifier symbols were batch-resolved - // in one getSymbolsAtLocations RPC over the tsgo SourceFile's `imports` - // list (see ensureModuleSpecifierSymbolsPrefetched). Multi-project lint - // runners resolve every import literal of every program file (BuilderState - // dependency walks, cache hashing); per-literal RPC dominates without this. - const moduleSpecPrefetched = new Set(); - /** getSymbolsInScope memo — keyed (file:start:end:meaning); see adapter. */ - const symbolsInScopeCache = new Map(); // tsserver (projectService) needs full ambient export batch for exportInfoMap; // vue-tsc / builder hosts without projectService use the light batch. const isProjectServiceProgram = !!(programCtx?.lsHost as any)?.projectService; - /** tryFindAmbientModule memo — keyed unquoted module name; null = miss. */ - const ambientModuleByNameCache = new Map(); - // Light ambient-module batch for builder referenced-files (module names + - // declaration merge only) via Checker.getAmbientModules. null = fetched - // and empty; undefined = not fetched. - let ambientModuleBatchCache: any = undefined; - // Checker-quality ambient symbols for tryFindAmbientModule / export-info - // rehydrate. Binder-only light symbols lack merged exports and can hang - // getAliasedSymbol when auto-import walks ambient modules. - let ambientModuleExportBatchCache: any = undefined; // Per-file index: start position → all tsgo nodes that start there. // Built once per file via a single AST walk, after which every // findTsgoNodeAtPosition call is an O(1) map lookup + a tiny kind/end @@ -10095,7 +10133,7 @@ export function createTsgoChecker(program: any): any { function getPropertiesOfTypeForExportEquals(type: any): readonly any[] { if (!type) return []; ensureProject(); - return memoGet(propertiesCache, type, () => project.checker.getPropertiesOfType(type) ?? []); + return memoGet(gen.propertiesCache, type, () => project.checker.getPropertiesOfType(type) ?? []); } function forEachExportEqualsProperties(moduleSymbol: any, cb: (symbol: any, key: string) => void): void { @@ -10161,61 +10199,36 @@ export function createTsgoChecker(program: any): any { // AST against the new snapshot. programCtx?.thinProgram?.__tnbInvalidateHostSourceFile?.(fileName); } - // Every updateSnapshot REPLACES the Go snapshot and disposes the - // previous generation's handle registry — the caches below all hold - // snapshot-bound handles, so they must reset on ANY bump, content - // change or not. (The former "content-only" guard kept stale handles - // across openFiles-only bumps, which is how replayed queries faulted - // with "symbol handle N not found in snapshot registry", sim-nav - // #5986 class. syncSnapshot's no-op skip is what keeps this from - // costing re-resolution on every query.) + // Any bump starts a new generation, content change or not (see + // newSnapshotCaches); syncSnapshot's no-op skip is what keeps this + // from costing re-resolution on every query. _rpcTraceEvent( "overlay.refresh.clearCaches", `files=${openFilesWithContent.length} paths=${openFilesWithContent.map(f => f.fileName).join(",")}` - + ` symByPosFiles=${symByPos.size} symbolsInScope=${symbolsInScopeCache.size}` - + ` nodeType=${nodeTypeCache.size} typeOfSymbol=${typeOfSymbolCache.size}` - + ` properties=${propertiesCache.size}`, + + ` symByPosFiles=${gen.symByPos.size} symbolsInScope=${gen.symbolsInScopeCache.size}` + + ` nodeType=${gen.nodeTypeCache.size} typeOfSymbol=${gen.typeOfSymbolCache.size}` + + ` properties=${gen.propertiesCache.size}`, ); - symByPos.clear(); - hostBoundSfMemo.clear(); - moduleSpecPrefetched.clear(); - symbolsInScopeCache.clear(); - ambientModuleByNameCache.clear(); - ambientModuleBatchCache = undefined; - ambientModuleExportBatchCache = undefined; - rpcSymbolCache.clear(); - nodeTypeCache.clear(); - typeOfSymbolCache.clear(); - propertiesCache.clear(); - propertyByNameCache.clear(); - propertyBulkLoaded.clear(); - signaturesByKindCache.clear(); - baseTypesCache.clear(); - _objCompletionPending = undefined; - _objCompletionBatch = undefined; - // Invalidate Symbol.parent memos pinned on instances that outlive the - // cleared maps (Soft-P′ / host-bound may still hold Symbol refs). + gen = newSnapshotCaches(); + // Symbol.parent memos are stamped on instances that outlive gen. _tnbParentMemoEpoch++; - // WeakMap has no clear(); drop Soft-P′/S′ memo so remapped decls cannot - // stick across overlay content refresh (comment claimed this already). - refinedSymBySym = new WeakMap(); } function getAmbientModuleBatch(): any { - if (ambientModuleBatchCache !== undefined) return ambientModuleBatchCache ?? undefined; - ambientModuleBatchCache = project.checker.getAmbientModules() ?? null; - return ambientModuleBatchCache ?? undefined; + if (gen.ambientModuleBatchCache !== undefined) return gen.ambientModuleBatchCache ?? undefined; + gen.ambientModuleBatchCache = project.checker.getAmbientModules() ?? null; + return gen.ambientModuleBatchCache ?? undefined; } function getAmbientModuleExportBatch(): any { - if (ambientModuleExportBatchCache !== undefined) return ambientModuleExportBatchCache ?? undefined; + if (gen.ambientModuleExportBatchCache !== undefined) return gen.ambientModuleExportBatchCache ?? undefined; try { - ambientModuleExportBatchCache = normalizeExportMapWireNames(project.checker.getModuleExportMap?.()) ?? null; + gen.ambientModuleExportBatchCache = normalizeExportMapWireNames(project.checker.getModuleExportMap?.()) ?? null; } catch { - ambientModuleExportBatchCache = null; + gen.ambientModuleExportBatchCache = null; } - return ambientModuleExportBatchCache ?? undefined; + return gen.ambientModuleExportBatchCache ?? undefined; } function getTsgoSourceFile(fileName: string): any { @@ -10279,21 +10292,21 @@ export function createTsgoChecker(program: any): any { * import literal of every program file; per-literal positional RPC plus the * node-index fallback dominated lint wall time. Results (including * undefined for unresolved modules) are stored positionally so subsequent - * queries hit symByPos. Returns false when the file has no tsgo mirror. + * queries hit gen.symByPos. Returns false when the file has no tsgo mirror. */ function ensureModuleSpecifierSymbolsPrefetched(fileName: string): boolean { const cacheName = symCacheFileName(fileName); - if (moduleSpecPrefetched.has(cacheName)) return true; + if (gen.moduleSpecPrefetched.has(cacheName)) return true; const sf = getTsgoSourceFile(fileName); if (!sf) return false; - moduleSpecPrefetched.add(cacheName); + gen.moduleSpecPrefetched.add(cacheName); const importNodes: readonly any[] = sf.imports ?? []; if (!importNodes.length) return true; let syms: readonly any[]; try { syms = project.checker.getSymbolAtLocation(importNodes as any[]) ?? []; } catch { - moduleSpecPrefetched.delete(cacheName); + gen.moduleSpecPrefetched.delete(cacheName); return false; } for (let i = 0; i < importNodes.length; i++) { @@ -10464,9 +10477,6 @@ export function createTsgoChecker(program: any): any { // direction (tsgo symbol → host navigation symbol) is refineNavSymbol. // Adapter methods therefore never branch on a symbol's origin. - /** Host→tsgo resolution memo; cleared with symByPos on overlay refresh. */ - const rpcSymbolCache = new Map(); - function tsgoSymbolForHostDeclaration(decl: any): any { const sf = decl?.getSourceFile?.(); if (!sf?.fileName) return undefined; @@ -10561,7 +10571,7 @@ export function createTsgoChecker(program: any): any { function resolveRpcSymbol(symbol: any): any { if (!symbol) return undefined; if (isTsgoBridgeSymbol(symbol)) return symbol; - if (rpcSymbolCache.has(symbol)) return rpcSymbolCache.get(symbol); + if (gen.rpcSymbolCache.has(symbol)) return gen.rpcSymbolCache.get(symbol); let resolved: any; const decls = symbol.declarations?.length ? symbol.declarations @@ -10570,7 +10580,7 @@ export function createTsgoChecker(program: any): any { resolved = tsgoSymbolForHostDeclaration(decl); if (resolved) break; } - rpcSymbolCache.set(symbol, resolved); + gen.rpcSymbolCache.set(symbol, resolved); return resolved; } @@ -10643,24 +10653,6 @@ export function createTsgoChecker(program: any): any { return facade; } - // ── Caches ─────────────────────────────────────────────────────── - const nodeTypeCache = new Map(); - const typeOfSymbolCache = new Map(); - const propertiesCache = new Map(); - // Per-type name→property map, built lazily from the memoized - // getPropertiesOfType result. Collapses N getPropertyOfType(name) - // RPCs into 1 getPropertiesOfType RPC + JS lookup per type. - const propertyByNameCache = new Map>(); - // Per-type signature cache keyed by SignatureKind. Unifies the proto - // (type.getCallSignatures/getConstructSignatures) and adapter - // (checker.getSignaturesOfType) paths onto one RPC per (type, kind). - const signaturesByKindCache = new Map>(); - // Per-type base-types cache. Unifies proto (type.getBaseTypes) and adapter - // (checker.getBaseTypes) onto one RPC per type. - const baseTypesCache = new Map(); - // Types for which getPropertiesOfType was used to bulk-fill propertyByNameCache. - const propertyBulkLoaded = new Set(); - const memoGet = (cache: Map, key: K, compute: () => V): V => { if (cache.has(key)) return cache.get(key)!; const v = compute(); @@ -10736,8 +10728,6 @@ export function createTsgoChecker(program: any): any { filteredMembers: any[]; // final members surviving the stock filter properties: any[]; } - let _objCompletionPending: { hostNode: any; tsgoNode: any; results: any[] } | undefined; - let _objCompletionBatch: ObjCompletionBatch | undefined; const sameTypeList = (a: readonly any[], b: readonly any[]): boolean => a.length === b.length && a.every((t, i) => t === b[i]); @@ -10747,9 +10737,9 @@ export function createTsgoChecker(program: any): any { // arrives, the next per-member call recomputes for the new node (the // discriminant verdicts are node-dependent). const activeObjCompletionBatch = (): ObjCompletionBatch | undefined => { - const b = _objCompletionBatch; + const b = gen.objCompletionBatch; if (!b) return undefined; - if (_objCompletionPending && _objCompletionPending.hostNode !== b.hostNode) return undefined; + if (gen.objCompletionPending && gen.objCompletionPending.hostNode !== b.hostNode) return undefined; return b; }; @@ -10759,7 +10749,7 @@ export function createTsgoChecker(program: any): any { // only: completions.ts reads `.types` before filtering, so the memo is // already populated and this check never issues an RPC of its own. const tryStartObjCompletionBatch = (memberType: any): ObjCompletionBatch | undefined => { - const pending = _objCompletionPending; + const pending = gen.objCompletionPending; const proj = _currentProjectRef.project; if (!pending || !proj || typeof proj.checker.getPropertiesForObjectExpression !== "function") return undefined; let contextualType: any; @@ -10803,19 +10793,19 @@ export function createTsgoChecker(program: any): any { // before any checker call); leave them out so an unexpected query // falls through to the real RPC instead of a fabricated verdict. if ((fm.type.flags & TF.Primitive) === 0) verdicts.set(fm.type, fm); - if (fm.apparentProperties && !propertiesCache.has(fm.type)) { - propertiesCache.set(fm.type, fm.apparentProperties); + if (fm.apparentProperties && !gen.propertiesCache.has(fm.type)) { + gen.propertiesCache.set(fm.type, fm.apparentProperties); } } for (const t of info.filteredTypes) fixupType(t); const finalType = info.mergedType ?? info.promiseFilteredType; const isFinalUnion = info.finalMembers.length > 0; // Non-union final type: stock calls type.getApparentProperties() on it, - // which routes through propertiesCache — seed it. - if (!isFinalUnion && finalType && !propertiesCache.has(finalType)) { - propertiesCache.set(finalType, info.properties); + // which routes through gen.propertiesCache — seed it. + if (!isFinalUnion && finalType && !gen.propertiesCache.has(finalType)) { + gen.propertiesCache.set(finalType, info.properties); } - _objCompletionBatch = { + gen.objCompletionBatch = { hostNode: pending.hostNode, completionsType, promised, @@ -10827,22 +10817,22 @@ export function createTsgoChecker(program: any): any { filteredMembers: info.filteredTypes, properties: info.properties as any[], }; - return _objCompletionBatch; + return gen.objCompletionBatch; }; const resolvePropertyOfType = (type: any, name: string): any => { const proj = projectForBridgeObject(type) ?? _currentProjectRef.project; if (!proj || !type) return undefined; - let byName = propertyByNameCache.get(type); + let byName = gen.propertyByNameCache.get(type); if (!byName) { byName = new Map(); - propertyByNameCache.set(type, byName); + gen.propertyByNameCache.set(type, byName); } if (byName.has(name)) return byName.get(name); // One getPropertiesOfType RPC per type replaces many getPropertyOfType RPCs. - if (!propertyBulkLoaded.has(type)) { - propertyBulkLoaded.add(type); - const props = memoGet(propertiesCache, type, () => proj.checker.getPropertiesOfType(type) ?? []); + if (!gen.propertyBulkLoaded.has(type)) { + gen.propertyBulkLoaded.add(type); + const props = memoGet(gen.propertiesCache, type, () => proj.checker.getPropertiesOfType(type) ?? []); for (const p of props) { if (p?.name) byName.set(p.name, p); } @@ -10856,8 +10846,8 @@ export function createTsgoChecker(program: any): any { const getSignaturesCached = (type: any, kind: number): readonly any[] => { const proj = projectForBridgeObject(type) ?? _currentProjectRef.project; if (!proj) return []; - let byKind = signaturesByKindCache.get(type); - if (!byKind) { byKind = new Map(); signaturesByKindCache.set(type, byKind); } + let byKind = gen.signaturesByKindCache.get(type); + if (!byKind) { byKind = new Map(); gen.signaturesByKindCache.set(type, byKind); } const hit = byKind.get(kind); if (hit !== undefined) return hit; const r = proj.checker.getSignaturesOfType(type, kind) ?? []; @@ -10868,7 +10858,7 @@ export function createTsgoChecker(program: any): any { const getBaseTypesCached = (type: any): readonly any[] => { const proj = projectForBridgeObject(type) ?? _currentProjectRef.project; if (!proj) return []; - return memoGet(baseTypesCache, type, () => proj.checker.getBaseTypes(type) ?? []); + return memoGet(gen.baseTypesCache, type, () => proj.checker.getBaseTypes(type) ?? []); }; // Faithful forward: tsgo GetTypeAtLocation (checker getTypeOfNode) handles @@ -10883,14 +10873,8 @@ export function createTsgoChecker(program: any): any { } // ── Build adapter object ───────────────────────────────────────── - // Memoized per checker generation: refineNavSymbol resolves the module - // parent of ~2k scope symbols per completion, each walking declarations - // and calling getHostBoundSf per declaration file. Repeating the - // resolveHostFileName + getScriptVersion + sfCache probe per call burned - // most of the completion wall. null = confirmed not host-parsed. - const hostBoundSfMemo = new Map(); const getHostBoundSf = (fileName: string): any | undefined => { - const memo = hostBoundSfMemo.get(fileName); + const memo = gen.hostBoundSfMemo.get(fileName); if (memo !== undefined) return memo === null ? undefined : memo; // Lib files never take the host-parse path (getOrCreateSourceFile // gates preferHostSourceFiles on !isHostLibFile), so probing them @@ -10899,7 +10883,7 @@ export function createTsgoChecker(program: any): any { // it. Skip the program lookup entirely. const hostFileName = toHostFileName(fileName); if (isBundledLibPath(fileName) || isHostLibFile(hostFileName)) { - hostBoundSfMemo.set(fileName, null); + gen.hostBoundSfMemo.set(fileName, null); return undefined; } // A .d.ts only needs host-bound remap when the host actually serves it @@ -10913,7 +10897,7 @@ export function createTsgoChecker(program: any): any { && isOverlayCandidatePath(hostFileName) && (hostHasScriptSnapshot(hostForOverlaySyncLocal(), hostFileName, hostFileName) || _syncedOverlayContentByFile.has(hostFileName)))) { - hostBoundSfMemo.set(fileName, null); + gen.hostBoundSfMemo.set(fileName, null); return undefined; } // Prefer THIS project's thin program: _hostProgramRef is module-global @@ -10929,15 +10913,15 @@ export function createTsgoChecker(program: any): any { // Soft-P′ soft-bound disk/node_modules files carry binder fields // (ExportSpecifier.symbol) without the overlay identity brand. if (!isHostParsedSourceFile(sf)) { - hostBoundSfMemo.set(fileName, null); + gen.hostBoundSfMemo.set(fileName, null); return undefined; } - hostBoundSfMemo.set(fileName, sf); + gen.hostBoundSfMemo.set(fileName, sf); return sf; }; const refineNavSymbol = (sym: any) => { if (!sym) return sym; - const cached = refinedSymBySym.get(sym); + const cached = gen.refinedSymBySym.get(sym); if (cached !== undefined) return cached; // Completion pulls ~1000 getSymbolsInScope globals per keystroke. // When host-bound (.vue) files exist, pure lib/ambient symbols with no @@ -10960,7 +10944,7 @@ export function createTsgoChecker(program: any): any { ensureSymbolContextualDocCompat(sym), getHostBoundSf, ); - refinedSymBySym.set(sym, light); + gen.refinedSymBySym.set(sym, light); return light; } if (!_hasHostBoundFiles) { @@ -10970,14 +10954,14 @@ export function createTsgoChecker(program: any): any { ensureClassLikeSymbolDeclarations(ensureSymbolContextualDocCompat(sym)), getHostBoundSf, ); - refinedSymBySym.set(sym, refinedNoHost); + gen.refinedSymBySym.set(sym, refinedNoHost); return refinedNoHost; } const refined = ensureClassLikeSymbolDeclarations( ensureSymbolContextualDocCompat(refineHostNavigationSymbol(sym, getHostBoundSf)), ); if (_traceSymEnabled) traceSym(`refineNavSymbol in=${traceSymSymbol(sym)} out=${traceSymSymbol(refined)}`); - refinedSymBySym.set(sym, refined); + gen.refinedSymBySym.set(sym, refined); return refined; }; @@ -11067,7 +11051,7 @@ export function createTsgoChecker(program: any): any { // range): stock returns undefined for the comment container, and // allowing positional/cache hits here poisons quickinfo inside // param JSDoc (`/** left */ a`) after an earlier probe warms - // symByPos. Broader JSDoc* short-circuit breaks component-meta + // gen.symByPos. Broader JSDoc* short-circuit breaks component-meta // (JSDoc type/tag nodes still need normal resolution). case SyntaxKind.JSDocComment: return { action: "undefined" }; @@ -11182,7 +11166,7 @@ export function createTsgoChecker(program: any): any { ensureProject(); // Stock (checker.ts:1714-1716): getParseTreeNode(nodeIn) then // getTypeOfNode, else errorType. Walk original before position map. - return memoGet(nodeTypeCache, node, () => { + return memoGet(gen.nodeTypeCache, node, () => { const t0 = process.env.TSGO_PROFILE === "1" ? Date.now() : 0; const tsgoNode = resolveHostNodeToTsgo(node); if (!tsgoNode) { @@ -11254,7 +11238,7 @@ export function createTsgoChecker(program: any): any { // file's module symbol (sf.symbol), not a tsgo position hit on the // first statement (e.g. the codegen export const) which lacks the default export. if (node.kind === SyntaxKind.SourceFile && sf.symbol) { - // Return the module symbol directly. Do NOT write symByPos here: + // Return the module symbol directly. Do NOT write gen.symByPos here: // this whole-file symbol has no single span, and caching it under // position 0 would poison lookups for any real node at pos 0. // This branch already short-circuits every SourceFile query, so a @@ -11423,7 +11407,7 @@ export function createTsgoChecker(program: any): any { // dominant cross-file query in multi-project lint). If the literal // wasn't in the file's imports list (rare — e.g. require() text in // a non-module position), fall through to the per-node path. - if (!sf.__tnbHostBound && !moduleSpecPrefetched.has(cacheName) && isModuleSpecifierStringLiteral(node)) { + if (!sf.__tnbHostBound && !gen.moduleSpecPrefetched.has(cacheName) && isModuleSpecifierStringLiteral(node)) { if (ensureModuleSpecifierSymbolsPrefetched(sf.fileName)) { cached = probeSymCache(cacheName, start, end); if (cached.found) { @@ -11504,12 +11488,12 @@ export function createTsgoChecker(program: any): any { // completions asks these right before getPropertiesForObjectExpression, // which lets the per-member calls below collapse into one batch RPC. if ((node.kind === SyntaxKind.ObjectLiteralExpression || node.kind === SyntaxKind.JsxAttributes) && tsgoNode.kind === node.kind) { - const pending = _objCompletionPending; + const pending = gen.objCompletionPending; if (pending && pending.hostNode === node) { pending.results.push(t); } else { - _objCompletionPending = { hostNode: node, tsgoNode, results: [t] }; + gen.objCompletionPending = { hostNode: node, tsgoNode, results: [t] }; } } return t; @@ -11659,7 +11643,7 @@ export function createTsgoChecker(program: any): any { getTypeOfSymbol(symbol: any): any { if (!symbol) return undefined; ensureProject(); - return memoGet(typeOfSymbolCache, symbol, () => { + return memoGet(gen.typeOfSymbolCache, symbol, () => { const t = rpc().getTypeOfSymbol(symbol); if (t) { fixupType(t); return t; } // Stock getTypeOfSymbol never returns undefined (checker.ts:12960): @@ -11937,7 +11921,7 @@ export function createTsgoChecker(program: any): any { getPropertiesOfType(type: any): readonly any[] { ensureProject(); if (!type) return []; - return memoGet(propertiesCache, type, () => project.checker.getPropertiesOfType(type) ?? []); + return memoGet(gen.propertiesCache, type, () => project.checker.getPropertiesOfType(type) ?? []); }, getPropertyOfType(type: any, name: string): any { ensureProject(); @@ -13150,8 +13134,8 @@ export function createTsgoChecker(program: any): any { if (!moduleName) return undefined; const key = moduleName.replace(/^"|"$/g, ""); ensureProject(); - if (ambientModuleByNameCache.has(key)) { - return ambientModuleByNameCache.get(key) ?? undefined; + if (gen.ambientModuleByNameCache.has(key)) { + return gen.ambientModuleByNameCache.get(key) ?? undefined; } try { const batch = getAmbientModuleExportBatch(); @@ -13159,13 +13143,13 @@ export function createTsgoChecker(program: any): any { if (mod.moduleFileName) continue; const name = mod.moduleName?.replace(/^"|"$/g, ""); if (name === key) { - ambientModuleByNameCache.set(key, mod.moduleSymbol); + gen.ambientModuleByNameCache.set(key, mod.moduleSymbol); return mod.moduleSymbol; } } } catch { /* empty */ } - ambientModuleByNameCache.set(key, null); + gen.ambientModuleByNameCache.set(key, null); return undefined; }, @@ -13182,7 +13166,7 @@ export function createTsgoChecker(program: any): any { const start = location.getStart(sf); const end = location.getEnd(sf); const scopeKey = `${symCacheFileName(sf.fileName)}:${start}:${end}:${meaning}`; - const memo = symbolsInScopeCache.get(scopeKey); + const memo = gen.symbolsInScopeCache.get(scopeKey); if (memo) return memo; let tsgoNode = findTsgoNodeAtPosition(sf.fileName, start, location.kind, end); if (!tsgoNode) { @@ -13198,13 +13182,13 @@ export function createTsgoChecker(program: any): any { // host binder locals so completion sortText uses === sourceFile. if (isHostParsedSourceFile(sf)) { result = mergeHostLocalScopeSymbols(result, location, meaning).map(sym => - refinedSymBySym.has(sym) ? sym : refineNavSymbol(sym), + gen.refinedSymBySym.has(sym) ? sym : refineNavSymbol(sym), ); } // Stock never puts module-default / re-export aliases into scope; // filtering them restores the `default` keyword completion slot. result = result.filter((sym: any) => !isStolenDefaultKeywordScopeSymbol(sym)); - symbolsInScopeCache.set(scopeKey, result); + gen.symbolsInScopeCache.set(scopeKey, result); return result; } // Genuine host-only virtual files have no tsgo mirror; walk host