Path mappings based module resolution
vladima opened this issue · 133 comments
Proposed module resolution strategy
UPDATE: proposal below is updated based on the results of the design meeting.
Initial version can be found here.
Primary differences:
- instead of having
baseUrl
as a separate module resolution strategy we are introducing a set of properties
that will allow to customize resoluton process in existing resolution strategies but base strategy still is used as a fallback. rootDirs
are decoupled from thebaseUrl
and can be used without it.
Currently TypeScript supports two ways of resolving module names: classic
(module name always resolves to a file, module are searched using a folder walk)
and node
(uses rules similar to node module loader, was introduced in TypeScript 1.6).
These approaches worked reasonably well but they were not able to model baseUrl based mechanics used by
RequireJS or SystemJS.
We could introduce third type of module resolution that will fill this gap but this will mean that once user has started to use this new type then support to
discover typings embedded in node modules (and distributed via npm
) is lost. Effectively user that wanted both to use baseUrl
to refer to modules defined inside the project
and rely on npm
to obtain modules with typings will have to choose what part of the system will be broken.
Instead of doing this we'll allow to declare a set of properties that will augment existing module resolution strategies. These properties are:
baseUrl
, paths
and rootDirs
(paths
can only be used if baseUrl
is set). If at least one of these properties is defined then compiler will try to
use it to resolve module name and if it fail - will fallback to a default behavior for a current resolution strategy.
Also choice of resolution strategy determines what does it mean to load a module from a given path. To be more concrete given some module name /a/b/c
:
classic
resolver will check for the presense of files/a/b/c.ts
,/a/b/c.tsx
and/a/b/c.d.ts
.node
resolver will first try to load module as file by probing the same files asclassic
and then try to load module from directory
(check/a/b/c/index
with supported extension, then peek intopackage.json
etc. More details can be found in this issue)
Properties
BaseUrl
All non-rooted paths are computed relative to baseUrl.
Value of baseUrl is determined as either:
- value of baseUrl command line argument (if given path is relative it is computed based on current directory)
- value of baseUrl propery in 'tsconfig.json' (if given path is relative it is computed based on then location of 'tsconfig.json')
Path mappings
Sometimes modules are not directly located under baseUrl. It is possible to control how locations are computed in such cases
using path mappings. Path mappings are specified using the following JSON structure:
{
"paths": {
"pattern-1": ["list of substitutions"],
"pattern-2": ["list of substitutions"],
...
"pattern-N": ["list of substitutions"]
}
}
Patterns and substitutions are strings that can have zero or one asteriks ('*').
Interpretation of both patterns and substitutions will be described in Resolution process section.
Resolution process
Non-relative module names are resolved slightly differently comparing
to relative (start with "./" or "../") and rooted module names (start with "/", drive name or schema).
Resolution of non-relative module names (mostly matches SystemJS)
// mimics path mappings in SystemJS
// NOTE: moduleExists checks if file with any supported extension exists on disk
function resolveNonRelativeModuleName(moduleName: string): string {
// check if module name should be used as-is or it should be mapped to different value
let longestMatchedPrefixLength = 0;
let matchedPattern: string;
let matchedWildcard: string;
for (let pattern in config.paths) {
assert(pattern.countOf('*') <= 1);
let indexOfWildcard = pattern.indexOf('*');
if (indexOfWildcard !== -1) {
// if pattern contains asterisk then asterisk acts as a capture group with a greedy matching
// i.e. for the string 'abbb' pattern 'a*b' will get 'bb' as '*'
// check if module name starts with prefix, ends with suffix and these two don't overlap
let prefix = pattern.substr(0, indexOfWildcard);
let suffix = pattern.substr(indexOfWildcard + 1);
if (moduleName.length >= prefix.length + suffix.length &&
moduleName.startsWith(prefix) &&
moduleName.endsWith(suffix)) {
// use length of matched prefix as betterness criteria
if (longestMatchedPrefixLength < prefix.length) {
// save length of the prefix
longestMatchedPrefixLength = prefix.length;
// save matched pattern
matchedPattern = pattern;
// save matched wildcard content
matchedWildcard = moduleName.substr(prefix.length, moduleName.length - suffix.length);
}
}
}
else {
// pattern does not contain asterisk - module name should exactly match pattern to succeed
if (pattern === moduleName) {
// save pattern
matchedPattern = pattern;
// drop saved wildcard match
matchedWildcard = undefined;
// exact match is found - can exit early
break;
}
}
}
if (!matchedPattern) {
// no pattern was matched so module name can be used as-is
let path = combine(baseUrl, moduleName);
return moduleExists(path) ? path : undefined;
}
// some pattern was matched - module name needs to be substituted
let substitutions = config.paths[matchedPattern].asArray();
for (let subst of substitutions) {
assert(substs.countOf('*') <= 1);
// replace * in substitution with matched wildcard
let path = matchedWildcard ? subst.replace("*", matchedWildcard) : subst;
// if substituion is a relative path - combine it with baseUrl
path = isRelative(path) ? combine(baseUrl, path) : path;
if (moduleExists(path)) {
return path;
}
}
return undefined;
}
Resolution of relative module names
Default resolution logic (matches SystemJS)
Relative module names are computed treating location of source file that contains the import as base folder.
Path mappings are not applied.
function resolveRelativeModuleName(moduleName: string, containingFile: string): string {
let path = combine(getDirectoryName(containingFile), moduleName);
return moduleExists(path) ? path : undefined;
}
Using rootDirs
'rootDirs' allows the project to be spreaded across multiple locations and resolve modules with relative names as if multiple project roots were merged together in one folder. For example project contains source files that are located in different directories on then file system (not under the same root) but user still still prefers to use relative module names because in runtime such names can be successfully resolved due to bundling.
For example consider this project structure:
shared
└── projects
└── project
└── src
├── viewManager.ts (imports './views/view1')
└── views
└── view2.ts (imports './view1')
userFiles
└── project
└── src
└── views
└── view1.ts (imports './view2')
Logically files in userFiles/project
and shared/projects/project
belong to the same project and
after build they indeed will be bundled together.
In order to support this we'll add configuration property "rootDirs":
{
"rootDirs": [
"rootDir-1/",
"rootDir-2/",
...
"rootDir-n/"
]
}
This property stores list of base folders, every folder name can be either absolute or relative.
Elements in rootDirs
that represent non-absolute paths will be converted to absolute using location of tsconfig.json as a base folder - this is the common approach for all paths defined in tsconfig.json
///Algorithm for resolving relative module name
function resolveRelativeModuleName(moduleName: string, containingFile: string): string {
// convert relative module name to absolute using location of containing file
// this step is exactly the same as when doing resolution without path mapping
let path = combine(getDirectoryName(containingFile), moduleName);
// convert absolute module name to non-relative
// try to find element in 'rootDirs' that is the longest prefix for "path' and return path.substr(prefix.length) as non-relative name
let { matchingRootDir, nonRelativeName } = tryFindLongestPrefixAndReturnSuffix(rootDirs, path);
if (!matchingRootDir) {
// cannot extract non relative name
return undefined;
}
// first try to load module from initial location
if (moduleExists(path)) {
return path;
}
// then try other entries in rootDirs
for (const rootDir of rootDirs) {
if (rootDir === matchingRootDir) {
continue;
}
const candidate = combine(rootDir, nonRelativeName);
if (moduleExists(candidate)) {
return candidate;
}
}
// failure case
return undefined;
}
Configuration for the example above:
{
"rootDirs": [
"userFiles/project/",
"/shared/projects/project/"
]
}
Example 1
projectRoot
├── folder1
│ └── file1.ts (imports 'folder2/file2')
├── folder2
│ ├── file2.ts (imports './file3')
│ └── file3.ts
└── tsconfig.json
// configuration in tsconfig.json
{
"baseUrl": "."
}
- import 'folder2/file2'
- baseUrl is specified in configuration and has value '.' -> baseUrl is computed relative to
the location of tsconfig.json ->projectRoot
- path mappings are not available -> path = moduleName
- resolved module file name = combine(baseUrl, path) ->
projectRoot/folder2/file2.ts
- baseUrl is specified in configuration and has value '.' -> baseUrl is computed relative to
- import './file3'
- moduleName is relative and rootDirs are not specified in configuration - compute module name
relative to the location of containing file: resolved module file name =projectRoot/folder2/file3.ts
- moduleName is relative and rootDirs are not specified in configuration - compute module name
Example 2
projectRoot
├── folder1
│ ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│ └── file2.ts
├── generated
│ ├── folder1
│ └── folder2
│ └── file3.ts
└── tsconfig.json
// configuration in tsconfig.json
{
"baseUrl": ".",
"paths": {
"*": [
"*",
"generated/*"
]
}
}
- import 'folder1/file2'
- baseUrl is specified in configuration and has value '.' -> baseUrl is computed relative to
the location of tsconfig.json ->projectRoot
- configuration contains path mappings.
- pattern '*' is matched and wildcard captures the whole module name
- try first substitution in the list: '*' ->
folder1/file2
- result of substitution is relative name - combine it with baseUrl ->
projectRoot/folder1/file2.ts
.
This file exists.
- baseUrl is specified in configuration and has value '.' -> baseUrl is computed relative to
- import 'folder2/file2'
- baseUrl is specified in configuration and has value '.' -> baseUrl is computed relative to
the location of tsconfig.json and will be folder that contains tsconfig.json - configuration contains path mappings.
- pattern '*' is matched and wildcard captures the whole module name
- try first substitution in the list: '*' ->
folder2/file3
- result of substitution is relative name - combine it with baseUrl ->
projectRoot/folder2/file3.ts
.
File does not exists, move to the second substitution - second substitution 'generated/*' ->
generated/folder2/file3
- result of substitution is relative name - combine it with baseUrl ->
projectRoot/generated/folder2/file3.ts
.
File exists
- baseUrl is specified in configuration and has value '.' -> baseUrl is computed relative to
Example 3
rootDir
├── folder1
│ └── file1.ts (imports './file2')
├── generated
│ ├── folder1
│ │ ├── file2.ts
│ │ └── file3.ts (imports '../folder1/file1')
│ └── folder2
└── tsconfig.json
// configuration in tsconfig.json
{
"rootDirs": [
"./",
"./generated/"
],
}
All non-rooted entries in rootDirs
are expanded using location of tsconfig.json as base location so after expansion rootDirs
will
look like this:
"rootDirs": [
"rootDir/",
"rootDir/generated/"
],
- import './file2'
- name is relative, first make it absolute using location of containing file as base location -
rootDir/folder1/file2
- for this string find the longest prefix in
rootDirs
-rootDir/
and for this prefix compute as suffix -folder1/file2
- since matching entry in
rootDirs
was found try to resolve module usingrootDir
- first check ifrootDir/folder1/file2
can be resolved as module - such module does not exist - try remaining entries in
rootDirs
- check if modulerootDir/generated/folder1/file2
exists - yes.
- name is relative, first make it absolute using location of containing file as base location -
- import '../folder1/file1'
- name is relative, first make it absolute using location of containing file as base location -
rootDir/generated/folder1/file1
- for this string find the longest prefix in
rootDirs
-rootDir/generated
and for this prefix compute as suffix -folder1/file1
- since matching entry in
rootDirs
was found try to resolve module usingrootDir
- first check ifrootDir/generated/folder1/file1
can be resolved as module - such module does not exist - try remaining entries in
rootDirs
- check if modulerootDir/folder1/file1
exists - yes.
- name is relative, first make it absolute using location of containing file as base location -
The proposal seems sound 👍
I think the references to data should not be considered for now: their added value is marginal but the added configuration complexity would not be (eg: either refs are mandatory or there would have to be a good way to distinguish them from directory names). They seem like a nice improvement to consider after this is implemented and usage data about the mappings becomes available.
The biggest concern with this is that it sounds like a lot of complexity. I can understand the desire to align to SystemJS in hopes that it aligns to whatwg and eventually the loaders that are implemented in the runtime environments. Maybe it is the me being "myopic" but having lived with AMD for nearly a decade now, there are few use cases where my loader configuration is overly complex. For the most part "it just works". Up to this point with TypeScript, largely as well "it just worked", now I fear that I will have to spend a day trying to get TypeScript to understand where my modules are.
To be more specific, relative module resolution... Is there not a 95% happy path for relative module resolution? The AMD spec specifies:
- Relative identifiers are resolved relative to the identifier of the module in which "require" is written and called.
That is about as straight forward of a resolution logic as you can get.
And when that use case does not work, there is baseUrl, path, packages and the sledge hammer map. Each of those are simple and straight forward, without a significant amount of options. All the tools are there and the thing is 95% of the time, there is minimal configuration to get going, often with a single built layer for production, no configuration.
I am likely tilting at windmills, but sometimes it does seem like we are creating 32 varieties of Colgate here...
we have a request for this specific scenario, but I do agree that in majority of cases it is not necessary. That is why it should be opt-in thing: if rootDirs
are not specified then all relative module names are resolved using location of containing module which is the behavior most of people would expect. I've tried to emphasize the intent by increasing complexity in examples :
- simplest scenario - possible -
baseUrl
is enough (and sometimes it can be inferred) - more complicated case that needs complicated path manipulations (but no specific behavior for relative names ) - possible - path mappings and
baseUrl
should be enough - specific case than requires applying path mappings to relative names as well - possible - use path mappings,
baseUrl
androotDirs
.
I guess I am just missing how the last two points can't be addressed by a simple paths
that only takes a path, either or absolute to the baseUrl
, and then some recursion that keeps relative MIDs relative to the importer. Even with the other scenarios, I suspect you would also still need some sort of sledgehammer map
. The specific use case I run into now that makes this challenging (and therefore map would work) is when I am using MIDs that contain AMD plugins. Right now I have to create an ambient module that imports and then exports what I am trying to do.
@kitsonk I have the use case @vladima is talking about. For me, just a set of --includeDir
s would be sufficient (but I do need the canonicalization-of-relative-paths!). Once you add that, you might as well try and be compatible with what SystemJS does, I don't think it substantially increases complexity over that.
@mprobst Could you provide a concrete example of this need and how you would solve it with SystemJS? I'm having a hard time coming up with a real-world example of merging two code trees together that couldn't be solved by creating a package for one tree and importing it in the other.
@bryanforbes I think @vladima's example is good. Imagine you have something that generates TypeScript source. E.g. Angular 2 will have a build tool that transforms templates to TypeScript code that you can directly call in your app, for various reasons (mostly app startup time).
So you have your Angular controllers together with your templates, src/some/widget/foo_controller.ts
and src/some/widget/foo_template.html
. But you don't want to generate files in your regular source folder, as that creates a mess with version control, so you follow best practice and have a src
folder and a build
folder. The Angular template compiler generates build/some/widget/foo_template.ts
. In your foo_controller.ts
, you import FooTemplate from './foo_template';
.
This works if you pass as rootDirs
src
and build
, as ./foo_template
would get canonicalized to some/widget/foo_template
, then looked up in src
and build
in order, and found in build/some/widget/foo_template.ts
. In SystemJS, I believe you would have a mapping of src/*
and build/*
.
@mprobst Thanks for clarifying! I didn't understand the generated
directory containing TypeScript files, but your explanation helps. What still confuses me is why merging two trees together necessitates another configuration flag with potentially duplicated settings (rootDirs
) instead of just using path mapping to solve everything.
My concern about using path mapping for both canonicalization of relative module names and remapping non-relative module names is that this configuration settings become overloaded:
- it becomes more difficult for the end-user in understanding of the concept since it is no longer just path mapping
- it may lead to interesting bugs when some value of path mapping that was not intended to be used for canonicalization will be picked for this purpose.
To deal with duplication I'd rather prefer to have something for data sharing (i.e. references) instead of re-purposing field whose meaning is a already well-defined
👍
Can this go in 1.7? Pretty please with a cherry on top? And is there some way I can play with this now?
@vladima Sorry for the delay in replying. I think my concern now is that you're giving new names to already defined concepts. As I see it (and aside from the obvious difference that these property names take arrays), paths
is basically SystemJS's map
, and rootDirs
is basically like SystemJS's paths
. Why not just use those names instead? The ideas are similar to what we already know from AMD and SystemJS, so why invent new names?
@bryanforbes I'm not sure how well these map with SystemJS, but for what it's worth, rootDirs
is a much more specific and understandable name than just paths
. I'd take rootDirs
over paths
any time.
It looks like a lot of projects in the wild have already been using babel for ES6 in combination with webpack as a pretty standard configuration. It might be worth looking at how the module path resolution works in webpack.
There is no need to introduce other concepts/naming, it could be taken from https://webpack.github.io/docs/resolving.html and its configuration https://webpack.github.io/docs/configuration.html#resolve.
@opichals I might be missing something, but by my reading webpack's resolve does not meet the requirements above for resolving files relative to a set of directories.
@mprobst The resolve.root configuration variable makes it possible for a module to be searched for in folders similarly to the rootDirs
from the proposal.
The webpack's resolve.root can be an array of absolute paths (could not directly confirm this just by reading the docs, so I checked the sources.
@opichals yes, but relative imports are not canonicalized against the list of roots, are they? I read the docs as saying if I load ./foo
from a file physically located at /bar/baz
, I'll always end up at /bar/foo
instead of searching my rootDirs
.
@mprobst True. I would find that extremely confusing if any loader attempted to resolve relative path against anything else than just the folder it is physically located at (something require.js tries to do and became a nuisance to configure because of). As stated above such resolution logic seems to add complexity which reflects in complicated use and debugging.
I think we collected a couple of compelling use cases above.
Standa Opichal notifications@github.com schrieb am Mi., 21. Okt. 2015,
16:15:
@mprobst https://github.com/mprobst True. I would find that extremely
confusing if any loader attempted to resolve relative path against anything
else than just the folder it is physically located at (something require.js
tries to do and became a nuisance to configure because of). As stated above
such resolution logic seems to add complexity which reflects in complicated
use and debugging.—
Reply to this email directly or view it on GitHub
#5039 (comment)
.
I would find that extremely confusing if any loader attempted to resolve relative path against anything else than just the folder it is physically located at (something require.js tries to do and became a nuisance to configure because of).
When does RequireJS do this?
You can achieve this with RequireJS because it applies the path mappings to any path segment of any require no matter whether the require module path is absolute or not. e.g. for { path: { 'pkg': '../folder1' } }:
- require('pkg/file1') resolves to ../folder1/file1,
- require('./local/folder2/pkg/folder1') resolves to ./local/folder2/../folder1/file1.
Also it when two require() calls resolve to a single file using different arguments the module gets loaded twice.
For the proposal I would rather like to see an API-based extensibility approach to support the likes of 'Example 3' (let the user replace the resolution function somehow through a configuration or a plugin).
Hey guys
What if when resolving the imports if the path is not relative and is not found in the node_modules folders you check if is one of the parent directories
for example for a structure like :
my-project/
|- src/
|- app/
|- App.ts
|- CommonImports.ts
|- my-feature/
|- MyFeature.ts
|- sub-feature/
|- SubFeature.ts
CommonImports.ts could be imported in any of the other ts files like:
import * from "app/CommonImports" or
import * from "src/app/CommonImports"
The idea is you will search a parent folder that matches the begining of the import path till you get to the parent of the node_modules folder or the parent of the folder where tsc runs or tsconfig.json exist
this will also allow imports like
import bootstrap from "my-project/bower/src/bootstrap/bootstrap"
All this with 0 extra config in the tsconfig.json
An even is you later decide to implement path/map like systemjs could still work together giving preference to path declared in path/map
The only problem i see is that not sure if anyone else those this way, but should be easy to understand the i think.
A benefit of been able to resolve the import without requiring a path configuration, is that it will allow you to import the .ts files of a third party library completely written in ts, instead of the usual master.js + .d.ts that has to be imported usually, very useful for widgets libraries like angular material, where you are only interested in a few files not the entire lib, not sure if you ever though on supporting that kind of scenario
@gabrielguerrero the current behavior will not change. If you do specify path mappings it will be used, if you do not the current resolution logic will be used. i think this should work in your case.
Hey @mhegazy yeah the path/map will cover my case so cool if it gets implemented, I was just thinking if you wanted a no config required way, the resolve of parent dir in the import could provably work for most of the cases people need, so just giving ideas
Man, I'm feeling great about moving this feature to 1.7...
Someone tell Anders I said it was a go. :)
+1 million! The ability to do this will greatly improve support for different builds, assets organization, etc. For example with JSPM now I'm forced to duplicated my dependencies using npm just to get tsc happy.
👍 I'm currently manually copying d.ts files to a faux node_modules folder in order to get tsc to compile. This looks like it would clean up my use case!
I haven't heard a yes or no to my question of whether or not this can be moved from 1.8 to 1.7. If there was a beta of 1.7 with this in it, I'd test it today!
1.7 is in stabilization mode right now, so I'm pretty sure the answer is no.
this is currently work in progress. I'll post on this thread once I have something to try
Just to confirm - would I be correct in thinking that the resolution of .d.ts files are within the scope of this issue?
I've got a scenario where Project A depends on the build output of Project B. I want to bundle the auto-generated d.ts files from Project B alongside the artefact for use in Project A.
In Project A I have: import {X} from 'projectb'
, where 'projectb' is the name of my exported module (paths set up in RequireJS). However, I've completely struggled to get Project A's compile step to understand any type information relating to X (the types of any exports from Project B).
Another use case for path mappings involves working with (git) submodules. We do this on large projects to separate repositories (work domains). But is has an undesirable side effect when the source of a module exists multiple times in a project due to the use of submodules (the TypeScript compiler treats those duplicate sources as separate modules which is technically correct). Assume the following simplified example:
Project git
|-- A
| |-- a.ts
| |-- C (git submodule)
| |-- c.ts
|-- B
|-- b.ts
|-- C (git submodule)
|-- c.ts
Submodule C in a separate git
|-- c.ts
Let's assume the submodule C contains a class which is used in module A and module B. Module C is a stand alone module (has its own repository) which is embedded in module A and B using a git submodule (so we can maintain C in a single place and easily update modules who depend on it using a git submodule update
). This results in duplicate sources for module C in the project repository.
Now let's say class A
extends C
:
// a.ts
import { C } from "./C/c.ts"; // Import C which is a submodule rep under A
class A extends C {
}
And b.ts
contains a function which accepts any class which is derived from class C
:
// b.ts
import { C } from "./C/c.ts"; // Import C which is a submodule rep under B
import { A } from "../A/a.ts";
function DoSomethingNice(c: C): void {
}
DoSomethingNice(new A()); // Oh noes, error!!!
The above example generates a compiler error. Class A is not derived from the same class C as used in module B. TypeScript returns something like Types have separate declarations bla bla
. This makes sense, since the compiler doesn't know that the submodule C in module A (implemented in ./A/C/c.ts
) is exactly the same as C in module B (implemented in ./B/C/c.ts
). It's a duplicate source.
Now the world would be perfect when the TypeScript compiler recognizes duplicate/identical implementations (by generating some sort of hash for each module), knowing which files are identical and implement the same module (please develop TypeScript team 😉). That would really pave the road for working with projects spanned over multiple repositories and using git submodules to chain it all together (sometimes resulting in duplicate sources for the same module used multiple times in a project)! It would even strengthen the use of git modules, since the compiler would fire errors when different versions (branches) of a submodule are used within the same project.
So with path mappings it would be possible to achieve sort of the same setup, by mapping duplicate module sources to a single location in the project.
Looking forward to TypeScript 1.8!
What module targets does this proposal affect? Specifically, will this bring baseUrl support to the commonjs and es6 module targets?
currently module resolution strategy is decoupled from module targets so you can use whatever combination you like by specifying moduleResolution
option. If this option is omitted then compiler will try to use strategy it thinks will be the most appropriate: i.e. use node
if target
is commonjs
Another work of art for Babel - https://github.com/tleunen/babel-plugin-module-alias
This is pretty essential to developing any real application with Angular 2.
Looks like using: "moduleResolution": "node", in tsconfig.json is required for Angular 2.
So the only way to include custom components is to use relative paths... yuck.
This is a nightmare when you start to scale and move around.
At least allow "moduleResolution": "node" to also resolve based on an absolute path from the root dir.
+1 can't wait :)
+1 want to use jspm with tsc
+1
Same here. JSPM rocks, but TypeScript integration can be a lot better with this feature.
+1 Сustom resolver will not allow IDE analysis code. Ability to specify paths mappings is required. Paths mappings can be added to tsconfig.json then we can generate paths in tsconfig.json using build tools. Can solve this problem otherwise, by copying the definition files in a folder node_modules using build tools and specify moduleResolution: "node".
Somehow I got it in my head that this was slated for 1.8, which clearly didn't happen.
I realize that the in the grand scheme, there's a "real" path mapping strategy that needs to be developed, and that it somehow has to work with JSPM, etc.
But (a) is there any scenario in which '/someModule'
would not be interpreted as referring to someModule.ts at the project root? and (b) if '/someModule'
is to refer to the project root, could that portion of the problem be solved on the fast track while the bigger mapping strategy and technology are still being developed?
This would at least enable ES6-friendly path references and would allow developers to avoid creating full blown node modules just to be able to reference commonly used modules from a known location without having to go relative (in other words, '../../../someModule'
is a real pain in the butt).
+1 for @laurelnaiad's suggestion. This would be a quick win for that part of the problems discussed above.
Now that this is merged (🎉) how would one set the path mappings so that the compiler picks up the modules that were installed via jspm?
Now that this is merged
@frederikschubert Is it? I don't think this is merged 🌹
Sweet 🍬 Can this issue be closed 🌹
#5728 is merged. should be in typescript@next
later tonight. please try it out and let us know if there are any issues.
How do you install vnext in Visual Studio?
@amcdnl That's for VSCode, which is helpful certainly, but I'm mainly interested in installing vNext into Visual Studio 2015. I apologize for not being more specific.
@mhegazy By adding this to the 2.0 milestone, does that mean it won't be going into 1.8?
@alexdresko no, this will be in 1.8 ... see the roadmap page
I have the same question as @frederikschubert, how can we use this awesome new functionality to make tsc aware of jspm modules?
I'm testing this out and got Example 1 above working (only using baseUrl). But having trouble with Example 2. Probably I am doing something wrong but cannot see what. Here is what I have:
$ tree
.
├── folder1
│ ├── file1.ts
│ └── file2.ts
├── generated
│ └── folder2
│ └── file3.ts
└── tsconfig.json
3 directories, 4 files
$ cat tsconfig.json
{
"compilerOptions": {
"module": "system",
"sourceMap": true,
"outDir": "js_out",
"noEmitOnError": true
},
"files": [
"folder1/file1.ts"
],
"baseUrl": ".",
"paths": {
"*": ["*", "generated/*"]
}
}
$ cat folder1/file1.ts
import {hello} from 'folder1/file2';
import {world} from 'folder2/file3';
console.log(hello + world);
$ cat folder1/file2.ts
export const hello = "Hello";
$ cat generated/folder2/file3.ts
export const world = " World!";
$ tsc --version
Version 1.9.0-dev.20160128
$ tsc
folder1/file1.ts(2,21): error TS2307: Cannot find module 'folder2/file3'.
folder1/file1.ts(2,21): error TS2307: Cannot find module 'folder2/file3'.
@mhegazy By adding this to the 2.0 milestone, does that mean it won't be going into 1.8?
that is correct. We branched for 1.8 already, and we just need to stabilize it. path mapping, and readonly property support are now available in typescript@next
, and in TS 2.0 but not in 1.8.
@alexdresko no, this will be in 1.8 ... see the roadmap page
sorry about that. road map should be updated now.
@janakerman, baseURL
and Paths
are part of the compilerOptions
, your tsconfig.json file should look like:
{
"compilerOptions": {
"module": "system",
"sourceMap": true,
"outDir": "js_out",
"noEmitOnError": true,
"baseUrl": ".",
"paths": {
"*": ["*", "generated/*"]
}
},
"files": [
"folder1/file1.ts"
]
}
@mhegazy Thanks, I know I was missing something obvious :-). Now it works!
@mhegazy if it is done, why did it get pushed to 2.0? :(
Also, is there any versioned docs? like that show these nightly updates?
@mhegazy if it is done, why did it get pushed to 2.0? :(
Sorry about that, but we need to stabilize the release to ship it, and adding more code, adds more bugs, and delays the whole process.
It should be in the nightly today, npm install typescript@next
and now on NuGet (https://www.myget.org/gallery/typescript-preview)
Also, is there any versioned docs? like that show these nightly updates?
i am not sure i understand the question
@alexdresko I had commented on the PR about support for proj files in vs and seems like it isn't supported.
Regarding specifying path mappings in .csproj - this won't work, paths and rootDirs will be options that can be specified only via tsconfig.json
@prabirshrestha I want tsconfig file support. I was referring to tooling and compiler support for this new stuff.
@mhegazy So vnext is 2.0? Does it also contain everything in 1.8 already?
So vnext is 2.0? Does it also contain everything in 1.8 already?
The plan is the next release is labeled 2.0. we try to keep them around 8 weeks apart. typescript@next
has all 1.8 features/bug fixes + any post 1.8 work.
From what I understand the nuget packages provide you with support for Visual Studio builds (and msbuild) but not for intellisense as that would require changes in the compiler host of the VS TypeScript plugin.
Also, from what I understand the updates to the compiler host will only be available by the time 2.0 is released, so until then Visual Studio builds can be used with this new feature but you'll have to resort to some other strategy to get intellisense working.
Also, from what I understand the updates to the compiler host will only be available by the time 2.0 is released, so until then Visual Studio builds can be used with this new feature but you'll have to resort to some other strategy to get intellisense working.
Most changes do not require you to install a new version of the VS TypeScript plugin in. The nightly build currently does not include the full plugin setup, we are working on getting an installer published nightly along with the npm package
to use the nighlies in VS, please chek out the steps in https://github.com/Microsoft/TypeScript/wiki/Nightly-drops#visual-studio-2013-and-2015
So is support for JSPM available now in 1.8 nightly builds? or do we need to wait for 2.0?
JSPM is awesome and would love to have support for it with TS... tx for doing this...
regards
Sean
@mhegazy If I want to use the new 'paths' and 'baseUrl' mapping functionality with Visual Studio 2015 now, can I install the nightly build and expect that to work with Visual Studio, or are there Visual Studio components that would also need to be updated (that aren't available yet)? I currently have 1.8 installed and working with VS 2015 (after successfully working through some install issues documented in #6958). Is the new path resolution logic solely the responsibility of tsc, or would intellisense need updates for that as well?
Using the 1.9.x nightly build, I'm unable to get the new path mappings based module resolution to work with the command-line compiler (tsc
). Should it be working? Obviously, this is a bleeding edge feature, but it would be a huge simplifier for our development process, so we're trying to start using it now (since it already works for us with SystemJS for runtime compilation).
@mpseidel Can you post an example usage or take a glance at my usage below? It might be in my configuration.
File System:
client/
-- app/
---- components/
------ app.component.ts
---- jspm_packages/
------ npm/
-------- angular2@2.0.0-beta.3/
---------- core.d.ts
---------- core.js
-- tsconfig.json
Compiler Config in tsconfig.json
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": false,
"moduleResolution": "baseUrl",
"baseUrl": "",
"paths": {
"angular2/*": ["../jspm_packages/npm/angular2@2.0.0-beta.3/*"]
}
}
}
Module Resolution in app.component.ts
import { Component } from 'angular2/core';
Error Messages
Upon running tsc
on any files in our project, we get the following error:
error TS6063: Argument for '--moduleResolution' option must be 'node' or 'classic'.
If we run the TypeScript compiler via grunt-typescript
or atom-typescript
, we get the following error:
error TS2307: Cannot find module 'angular2/core'.
As far as I understand baseUrl
is not a valid option value for moduleResolution
.
This is my tsconfig.json. I use it for both client side (webpack) and for server side node code.
{ "compilerOptions": { "sourceMap": true, "target": "es6", "module": "commonjs", "jsx": "react", "experimentalDecorators": true, "baseUrl": ".", "paths": { "*": [ "*", "app/*", "app/client/*" ] } } }
@mko also take a look at tryLoadModuleUsingOptionalResolutionSettings
in typescript.js
. It is being called both from classicNameResolver
and nodeModuleNameResolver
Interesting. The way I read it in the original PR #5728 it looked like baseUrl
was meant to be a moduleResolution
value. I guess I got my signals crossed there.
With any of the three options (moduleResolution: "node"
, moduleResolution: "classic"
, and without any moduleResolution
value, I continue to get error TS2307: Cannot find module 'angular2/core'.
.
Your paths
value uses an interesting combination of wildcards that I saw in another one of the threads that said they had it working. We really need this to work for importing our jspm_packages
since their folder names include versions (which is really odd and makes referencing those packages in imports a pain if not using SystemJS).
Okay. So, I was able to get it to work with moduleResolution: "classic"
by changing baseUrl
to .
and restructuring to move our jspm_packages
under that base URL instead of using the ../
previous directory prefix.
I'll be really glad when this feature is fully documented. ;)
Have you tried baseUrl: "." And ... : "./jspm_packages..."?
Yep. That's what I changed it to to get it to work.
Ah - missed your last comment :) glad it works for you now!
I'm trying to get this feature to work in Visual Studio 2015.
I've followed the instructions in https://github.com/Microsoft/TypeScript/wiki/Nightly-drops#visual-studio-2013-and-2015 and have Version 1.9.0-dev.20160211 configured for Visual Studio. All of the syntax errors went away so it seems to be working when I save, but when I do a build visual studio gives the error Build: Unknown compiler option 'baseUrl' and the same for 'paths'.
Regardless of save or build, none of my .js files ever get created when working in visual studio but they work just fine when I just run tsc in the directory at the command line.
Any ideas?
Update
If I delete all of the version folders in the Microsoft SDK's folder, I get
The specified task executable location "C:\Program Files (x86)\Microsoft SDKs\TypeScript\1.9\tsc.exe" is invalid.
So it looks like it is still trying to use the old version instead of the npm installed/ VSDevMode.ps1 version.
Unfortunately this is not 'script-side-only' change and it needs updates for the managed side (that were not yet officially released) to work correctly.
Thanks for the clarification. Is there a timeline on when it will be released and work correctly?
Is there any way to make this work now in VS (hacks, code, pre-release, etc)?
@vladima did this make it to 1.8.0
? I cannot use next
as tslint
has a peer dep of 1.7.x
and npm doesn't install either versions.
Nevermind, it seems like it is not part of it.
if you use typescript@next, you should use tslint@next as well.
@mhegazy yep, figured that out after one of the maintainers pointed it out :) thanks
+1
What's the reason for not using option rootDirs
for resolving non-relative module names? Is it a bug?
Ah, as far as I understand today, rootDirs
is for resolving relative module names and baseUrl
and paths
are for resolving non-relative module names. And that's it.
apologies, did not notice your question. Yes, your understanding is correct, baseUrl
/ paths
are used to resolve non-relative module names and rootDirs
are applied only for relative names
Well, that works fine, but looks a bit done twice:
{
"compilerOptions": {
"baseUrl": "src/main",
"paths": {
"app/*": [
"../branding/lh/app/*",
"../dev/app/*"
]
},
"rootDirs": [
"src/main/",
"src/branding/lh/",
"src/dev/"
],
"outDir": "build/ts2js",
...
}
this is great, it's what we need for jspm_modules (instead of node_modules)...
Can this implementation rewrite rooted paths to relative paths for a CommonJS target? E.g.
├── folder1
│ ├── file1.ts
├── folder2
│ └── file2.ts
└── tsconfig.json
with moduleResolution node
and baseUrl "."
and module commonjs
, and this TypeScript:
file1.ts
import two from "folder2/file2";
Can it emit this JS such that Node can actually execute the require?
file1.js
var two = require("../folder2/file2");
@jwbay this feature, along with the rest of the module resolution capabilities, are only to help the compiler find the module source given a module name. no changes to the output js code. if you require "folder2/file1"
it will always be emitted this way. you might get errors if the compiler could not find a folder2/file1.ts
, but no change to the output.
You can find more documentation at https://github.com/Microsoft/TypeScript-Handbook/blob/release-2.0/pages/Module%20Resolution.md#additional-module-resolution-flags
This might be silly, but have you considered simply providing a hook into module resolution logic where users of the Typescript compiler are able to provide an async resolution function that returns the actual path on the filesystem? Instead of trying to find a "one size fits all" solution, this would allow the community to figure out ways to make it work with different packaging systems, file system layouts, etc.
Typescript is statically checked. Async is pretty far from static, so this sounds like it would be a stretch.
anyone knows if path mapping via tsconfig already landed in ts@next as does not seem to work,
tx
Sean
Path mapping support was checked in a while back. Can you please elaborate what are you trying to do and why do you think it does not work?