Skip to content

Proposal: Allow paths compilerOption without baseUrl #31869

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

Closed
5 tasks done
mheiber opened this issue Jun 12, 2019 · 14 comments · Fixed by #40101
Closed
5 tasks done

Proposal: Allow paths compilerOption without baseUrl #31869

mheiber opened this issue Jun 12, 2019 · 14 comments · Fixed by #40101
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@mheiber
Copy link
Contributor

mheiber commented Jun 12, 2019

Related issue: #28321

Search Terms

baseUrl, paths, compilerOptions

Suggestion

Do not require 'baseUrl' in compilerOptions when 'paths' is provided.

Use Cases

We want to use 'paths' for type-checking, but don't want to resolve non-module relative names:

./1.ts

import { two } from "2"

./2.ts

export const two = 3;

Examples

tsconfig

{
  "compilerOptions": {
    "paths": {
        "foo": ["../foo"]
    }
  }
}

Actual behavior of paths without baseUrl:

Error: Option 'paths' cannot be used without specifying '--baseUrl' option

Expected behavior of paths without baseUrl:

  • No error message
  • paths are resolved relative to the project directory (which I think means the same thing as "where the tsconfig is")

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@robpalme
Copy link

We have a horrifying workaround that intentionally prevents resolving bare-specifiers against the "baseUrl" whilst permitting "paths" to be used.

It involves setting an impossible directory as "baseUrl" and then prepending "paths" values with "../" to jump out of the impossible directory back up to the project root.

{
  "compilerOptions": {
    "baseUrl": "\u0000",
    "paths": {
        "foo": ["../../foo"]
    }
  }
}

... which works today and achieves the same thing as the proposed example ...

{
  "compilerOptions": {
    "paths": {
        "foo": ["../foo"]
    }
  }
}

It would be great to be able to drop this workaround.

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Jun 25, 2019
@RyanCavanaugh
Copy link
Member

I'm pretty confused about what's going on here (since there is no file named "foo"). Can you provide some more examples of what you'd want this to do? What is the proposed difference between what you want and just setting baseUrl to the "project root" yourself?

@mheiber
Copy link
Contributor Author

mheiber commented Jun 25, 2019

Thanks for taking a look @RyanCavanaugh

I'm pretty confused about what's going on here (since there is no file named "foo")

The presence of a "foo" file is not intended to be relevant to this example. We mentioned it to show that specifying compilerOptions.paths forces also specifying baseUrl.

What is the proposed difference between what you want and just setting baseUrl to the "project root" yourself

When we set baseUrl, the semantics of TS non-relative imports changes in a way that conflicts with ECMAScript semantics. We're hoping to avoid this behavior. There's a complete example below.

Desired behavior:

tsconfig:

{
  "compilerOptions": {
    "paths": {
        "foo": ["../foo"]
    }
  }
}

file systen:

  • foo.ts
    • proj
      • tsconfig.json
      • 1.ts
      • 2.ts

1.ts should error:

import two from "2"; // Error: Cannot find module '2'

2.ts contents (not relevant):

export default 2;

Actual behavior when setting paths without baseUrl

all files are as described above ^^

TS Errors:

Error: Options 'paths' cannot be specified without specifying '--baseUrl' option

Actual behavior when setting baseUrl to project root manually

tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
        "foo": ["../foo"]
    }
  }
}

all other files are as described above ^^

Adding baseUrl changes the semantics of imports. Spefically, there is no longer an error in 1.ts after this change:

import two from "2"; // no error, but we want 'Cannot find module '2', which is the behavior when 'baseUrl' is not specified

TLDR: We're hoping to be able to use 'paths' without changing how all non-relative module paths are resolved

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript and removed Needs More Info The issue still hasn't been fully clarified labels Jun 25, 2019
@russelldavis
Copy link
Contributor

To give a more concrete example of why fixing this is important: setting baseUrl means that every new file you add to the baseUrl location (usually ., the project root) is an opportunity to accidentally break things.

For example: your code might be doing import * as ts from 'typescript', successfully importing from node_modules/typescript. You later create a file called typescript.ts at the project root for some testing. Whoops, your existing imports of typescript are now broken.

It's ideal if the only things that can override node_modules are explicit and minimal; using paths without baseUrl is the way to do that.

@russelldavis
Copy link
Contributor

In the meanwhile, I discovered another workaround:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
        "foo": ["../foo"],
        "*": ["__INVALID__/*"]
    }
  }
}

The other workaround caused some problems for me with Webstorm (and I think other tools), which didn't like having a baseUrl pointing to a nonexistent directory. (But it inspired this one, so thanks @robpalme.)

@andrewbranch
Copy link
Member

There’s one ambiguity that needs to be cleared up in order to allow dropping baseUrl. Consider two paths entries:

"foo1": ["./lib/foo"],
"foo2": ["lib/foo"]

The former value is a relative path, while the latter is an unrooted path. Currently, both of these are resolved to the same location, relative to baseUrl.

