Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tsc/internal/execute/incremental/buildInfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ type BuildInfo struct {
AffectedFilesPendingEmit []*BuildInfoFilePendingEmit `json:"affectedFilesPendingEmit,omitzero"`
LatestChangedDtsFile BuildInfoPath `json:"latestChangedDtsFile,omitzero"` // Because this is only output file in the program, we dont need fileId to deduplicate name
EmitSignatures []*BuildInfoEmitSignature `json:"emitSignatures,omitzero"`
LinkedPackagesSignature string `json:"linkedPackagesSignature,omitzero"`
ResolvedRoot []*BuildInfoResolvedRoot `json:"resolvedRoot,omitzero"`

// NonIncrementalProgram info
Expand Down
1 change: 1 addition & 0 deletions tsc/internal/execute/incremental/buildinfotosnapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func buildInfoToSnapshot(buildInfo *BuildInfo, config *tsoptions.ParsedCommandLi
if buildInfo.LatestChangedDtsFile != "" {
to.snapshot.latestChangedDtsFile = to.toAbsoluteFileName(buildInfo.LatestChangedDtsFile)
}
to.snapshot.linkedPackagesSignature = buildInfo.LinkedPackagesSignature
to.snapshot.hasErrors = core.IfElse(buildInfo.Errors, core.TSTrue, core.TSFalse)
to.snapshot.hasSemanticErrors = buildInfo.SemanticErrors
to.snapshot.checkPending = buildInfo.CheckPending
Expand Down
117 changes: 117 additions & 0 deletions tsc/internal/execute/incremental/programtosnapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ package incremental

import (
"context"
"fmt"
"slices"
"strings"

"github.com/microsoft/TypeScript/tsc/internal/ast"
"github.com/microsoft/TypeScript/tsc/internal/binder"
"github.com/microsoft/TypeScript/tsc/internal/checker"
"github.com/microsoft/TypeScript/tsc/internal/collections"
"github.com/microsoft/TypeScript/tsc/internal/compiler"
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/json"
"github.com/microsoft/TypeScript/tsc/internal/modulespecifiers"
"github.com/microsoft/TypeScript/tsc/internal/outputpaths"
"github.com/microsoft/TypeScript/tsc/internal/packagejson"
"github.com/microsoft/TypeScript/tsc/internal/symlinks"
"github.com/microsoft/TypeScript/tsc/internal/tsoptions"
"github.com/microsoft/TypeScript/tsc/internal/tspath"
)
Expand All @@ -33,6 +41,7 @@ func programToSnapshot(program *compiler.Program, oldProgram *Program, hashWithT
to.computeProgramFileChanges()
to.handleFileDelete()
to.handleGlobalScopeChange()
to.computeLinkedPackagesSignature()
to.handlePendingEmit()
to.handlePendingCheck()
}
Expand Down Expand Up @@ -237,6 +246,9 @@ func (t *toProgramSnapshot) handlePendingEmit() {
} else {
pendingEmitKind = getPendingEmitKindWithOptions(t.snapshot.options, t.oldProgram.snapshot.options)
}
if t.snapshot.linkedPackagesSignature != t.oldProgram.snapshot.linkedPackagesSignature {
pendingEmitKind |= GetFileEmitKind(t.snapshot.options) & FileEmitKindAllDts
}
if pendingEmitKind != FileEmitKindNone {
// Add all files to affectedFilesPendingEmit since emit changed
for _, file := range t.program.GetSourceFiles() {
Expand All @@ -250,6 +262,111 @@ func (t *toProgramSnapshot) handlePendingEmit() {
}
}

func (t *toProgramSnapshot) computeLinkedPackagesSignature() {
if !t.snapshot.options.GetEmitDeclarations() {
return
}
caseSensitivity := t.program.CaseSensitivity()
var baseDirectory string
if buildInfoFileName := outputpaths.GetBuildInfoFileName(t.snapshot.options, caseSensitivity); buildInfoFileName != "" {
baseDirectory = buildInfoFileName.Directory().AsString()
}
toSignaturePath := func(path tspath.PathKey) string {
if baseDirectory == "" {
return path.AsString()
}
return tspath.GetRelativePathFromDirectory(baseDirectory, path.AsString(), caseSensitivity)
}
symlinkCache := t.program.GetSymlinkCache()
var entries []string
symlinkCache.Directories().Range(func(symlinkPath tspath.PathKey, link *symlinks.KnownDirectoryLink) bool {
if link != nil {
entries = append(entries, toSignaturePath(symlinkPath)+" -> "+toSignaturePath(link.RealPath))
}
return true
})
if len(entries) == 0 {
return
}
var seenDirectories, seenPackageDirectories collections.Set[tspath.PathKey]
addPackageDirectory := func(directory tspath.RootedDirectoryPath) {
path := caseSensitivity.PathKey(directory.AsPath())
if !seenPackageDirectories.AddIfAbsent(path) {
return
}
if info := t.program.GetPackageJsonInfo(directory.ResolveFile("package.json")); info != nil {
entries = append(entries, toSignaturePath(path)+" "+getPackageEntryFields(info.GetContents()))
}
}
for _, file := range t.program.GetSourceFiles() {
for _, fileName := range []tspath.RootedFilePath{file.FileName(), t.program.GetSourceOfProjectReferenceIfOutputIncluded(file)} {
if !seenDirectories.AddIfAbsent(caseSensitivity.PathKey(fileName.Directory().AsPath())) {
continue
}
tspath.ForEachAncestorDirectoryPathStoppingAtGlobalCache(
t.program.GetGlobalTypingsCacheLocation(),
fileName.Directory(),
func(realPathDirectory tspath.RootedDirectoryPath) (bool, bool) {
symlinkDirectories, ok := symlinkCache.DirectoriesByRealpath().Load(caseSensitivity.PathKey(realPathDirectory.AsPath()))
if !ok {
return false, false
}
relative, _ := caseSensitivity.RelativeFilePathFromDirectory(realPathDirectory, fileName)
symlinkDirectories.Range(func(symlinkDirectory tspath.RootedDirectoryPath) bool {
forEachPackageDirectoryOfModule(symlinkDirectory.ResolveRelativeFile(relative), addPackageDirectory)
return true
})
return false, false
},
)
}
}
slices.Sort(entries)
t.snapshot.linkedPackagesSignature = t.snapshot.computeHash(strings.Join(entries, "\n"))
}

func forEachPackageDirectoryOfModule(fileName tspath.RootedFilePath, callback func(directory tspath.RootedDirectoryPath)) {
parts := modulespecifiers.GetNodeModulePathParts(fileName)
if parts == nil || parts.IsDirectNodeModulesFile {
return
}
directory := parts.PackageRootDirectory
components := strings.Split(parts.PackageRelativePath.AsString(), "/")
for _, component := range components[:len(components)-1] {
callback(directory)
directory = directory.ResolveDirectory(component)
}
callback(directory)
}

func getPackageEntryFields(contents *packagejson.PackageJson) string {
validValue := func(field packagejson.Expected[string]) *string {
if value, ok := field.GetValue(); ok {
return &value
}
return nil
}
fields, err := json.Marshal(struct {
Type string `json:"type,omitzero"`
Main *string `json:"main,omitzero"`
Types *string `json:"types,omitzero"`
Typings *string `json:"typings,omitzero"`
TypesVersions packagejson.JSONValue `json:"typesVersions,omitzero"`
Exports packagejson.ExportsOrImports `json:"exports,omitzero"`
}{
Type: contents.Type.Value,
Main: validValue(contents.Main),
Types: validValue(contents.Types),
Typings: validValue(contents.Typings),
TypesVersions: contents.TypesVersions,
Exports: contents.Exports,
})
if err != nil {
panic(fmt.Sprintf("Failed to marshal package.json fields: %v", err))
}
return string(fields)
}

func (t *toProgramSnapshot) handlePendingCheck() {
if t.oldProgram != nil &&
t.snapshot.semanticDiagnosticsPerFile.Size() != len(t.program.GetSourceFiles()) &&
Expand Down
5 changes: 3 additions & 2 deletions tsc/internal/execute/incremental/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,9 @@ type snapshot struct {
// If semantic diagnostic check is pending
checkPending bool
// Looked up package.json files from
packageJsons []tspath.RootedFilePath
missingPackageJsons []tspath.RootedFilePath
packageJsons []tspath.RootedFilePath
missingPackageJsons []tspath.RootedFilePath
linkedPackagesSignature string

// Additional fields that are not serialized but needed to track state

Expand Down
1 change: 1 addition & 0 deletions tsc/internal/execute/incremental/snapshottobuildinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func snapshotToBuildInfo(snapshot *snapshot, program *compiler.Program, buildInf
if snapshot.latestChangedDtsFile != "" {
buildInfo.LatestChangedDtsFile = to.fileNameRelativeToBuildInfo(snapshot.latestChangedDtsFile)
}
buildInfo.LinkedPackagesSignature = snapshot.linkedPackagesSignature
} else {
to.setRootOfNonIncrementalProgram()
}
Expand Down
24 changes: 13 additions & 11 deletions tsc/internal/execute/tsctests/readablebuildinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type readableBuildInfo struct {
AffectedFilesPendingEmit []*readableBuildInfoFilePendingEmit `json:"affectedFilesPendingEmit,omitzero"`
LatestChangedDtsFile string `json:"latestChangedDtsFile,omitzero"` // Because this is only output file in the program, we dont need fileId to deduplicate name
EmitSignatures []*readableBuildInfoEmitSignature `json:"emitSignatures,omitzero"`
LinkedPackagesSignature string `json:"linkedPackagesSignature,omitzero"`
ResolvedRoot []*readableBuildInfoResolvedRoot `json:"resolvedRoot,omitzero"`
Size int `json:"size,omitzero"` // Size of the build info file

Expand Down Expand Up @@ -217,17 +218,18 @@ func (b *readableBuildInfoResolvedRoot) UnmarshalJSON(data []byte) error {

func toReadableBuildInfo(buildInfo *incremental.BuildInfo, buildInfoText string) string {
readable := readableBuildInfo{
buildInfo: buildInfo,
Version: buildInfo.Version,
Errors: buildInfo.Errors,
CheckPending: buildInfo.CheckPending,
FileNames: buildInfoPathsAsStrings(buildInfo.FileNames),
Options: buildInfo.Options,
LatestChangedDtsFile: buildInfo.LatestChangedDtsFile.AsString(),
SemanticErrors: buildInfo.SemanticErrors,
PackageJsons: buildInfoPathsAsStrings(buildInfo.PackageJsons),
MissingPackageJsons: buildInfoPathsAsStrings(buildInfo.MissingPackageJsons),
Size: len(buildInfoText),
buildInfo: buildInfo,
Version: buildInfo.Version,
Errors: buildInfo.Errors,
CheckPending: buildInfo.CheckPending,
FileNames: buildInfoPathsAsStrings(buildInfo.FileNames),
Options: buildInfo.Options,
LatestChangedDtsFile: buildInfo.LatestChangedDtsFile.AsString(),
LinkedPackagesSignature: buildInfo.LinkedPackagesSignature,
SemanticErrors: buildInfo.SemanticErrors,
PackageJsons: buildInfoPathsAsStrings(buildInfo.PackageJsons),
MissingPackageJsons: buildInfoPathsAsStrings(buildInfo.MissingPackageJsons),
Size: len(buildInfoText),
}
readable.setFileInfos()
readable.setRoot()
Expand Down
4 changes: 4 additions & 0 deletions tsc/internal/execute/tsctests/sys.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,10 @@ func (s *TestSys) removeNoError(path string) {
}
}

func (s *TestSys) addSymlink(path string, target string) {
s.mapFs().AddSymlink(strings.TrimPrefix(path, "/"), strings.TrimPrefix(target, "/"))
}

func (s *TestSys) readFileNoError(path string) string {
content, ok := s.fsFromFileMap().ReadFile(tspath.ToRootedFilePath(path, s.GetCurrentDirectory()))
if !ok {
Expand Down
114 changes: 114 additions & 0 deletions tsc/internal/execute/tsctests/tsc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2601,11 +2601,125 @@ func TestTscIncremental(t *testing.T) {
},
}

testCases = append(testCases, linkedPackageDeclarationEmitTestInput(
"updates declaration emit when linked dependency is removed and added",
nil,
))
testCases = append(testCases, linkedPackageNestedManifestTestInput(
"updates declaration emit when nested manifest of linked package changes",
nil,
))

for _, test := range testCases {
test.run(t, "incremental")
}
}

func linkedPackageDeclarationEmitFiles() FileMap {
return FileMap{
"/home/src/workspaces/project/a/package.json": `{"name":"a","version":"1.0.0","type":"module","dependencies":{"b":"1.0.0","c":"1.0.0"}}`,
"/home/src/workspaces/project/a/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"incremental": true,
"declaration": true,
"emitDeclarationOnly": true,
"strict": true,
"module": "nodenext",
"types": [],
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}`),
"/home/src/workspaces/project/a/src/index.ts": stringtestutil.Dedent(`
import { currentStage } from "b";
export const stage = currentStage();
`),
"/home/src/workspaces/project/a/node_modules/b": vfstest.Symlink("/home/src/workspaces/project/b"),
"/home/src/workspaces/project/a/node_modules/c": vfstest.Symlink("/home/src/workspaces/project/c"),
"/home/src/workspaces/project/b/package.json": `{"name":"b","version":"1.0.0","type":"module","types":"./index.d.ts","dependencies":{"c":"1.0.0"}}`,
"/home/src/workspaces/project/b/index.d.ts": `export declare const currentStage: () => import("c").Current;`,
"/home/src/workspaces/project/b/node_modules/c": vfstest.Symlink("/home/src/workspaces/project/c"),
"/home/src/workspaces/project/c/package.json": `{"name":"c","version":"1.0.0","type":"module","types":"./index.d.ts"}`,
"/home/src/workspaces/project/c/index.d.ts": stringtestutil.Dedent(`
export interface Current { readonly stage: string }
export declare const current: Current;
`),
}
}

func linkedPackageDeclarationEmitTestInput(subScenario string, commandLineArgs []string) *tscInput {
writeManifestOfA := func(sys *TestSys, dependencies string) {
sys.writeFileNoError("/home/src/workspaces/project/a/package.json", fmt.Sprintf(`{"name":"a","version":"1.0.0","type":"module","dependencies":{%s}}`, dependencies))
}
return &tscInput{
subScenario: subScenario,
files: linkedPackageDeclarationEmitFiles(),
cwd: "/home/src/workspaces/project/a",
commandLineArgs: commandLineArgs,
edits: []*tscEdit{
{
caption: "remove c from dependencies and unlink it",
edit: func(sys *TestSys) {
writeManifestOfA(sys, `"b":"1.0.0"`)
sys.removeNoError("/home/src/workspaces/project/a/node_modules/c")
},
},
noChange,
{
caption: "add c to dependencies and link it",
edit: func(sys *TestSys) {
writeManifestOfA(sys, `"b":"1.0.0","c":"1.0.0"`)
sys.addSymlink("/home/src/workspaces/project/a/node_modules/c", "/home/src/workspaces/project/c")
},
},
noChange,
},
}
}

func linkedPackageNestedManifestTestInput(subScenario string, commandLineArgs []string) *tscInput {
files := linkedPackageDeclarationEmitFiles()
delete(files, "/home/src/workspaces/project/c/index.d.ts")
files["/home/src/workspaces/project/b/index.d.ts"] = `export declare const currentStage: () => import("c/src/index.js").Current;`
files["/home/src/workspaces/project/c/package.json"] = `{"name":"c","version":"1.0.0","type":"module","types":"./src/main.d.ts"}`
files["/home/src/workspaces/project/c/src/package.json"] = `{"type":"module","types":"./index.d.ts"}`
files["/home/src/workspaces/project/c/src/index.d.ts"] = stringtestutil.Dedent(`
export interface Current { readonly stage: string }
export declare const current: Current;
`)
files["/home/src/workspaces/project/c/src/main.d.ts"] = `export declare const version: string;`
return &tscInput{
subScenario: subScenario,
files: files,
cwd: "/home/src/workspaces/project/a",
commandLineArgs: commandLineArgs,
edits: []*tscEdit{
{
caption: "point types of nested manifest of c to another entry",
edit: func(sys *TestSys) {
sys.replaceFileText("/home/src/workspaces/project/c/src/package.json", "./index.d.ts", "./main.d.ts")
},
},
noChange,
{
caption: "remove nested manifest of c",
edit: func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/c/src/package.json")
},
},
{
caption: "add nested manifest of c pointing types to index",
edit: func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/c/src/package.json", `{"type":"module","types":"./index.d.ts"}`)
},
},
noChange,
},
}
}

func TestTscIncrementalDeletion(t *testing.T) {
t.Parallel()
for _, mode := range []struct {
Expand Down
Loading