Skip to content

Allow 'paths' without 'baseUrl' #40101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
7 changes: 7 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2508,6 +2508,13 @@ namespace ts {
parseOwnConfigOfJson(json, host, basePath, configFileName, errors) :
parseOwnConfigOfJsonSourceFile(sourceFile!, host, basePath, configFileName, errors);

if (ownConfig.options?.paths) {
// If we end up needing to resolve relative paths from 'paths' relative to
// the config file location, we'll need to know where that config file was.
// Since 'paths' can be inherited from an extended config in another directory,
// we wouldn't know which directory to use unless we store it here.
ownConfig.options.pathsBasePath = basePath;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this should be non enumerable and should be in tsconfig build info or not. You would also want test case with incremental set.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a baseline that shows it in the .tsbuildinfo. It seems like it would be necessary if an incremental build doesn’t re-resolve everything in tsconfig, but I’m not sure how that works. Exactly what would you want to see in a test case?

}
if (ownConfig.extendedConfigPath) {
// copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios.
resolutionStack = resolutionStack.concat([resolvedPath]);
Expand Down
8 changes: 4 additions & 4 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3493,10 +3493,6 @@
"category": "Error",
"code": 5059
},
"Option 'paths' cannot be used without specifying '--baseUrl' option.": {
"category": "Error",
"code": 5060
},
"Pattern '{0}' can have at most one '*' character.": {
"category": "Error",
"code": 5061
Expand Down Expand Up @@ -3613,6 +3609,10 @@
"category": "Error",
"code": 5089
},
"Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?": {
"category": "Error",
"code": 5090
},

"Generates a sourcemap for each corresponding '.d.ts' file.": {
"category": "Message",
Expand Down
12 changes: 8 additions & 4 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -788,13 +788,16 @@ namespace ts {
}

function tryLoadModuleUsingPathsIfEligible(extensions: Extensions, moduleName: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState) {
const { baseUrl, paths } = state.compilerOptions;
if (baseUrl && paths && !pathIsRelative(moduleName)) {
const { baseUrl, paths, pathsBasePath } = state.compilerOptions;
if (paths && !pathIsRelative(moduleName)) {
if (state.traceEnabled) {
trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName);
if (baseUrl) {
trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName);
}
trace(state.host, Diagnostics.paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, moduleName);
}
return tryLoadModuleUsingPaths(extensions, moduleName, baseUrl, paths, loader, /*onlyRecordFailures*/ false, state);
const baseDirectory = baseUrl ?? Debug.checkDefined(pathsBasePath || state.host.getCurrentDirectory?.(), "Encountered 'paths' without a 'baseUrl', config file, or host 'getCurrentDirectory'.");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks sketchy, but I manually verified that the assertion should hold in all cases we currently have; it’s just hard to encode it into the types. The only times host.getCurrentDirectory is undefined are

  1. The command line parser and typings installer each call the module name resolver in an ad-hoc way under certain circumstances, but they pass a hard-coded compilerOptions that lack paths, so this code path is never taken.
  2. Project calls getAutomaticTypeDirectives with a DirectoryStructureHost, but that function never uses host.getCurrentDirectory().

Changing ModuleResolutionHost['getCurrentDirectory'] to be non-optional makes these call sites a bit of a nuisance.

return tryLoadModuleUsingPaths(extensions, moduleName, baseDirectory, paths, loader, /*onlyRecordFailures*/ false, state);
}
}

Expand Down Expand Up @@ -1368,6 +1371,7 @@ namespace ts {
}
const resolved = forEach(paths[matchedPatternText], subst => {
const path = matchedStar ? subst.replace("*", matchedStar) : subst;
// When baseUrl is not specified, the command line parser resolves relative paths to the config file location.
const candidate = normalizePath(combinePaths(baseDirectory, path));
if (state.traceEnabled) {
trace(state.host, Diagnostics.Trying_substitution_0_candidate_module_location_Colon_1, subst, path);
Expand Down
9 changes: 4 additions & 5 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2994,10 +2994,6 @@ namespace ts {
}
}

if (options.paths && options.baseUrl === undefined) {
createDiagnosticForOptionName(Diagnostics.Option_paths_cannot_be_used_without_specifying_baseUrl_option, "paths");
}

if (options.composite) {
if (options.declaration === false) {
createDiagnosticForOptionName(Diagnostics.Composite_projects_may_not_disable_declaration_emit, "declaration");
Expand Down Expand Up @@ -3056,6 +3052,9 @@ namespace ts {
if (!hasZeroOrOneAsteriskCharacter(subst)) {
createDiagnosticForOptionPathKeyValue(key, i, Diagnostics.Substitution_0_in_pattern_1_can_have_at_most_one_Asterisk_character, subst, key);
}
if (!options.baseUrl && !pathIsRelative(subst) && !pathIsAbsolute(subst)) {
createDiagnosticForOptionPathKeyValue(key, i, Diagnostics.Non_relative_paths_are_not_allowed_when_baseUrl_is_not_set_Did_you_forget_a_leading_Slash);
}
}
else {
createDiagnosticForOptionPathKeyValue(key, i, Diagnostics.Substitution_0_for_pattern_1_has_incorrect_type_expected_string_got_2, subst, key, typeOfSubst);
Expand Down Expand Up @@ -3339,7 +3338,7 @@ namespace ts {
});
}

function createDiagnosticForOptionPathKeyValue(key: string, valueIndex: number, message: DiagnosticMessage, arg0: string | number, arg1: string | number, arg2?: string | number) {
function createDiagnosticForOptionPathKeyValue(key: string, valueIndex: number, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number) {
let needCompilerDiagnostic = true;
const pathsSyntax = getOptionPathsSyntax();
for (const pathProp of pathsSyntax) {
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5787,6 +5787,8 @@ namespace ts {
outDir?: string;
outFile?: string;
paths?: MapLike<string[]>;
/** The directory of the config file that specified 'paths'. Used to resolve relative paths when 'baseUrl' is absent. */
/*@internal*/ pathsBasePath?: string;
/*@internal*/ plugins?: PluginImport[];
preserveConstEnums?: boolean;
preserveSymlinks?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/harness/compilerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ namespace compiler {
if (compilerOptions.skipDefaultLibCheck === undefined) compilerOptions.skipDefaultLibCheck = true;
if (compilerOptions.noErrorTruncation === undefined) compilerOptions.noErrorTruncation = true;

const preProgram = ts.length(rootFiles) < 100 ? ts.createProgram(rootFiles || [], { ...compilerOptions, traceResolution: false }, host) : undefined;
const preProgram = ts.length(rootFiles) < 100 ? ts.createProgram(rootFiles || [], { ...compilerOptions, configFile: compilerOptions.configFile, traceResolution: false }, host) : undefined;
const preErrors = preProgram && ts.getPreEmitDiagnostics(preProgram);

const program = ts.createProgram(rootFiles || [], compilerOptions, host);
Expand Down
16 changes: 16 additions & 0 deletions src/testRunner/unittests/programApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,20 @@ namespace ts {
assert.isEmpty(program.getSemanticDiagnostics());
});
});

describe("unittests:: programApi:: CompilerOptions relative paths", () => {
it("resolves relative paths by getCurrentDirectory", () => {
const main = new documents.TextDocument("/main.ts", "import \"module\";");
const mod = new documents.TextDocument("/lib/module.ts", "declare const foo: any;");

const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [main, mod], cwd: "/" });
const program = createProgram(["./main.ts"], {
paths: { "*": ["./lib/*"] }
}, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed }));

assert.isEmpty(program.getConfigFileParsingDiagnostics());
assert.isEmpty(program.getGlobalDiagnostics());
assert.isEmpty(program.getSemanticDiagnostics());
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
c:/root/tsconfig.json(5,17): error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?
c:/root/tsconfig.json(6,17): error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?


==== c:/root/tsconfig.json (2 errors) ====
{
"compilerOptions": {
"paths": {
"*": [
"*",
~~~
!!! error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?
"generated/*"
~~~~~~~~~~~~~
!!! error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?
]
}
}
}

==== c:/root/f1.ts (0 errors) ====
export var x = 1;

11 changes: 11 additions & 0 deletions tests/baselines/reference/pathMappingBasedModuleResolution1_amd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//// [f1.ts]
export var x = 1;


//// [f1.js]
define(["require", "exports"], function (require, exports) {
"use strict";
exports.__esModule = true;
exports.x = void 0;
exports.x = 1;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
=== c:/root/f1.ts ===
export var x = 1;
>x : Symbol(x, Decl(f1.ts, 0, 10))

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
=== c:/root/f1.ts ===
export var x = 1;
>x : number
>1 : 1

Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
c:/root/tsconfig.json(4,9): error TS5060: Option 'paths' cannot be used without specifying '--baseUrl' option.
c:/root/tsconfig.json(5,17): error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?
c:/root/tsconfig.json(6,17): error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?


==== c:/root/tsconfig.json (1 errors) ====
// paths should error in the absence of baseurl
==== c:/root/tsconfig.json (2 errors) ====
{
"compilerOptions": {
"paths": {
~~~~~~~
!!! error TS5060: Option 'paths' cannot be used without specifying '--baseUrl' option.
"*": [
"*",
~~~
!!! error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?
"generated/*"
~~~~~~~~~~~~~
!!! error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?
]
}
}
Expand Down
24 changes: 24 additions & 0 deletions tests/baselines/reference/pathMappingInheritedBaseUrl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//// [tests/cases/compiler/pathMappingInheritedBaseUrl.ts] ////

//// [tsconfig.base.json]
{
"compilerOptions": {
"baseUrl": "."
}
}

//// [index.ts]
export const p1 = 0;

//// [index.ts]
import { p1 } from "p1";


//// [index.js]
"use strict";
exports.__esModule = true;
exports.p1 = void 0;
exports.p1 = 0;
//// [index.js]
"use strict";
exports.__esModule = true;
17 changes: 17 additions & 0 deletions tests/baselines/reference/pathMappingWithoutBaseUrl1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//// [tests/cases/compiler/pathMappingWithoutBaseUrl1.ts] ////

//// [index.ts]
export const p1 = 0;

//// [index.ts]
import { p1 } from "p1";


//// [index.js]
"use strict";
exports.__esModule = true;
exports.p1 = void 0;
exports.p1 = 0;
//// [index.js]
"use strict";
exports.__esModule = true;
26 changes: 26 additions & 0 deletions tests/baselines/reference/pathMappingWithoutBaseUrl2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//// [tests/cases/compiler/pathMappingWithoutBaseUrl2.ts] ////

//// [tsconfig.base.json]
{
"compilerOptions": {
"paths": {
"p1": ["./lib/p1"]
}
}
}

//// [index.ts]
export const p1 = 0;

//// [index.ts]
import { p1 } from "p1";


//// [index.js]
"use strict";
exports.__esModule = true;
exports.p1 = void 0;
exports.p1 = 0;
//// [index.js]
"use strict";
exports.__esModule = true;
25 changes: 25 additions & 0 deletions tests/baselines/reference/pathsValidation5.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
tests/cases/compiler/tsconfig.json(5,26): error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?
tests/cases/compiler/tsconfig.json(6,19): error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?
tests/cases/compiler/tsconfig.json(7,23): error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?


==== tests/cases/compiler/tsconfig.json (3 errors) ====
{
"compilerOptions": {
"traceResolution": true,
"paths": {
"@interface/*": ["src/interface/*"],
~~~~~~~~~~~~~~~~~
!!! error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?
"@blah": ["blah"],
~~~~~~
!!! error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?
"@humbug/*": ["*/generated"]
~~~~~~~~~~~~~
!!! error TS5090: Non-relative paths are not allowed when 'baseUrl' is not set. Did you forget a leading './'?
}
}
}

==== tests/cases/compiler/src/main.ts (0 errors) ====
import 'someModule';
7 changes: 7 additions & 0 deletions tests/baselines/reference/pathsValidation5.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//// [main.ts]
import 'someModule';

//// [main.js]
"use strict";
exports.__esModule = true;
require("someModule");
21 changes: 21 additions & 0 deletions tests/baselines/reference/pathsValidation5.trace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[
"======== Resolving module 'someModule' from 'tests/cases/compiler/src/main.ts'. ========",
"Module resolution kind is not specified, using 'NodeJs'.",
"'paths' option is specified, looking for a pattern to match module name 'someModule'.",
"Loading module 'someModule' from 'node_modules' folder, target file type 'TypeScript'.",
"Directory 'tests/cases/compiler/src/node_modules' does not exist, skipping all lookups in it.",
"Directory 'tests/cases/compiler/node_modules' does not exist, skipping all lookups in it.",
"Directory 'tests/cases/node_modules' does not exist, skipping all lookups in it.",
"Directory 'tests/node_modules' does not exist, skipping all lookups in it.",
"Directory 'node_modules' does not exist, skipping all lookups in it.",
"Directory '/node_modules' does not exist, skipping all lookups in it.",
"'paths' option is specified, looking for a pattern to match module name 'someModule'.",
"Loading module 'someModule' from 'node_modules' folder, target file type 'JavaScript'.",
"Directory 'tests/cases/compiler/src/node_modules' does not exist, skipping all lookups in it.",
"Directory 'tests/cases/compiler/node_modules' does not exist, skipping all lookups in it.",
"Directory 'tests/cases/node_modules' does not exist, skipping all lookups in it.",
"Directory 'tests/node_modules' does not exist, skipping all lookups in it.",
"Directory 'node_modules' does not exist, skipping all lookups in it.",
"Directory '/node_modules' does not exist, skipping all lookups in it.",
"======== Module name 'someModule' was not resolved. ========"
]
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ a_1.X;
"./*"
]
},
"pathsBasePath": "/src",
"listFiles": true,
"configFilePath": "./tsconfig.b.json"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// @module: amd
// @traceResolution: true

// paths should error in the absence of baseurl

// @filename: c:/root/tsconfig.json
{
"compilerOptions": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// @module: commonjs
// @traceResolution: true

// paths should error in the absence of baseurl
// @filename: c:/root/tsconfig.json
{
"compilerOptions": {
Expand Down
25 changes: 25 additions & 0 deletions tests/cases/compiler/pathMappingInheritedBaseUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// @noTypesAndSymbols: true

// @Filename: /other/tsconfig.base.json
{
"compilerOptions": {
"baseUrl": "."
}
}

// @Filename: /project/tsconfig.json
{
"extends": "../other/tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"paths": {
"p1": ["./lib/p1"]
}
}
}

// @Filename: /other/lib/p1/index.ts
export const p1 = 0;

// @Filename: /project/index.ts
import { p1 } from "p1";
Loading