In the absence of a baseUrl, I think it’s fairly clear that the value for the foo1 mapping should be resolved relative to the tsconfig.json location, as all other relative paths in tsconfig.json are. It’s less clear what should happen with the value for the foo2 mapping. Without an explicit base, it looks like we could do a node_modules search for lib. This could be a useful feature, but is fairly asymmetrical from how resolution works with baseUrl.

@mheiber
Copy link
Contributor Author

mheiber commented Aug 7, 2020

There’s one ambiguity that needs to be cleared up in order to allow dropping baseUrl. Consider two paths entries:

"foo1": ["./lib/foo"],
"foo2": ["lib/foo"]

The former value is a relative path, while the latter is an unrooted path. Currently, both of these are resolved to the same location, relative to baseUrl.

In the absence of a baseUrl, I think it’s fairly clear that the value for the foo1 mapping should be resolved relative to the tsconfig.json location, as all other relative paths in tsconfig.json are. It’s less clear what should happen with the value for the foo2 mapping. Without an explicit base, it looks like we could do a node_modules search for lib. This could be a useful feature, but is fairly asymmetrical from how resolution works with baseUrl.

Thanks for looking into this. Re the foo2 example, another option is to error in such a case, so the user can fix the paths. (my two cents is) I think it's helpful to avoid new Node-specific things in a world with multiple JS runtimes.

@andrewbranch
Copy link
Member

I think it's helpful to avoid new Node-specific things in a world with multiple JS runtimes.

Good point; the framing of my question was node-specific, but I think my question still stands without making any node-specific assumptions. I shouldn’t have implied that if we see an unrooted path ("lib/foo") we would definitely do a node_modules search, but rather that this module specifier could be passed along to whatever module resolution strategy is selected, which currently would usually do a node_modules search. So I think my question is better stated: given the path map from my previous message and no baseUrl, when resolving the specifier "foo2", should the module resolver be asked to resolve "lib/foo" or "/path/to/tsconfig/directory/lib/foo"?

@mheiber
Copy link
Contributor Author

mheiber commented Aug 10, 2020

I think it's helpful to avoid new Node-specific things in a world with multiple JS runtimes.

Good point; the framing of my question was node-specific, but I think my question still stands without making any node-specific assumptions. I shouldn’t have implied that if we see an unrooted path ("lib/foo") we would definitely do a node_modules search, but rather that this module specifier could be passed along to whatever module resolution strategy is selected, which currently would usually do a node_modules search. So I think my question is better stated: given the path map from my previous message and no baseUrl, when resolving the specifier "foo2", should the module resolver be asked to resolve "lib/foo" or "/path/to/tsconfig/directory/lib/foo"?

Thanks for explaining, I understand better now. Fwiw I have no intuition about which behavior would be more intuitive in that case. I do like the idea of erroring, so the user can more explicitly express intent, but of course I lack the background to know what accords best with the design principles for compiler options.

@robpalme
Copy link

I do not think it is useful for bare-specifiers in "paths" values to ever be treated as tsconfig-relative. If the user wants that, they should use a relative specifier.

If they were instead passed to the resolver directly, it then raises the question of whether that resolution would include further path resolution.

{
  "paths": {
    "first": ["second"],
    "second": ["./foo"],
  }
}

Path resolution is a demon-haunted world of complexity. I would strongly err towards simplicity rather than completeness, and because I don't have a use-case for bare-specifiers here, I'd suggest erroring for now (as Max said). If someone wants the feature, it can always be added later.

@andrewbranch
Copy link
Member

Doing more than one path substitution would make circularities possible, and I can’t think of a real use for it. I think that, at least, we would not do.

@jakebailey
Copy link
Member

jakebailey commented Sep 1, 2020

I know this is being worked on, but one consideration I wanted to mention is how auto-imports are suggested.

I converted a big project into a monorepo that uses these path specifiers, and only later noticed (when I was actually writing code) that VS Code was suggesting imports like src/foo/bar because I had to specify baseUrl in any package that wanted to have paths. I can work around the unwanted suggestions by forcing the editor to use relative suggestions, but RelativePreference.Relative (what VS Code maps to here) completely ignores paths and offers imports that look like ../../../packages/something/src/some/file, which defeats the niceness of being able to use paths.

Am I correct in assuming that baseUrl not being required would then prevent those unwanted "absolute-relative-to-baseUrl" imports from being suggested in favor of relative, while still letting imports listed explicitly paths be suggested?

Or do I need to request a mode that's "relative, but respect paths" to cover this scenario?

@andrewbranch
Copy link
Member

Am I correct in assuming that baseUrl not being required would then prevent those unwanted "absolute-relative-to-baseUrl" imports from being suggested in favor of relative, while still letting imports listed explicitly paths be suggested?

Yep!

@jakebailey
Copy link
Member

Wonderful. I hope this can make it to 4.1. If only this were in 4.0 so we could use it now... 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants