diff --git a/internal/compiler/fileloader.go b/internal/compiler/fileloader.go index d18761ee29..941cdd6728 100644 --- a/internal/compiler/fileloader.go +++ b/internal/compiler/fileloader.go @@ -356,7 +356,7 @@ func (p *fileLoader) addProjectReferenceTasks(singleThreaded bool) { }) } } else { - for outputDts := range resolved.GetOutputDeclarationFileNames() { + for outputDts := range resolved.GetOutputDeclarationAndSourceFileNames() { if outputDts != "" { p.rootTasks = append(p.rootTasks, &parseTask{ normalizedFilePath: outputDts, diff --git a/internal/execute/tsctests/tsc_test.go b/internal/execute/tsctests/tsc_test.go index e145bf178b..31f0f94911 100644 --- a/internal/execute/tsctests/tsc_test.go +++ b/internal/execute/tsctests/tsc_test.go @@ -2550,6 +2550,62 @@ func TestTscModuleResolution(t *testing.T) { }, }, }, + { + subScenario: "resolution from d.ts of referenced project", + files: FileMap{ + "/home/src/workspaces/project/common.d.ts": "export type OnValue = (value: number) => void", + "/home/src/workspaces/project/producer/index.ts": stringtestutil.Dedent(` + export { ValueProducerDeclaration } from "./in-js" + import { OnValue } from "@common" + export interface ValueProducerFromTs { + onValue: OnValue; + } + `), + "/home/src/workspaces/project/producer/in-js.d.ts": stringtestutil.Dedent(` + import { OnValue } from "@common" + export interface ValueProducerDeclaration { + onValue: OnValue; + } + `), + "/home/src/workspaces/project/producer/tsconfig.json": stringtestutil.Dedent(` + { + "compilerOptions": { + "strict": true, + "composite": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "paths": { + "@common": ["../common.d.ts"], + }, + }, + }`), + "/home/src/workspaces/project/consumer/index.ts": stringtestutil.Dedent(` + import { ValueProducerDeclaration, ValueProducerFromTs } from "@producer" + declare let v: ValueProducerDeclaration; + // n is implicitly any because onValue is actually any (despite what the tooltip says) + v.onValue = (n) => { + } + // n is implicitly number as expected + declare let v2: ValueProducerFromTs; + v2.onValue = (n) => { + }`), + "/home/src/workspaces/project/consumer/tsconfig.json": stringtestutil.Dedent(` + { + "compilerOptions": { + "strict": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "paths": { + "@producer": ["../producer/index"], + }, + }, + "references": [ + { "path": "../producer" }, + ], + }`), + }, + commandLineArgs: []string{"--b", "consumer", "--traceResolution", "-v"}, + }, } for _, test := range testCases { diff --git a/internal/testutil/stringtestutil/stringtestutil.go b/internal/testutil/stringtestutil/stringtestutil.go index 80db676be1..4a1e70f271 100644 --- a/internal/testutil/stringtestutil/stringtestutil.go +++ b/internal/testutil/stringtestutil/stringtestutil.go @@ -13,11 +13,11 @@ func Dedent(text string) string { startLine := -1 lastLine := 0 for i, line := range lines { - firstNonTab := strings.IndexFunc(line, func(r rune) bool { - return r != '\t' + firstNonWhite := strings.IndexFunc(line, func(r rune) bool { + return !stringutil.IsWhiteSpaceLike(r) }) - if firstNonTab > 0 { - line = strings.Repeat(" ", firstNonTab) + line[firstNonTab:] + if firstNonWhite > 0 { + line = strings.ReplaceAll(line[0:firstNonWhite], "\t", " ") + line[firstNonWhite:] lines[i] = line } line = strings.TrimSpace(line) diff --git a/internal/tsoptions/parsedcommandline.go b/internal/tsoptions/parsedcommandline.go index f40fbc2639..198f9cafb4 100644 --- a/internal/tsoptions/parsedcommandline.go +++ b/internal/tsoptions/parsedcommandline.go @@ -87,7 +87,7 @@ func (p *ParsedCommandLine) ParseInputOutputNames() { sourceToOutput := map[tspath.Path]*SourceOutputAndProjectReference{} outputDtsToSource := map[tspath.Path]*SourceOutputAndProjectReference{} - for outputDts, source := range p.GetOutputDeclarationFileNames() { + for outputDts, source := range p.GetOutputDeclarationAndSourceFileNames() { path := tspath.ToPath(source, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) projectReference := &SourceOutputAndProjectReference{ Source: source, @@ -131,14 +131,11 @@ func (p *ParsedCommandLine) UseCaseSensitiveFileNames() bool { return p.comparePathsOptions.UseCaseSensitiveFileNames } -func (p *ParsedCommandLine) GetOutputDeclarationFileNames() iter.Seq2[string, string] { +func (p *ParsedCommandLine) GetOutputDeclarationAndSourceFileNames() iter.Seq2[string, string] { return func(yield func(dtsName string, inputName string) bool) { for _, fileName := range p.ParsedConfig.FileNames { - if tspath.IsDeclarationFileName(fileName) { - continue - } var outputDts string - if !tspath.FileExtensionIs(fileName, tspath.ExtensionJson) { + if !tspath.IsDeclarationFileName(fileName) && !tspath.FileExtensionIs(fileName, tspath.ExtensionJson) { outputDts = outputpaths.GetOutputDeclarationFileNameWorker(fileName, p.CompilerOptions(), p) } if !yield(outputDts, fileName) { diff --git a/testdata/baselines/reference/tsbuild/moduleResolution/resolution-from-d.ts-of-referenced-project.js b/testdata/baselines/reference/tsbuild/moduleResolution/resolution-from-d.ts-of-referenced-project.js new file mode 100644 index 0000000000..b429271e8e --- /dev/null +++ b/testdata/baselines/reference/tsbuild/moduleResolution/resolution-from-d.ts-of-referenced-project.js @@ -0,0 +1,290 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/common.d.ts] *new* +export type OnValue = (value: number) => void +//// [/home/src/workspaces/project/consumer/index.ts] *new* +import { ValueProducerDeclaration, ValueProducerFromTs } from "@producer" +declare let v: ValueProducerDeclaration; +// n is implicitly any because onValue is actually any (despite what the tooltip says) +v.onValue = (n) => { +} +// n is implicitly number as expected +declare let v2: ValueProducerFromTs; +v2.onValue = (n) => { +} +//// [/home/src/workspaces/project/consumer/tsconfig.json] *new* +{ + "compilerOptions": { + "strict": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "paths": { + "@producer": ["../producer/index"], + }, + }, + "references": [ + { "path": "../producer" }, + ], +} +//// [/home/src/workspaces/project/producer/in-js.d.ts] *new* +import { OnValue } from "@common" +export interface ValueProducerDeclaration { + onValue: OnValue; +} +//// [/home/src/workspaces/project/producer/index.ts] *new* +export { ValueProducerDeclaration } from "./in-js" +import { OnValue } from "@common" +export interface ValueProducerFromTs { + onValue: OnValue; +} +//// [/home/src/workspaces/project/producer/tsconfig.json] *new* +{ + "compilerOptions": { + "strict": true, + "composite": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "paths": { + "@common": ["../common.d.ts"], + }, + }, +} + +tsgo --b consumer --traceResolution -v +ExitStatus:: Success +Output:: +[HH:MM:SS AM] Projects in this build: + * producer/tsconfig.json + * consumer/tsconfig.json + +[HH:MM:SS AM] Project 'producer/tsconfig.json' is out of date because output file 'producer/tsconfig.tsbuildinfo' does not exist + +[HH:MM:SS AM] Building project 'producer/tsconfig.json'... + +======== Resolving module '@common' from '/home/src/workspaces/project/producer/in-js.d.ts'. ======== +Explicitly specified module resolution kind: 'NodeNext'. +Resolving in CJS mode with conditions 'require', 'types', 'node'. +'paths' option is specified, looking for a pattern to match module name '@common'. +Module name '@common', matched pattern '@common'. +Trying substitution '../common.d.ts', candidate module location: '../common.d.ts'. +File '/home/src/workspaces/project/common.d.ts' exists - use it as a name resolution result. +======== Module name '@common' was successfully resolved to '/home/src/workspaces/project/common.d.ts'. ======== +======== Resolving module './in-js' from '/home/src/workspaces/project/producer/index.ts'. ======== +Explicitly specified module resolution kind: 'NodeNext'. +Resolving in CJS mode with conditions 'require', 'types', 'node'. +Loading module as file / folder, candidate module location '/home/src/workspaces/project/producer/in-js', target file types: TypeScript, JavaScript, Declaration. +File '/home/src/workspaces/project/producer/in-js.ts' does not exist. +File '/home/src/workspaces/project/producer/in-js.tsx' does not exist. +File '/home/src/workspaces/project/producer/in-js.d.ts' exists - use it as a name resolution result. +======== Module name './in-js' was successfully resolved to '/home/src/workspaces/project/producer/in-js.d.ts'. ======== +======== Resolving module '@common' from '/home/src/workspaces/project/producer/index.ts'. ======== +Explicitly specified module resolution kind: 'NodeNext'. +Resolving in CJS mode with conditions 'require', 'types', 'node'. +'paths' option is specified, looking for a pattern to match module name '@common'. +Module name '@common', matched pattern '@common'. +Trying substitution '../common.d.ts', candidate module location: '../common.d.ts'. +File '/home/src/workspaces/project/common.d.ts' exists - use it as a name resolution result. +======== Module name '@common' was successfully resolved to '/home/src/workspaces/project/common.d.ts'. ======== +[HH:MM:SS AM] Project 'consumer/tsconfig.json' is out of date because output file 'consumer/tsconfig.tsbuildinfo' does not exist + +[HH:MM:SS AM] Building project 'consumer/tsconfig.json'... + +======== Resolving module '@producer' from '/home/src/workspaces/project/consumer/index.ts'. ======== +Explicitly specified module resolution kind: 'NodeNext'. +Resolving in CJS mode with conditions 'require', 'types', 'node'. +'paths' option is specified, looking for a pattern to match module name '@producer'. +Module name '@producer', matched pattern '@producer'. +Trying substitution '../producer/index', candidate module location: '../producer/index'. +Loading module as file / folder, candidate module location '/home/src/workspaces/project/producer/index', target file types: TypeScript, JavaScript, Declaration. +File '/home/src/workspaces/project/producer/index.ts' exists - use it as a name resolution result. +======== Module name '@producer' was successfully resolved to '/home/src/workspaces/project/producer/index.ts'. ======== +======== Resolving module './in-js' from '/home/src/workspaces/project/producer/index.ts'. ======== +Using compiler options of project reference redirect '/home/src/workspaces/project/producer/tsconfig.json'. +Explicitly specified module resolution kind: 'NodeNext'. +Resolving in CJS mode with conditions 'require', 'types', 'node'. +Loading module as file / folder, candidate module location '/home/src/workspaces/project/producer/in-js', target file types: TypeScript, JavaScript, Declaration. +File '/home/src/workspaces/project/producer/in-js.ts' does not exist. +File '/home/src/workspaces/project/producer/in-js.tsx' does not exist. +File '/home/src/workspaces/project/producer/in-js.d.ts' exists - use it as a name resolution result. +======== Module name './in-js' was successfully resolved to '/home/src/workspaces/project/producer/in-js.d.ts'. ======== +======== Resolving module '@common' from '/home/src/workspaces/project/producer/index.ts'. ======== +Using compiler options of project reference redirect '/home/src/workspaces/project/producer/tsconfig.json'. +Explicitly specified module resolution kind: 'NodeNext'. +Resolving in CJS mode with conditions 'require', 'types', 'node'. +'paths' option is specified, looking for a pattern to match module name '@common'. +Module name '@common', matched pattern '@common'. +Trying substitution '../common.d.ts', candidate module location: '../common.d.ts'. +File '/home/src/workspaces/project/common.d.ts' exists - use it as a name resolution result. +======== Module name '@common' was successfully resolved to '/home/src/workspaces/project/common.d.ts'. ======== +======== Resolving module '@common' from '/home/src/workspaces/project/producer/in-js.d.ts'. ======== +Using compiler options of project reference redirect '/home/src/workspaces/project/producer/tsconfig.json'. +Explicitly specified module resolution kind: 'NodeNext'. +Resolving in CJS mode with conditions 'require', 'types', 'node'. +'paths' option is specified, looking for a pattern to match module name '@common'. +Module name '@common', matched pattern '@common'. +Trying substitution '../common.d.ts', candidate module location: '../common.d.ts'. +File '/home/src/workspaces/project/common.d.ts' exists - use it as a name resolution result. +======== Module name '@common' was successfully resolved to '/home/src/workspaces/project/common.d.ts'. ======== +//// [/home/src/tslibs/TS/Lib/lib.esnext.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/consumer/index.js] *new* +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +// n is implicitly any because onValue is actually any (despite what the tooltip says) +v.onValue = (n) => { +}; +v2.onValue = (n) => { +}; + +//// [/home/src/workspaces/project/consumer/tsconfig.tsbuildinfo] *new* +{"version":"FakeTSVersion","root":["./index.ts"]} +//// [/home/src/workspaces/project/consumer/tsconfig.tsbuildinfo.readable.baseline.txt] *new* +{ + "version": "FakeTSVersion", + "root": [ + { + "files": [ + "./index.ts" + ], + "original": "./index.ts" + } + ], + "size": 49 +} +//// [/home/src/workspaces/project/producer/index.d.ts] *new* +export { ValueProducerDeclaration } from "./in-js"; +import { OnValue } from "@common"; +export interface ValueProducerFromTs { + onValue: OnValue; +} + +//// [/home/src/workspaces/project/producer/index.js] *new* +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); + +//// [/home/src/workspaces/project/producer/tsconfig.tsbuildinfo] *new* +{"version":"FakeTSVersion","root":[[3,4]],"fileNames":["lib.esnext.full.d.ts","../common.d.ts","./in-js.d.ts","./index.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},"3a9dc77bc99684cad72839382a1f46cb-export type OnValue = (value: number) => void","7677191ddc32f3fe5aa447395ff0553a-import { OnValue } from \"@common\"\nexport interface ValueProducerDeclaration {\n onValue: OnValue;\n}",{"version":"3bb4cb1dd0764fa7c558b11ba246518a-export { ValueProducerDeclaration } from \"./in-js\"\nimport { OnValue } from \"@common\"\nexport interface ValueProducerFromTs {\n onValue: OnValue;\n}","signature":"846f65cd8d68b44b74d0de4a41b17245-export { ValueProducerDeclaration } from \"./in-js\";\nimport { OnValue } from \"@common\";\nexport interface ValueProducerFromTs {\n onValue: OnValue;\n}\n","impliedNodeFormat":1}],"fileIdsList":[[2],[2,3]],"options":{"composite":true,"module":199,"strict":true},"referencedMap":[[3,1],[4,2]],"latestChangedDtsFile":"./index.d.ts"} +//// [/home/src/workspaces/project/producer/tsconfig.tsbuildinfo.readable.baseline.txt] *new* +{ + "version": "FakeTSVersion", + "root": [ + { + "files": [ + "./in-js.d.ts", + "./index.ts" + ], + "original": [ + 3, + 4 + ] + } + ], + "fileNames": [ + "lib.esnext.full.d.ts", + "../common.d.ts", + "./in-js.d.ts", + "./index.ts" + ], + "fileInfos": [ + { + "fileName": "lib.esnext.full.d.ts", + "version": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", + "signature": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", + "affectsGlobalScope": true, + "impliedNodeFormat": "CommonJS", + "original": { + "version": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", + "affectsGlobalScope": true, + "impliedNodeFormat": 1 + } + }, + { + "fileName": "../common.d.ts", + "version": "3a9dc77bc99684cad72839382a1f46cb-export type OnValue = (value: number) => void", + "signature": "3a9dc77bc99684cad72839382a1f46cb-export type OnValue = (value: number) => void", + "impliedNodeFormat": "CommonJS" + }, + { + "fileName": "./in-js.d.ts", + "version": "7677191ddc32f3fe5aa447395ff0553a-import { OnValue } from \"@common\"\nexport interface ValueProducerDeclaration {\n onValue: OnValue;\n}", + "signature": "7677191ddc32f3fe5aa447395ff0553a-import { OnValue } from \"@common\"\nexport interface ValueProducerDeclaration {\n onValue: OnValue;\n}", + "impliedNodeFormat": "CommonJS" + }, + { + "fileName": "./index.ts", + "version": "3bb4cb1dd0764fa7c558b11ba246518a-export { ValueProducerDeclaration } from \"./in-js\"\nimport { OnValue } from \"@common\"\nexport interface ValueProducerFromTs {\n onValue: OnValue;\n}", + "signature": "846f65cd8d68b44b74d0de4a41b17245-export { ValueProducerDeclaration } from \"./in-js\";\nimport { OnValue } from \"@common\";\nexport interface ValueProducerFromTs {\n onValue: OnValue;\n}\n", + "impliedNodeFormat": "CommonJS", + "original": { + "version": "3bb4cb1dd0764fa7c558b11ba246518a-export { ValueProducerDeclaration } from \"./in-js\"\nimport { OnValue } from \"@common\"\nexport interface ValueProducerFromTs {\n onValue: OnValue;\n}", + "signature": "846f65cd8d68b44b74d0de4a41b17245-export { ValueProducerDeclaration } from \"./in-js\";\nimport { OnValue } from \"@common\";\nexport interface ValueProducerFromTs {\n onValue: OnValue;\n}\n", + "impliedNodeFormat": 1 + } + } + ], + "fileIdsList": [ + [ + "../common.d.ts" + ], + [ + "../common.d.ts", + "./in-js.d.ts" + ] + ], + "options": { + "composite": true, + "module": 199, + "strict": true + }, + "referencedMap": { + "./in-js.d.ts": [ + "../common.d.ts" + ], + "./index.ts": [ + "../common.d.ts", + "./in-js.d.ts" + ] + }, + "latestChangedDtsFile": "./index.d.ts", + "size": 1713 +} + +producer/tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.esnext.full.d.ts +*refresh* /home/src/workspaces/project/common.d.ts +*refresh* /home/src/workspaces/project/producer/in-js.d.ts +*refresh* /home/src/workspaces/project/producer/index.ts +Signatures:: +(stored at emit) /home/src/workspaces/project/producer/index.ts + +consumer/tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.esnext.full.d.ts +*refresh* /home/src/workspaces/project/common.d.ts +*refresh* /home/src/workspaces/project/producer/in-js.d.ts +*refresh* /home/src/workspaces/project/producer/index.d.ts +*refresh* /home/src/workspaces/project/consumer/index.ts +Signatures::