Repository navigation
Path mappings based module resolution #5039
Description
Activity
- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptIn DiscussionNot yet reached consensusNot yet reached consensus
on Sep 30, 2015 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...
vladima commented
on Oct 1, 2015 ContributorAuthorMore actionswe 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
rootDirsare 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 -
baseUrlis 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
baseUrlshould be enough - specific case than requires applying path mappings to relative names as well - possible - use path mappings,
baseUrlandrootDirs.
- simplest scenario - possible -
I guess I am just missing how the last two points can't be addressed by a simple
pathsthat only takes a path, either or absolute to thebaseUrl, 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 sledgehammermap. 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.Kitson Kelly (@kitsonk) I have the use case Vladimir Matveev (@vladima) is talking about. For me, just a set of
--includeDirs 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.bryanforbes commented
on Oct 2, 2015 ContributorMore actionsMartin Probst (@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.
Bryan Forbes (@bryanforbes) I think Vladimir Matveev (@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.tsandsrc/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 asrcfolder and abuildfolder. The Angular template compiler generatesbuild/some/widget/foo_template.ts. In yourfoo_controller.ts, youimport FooTemplate from './foo_template';.This works if you pass as
rootDirssrcandbuild, as./foo_templatewould get canonicalized tosome/widget/foo_template, then looked up insrcandbuildin order, and found inbuild/some/widget/foo_template.ts. In SystemJS, I believe you would have a mapping ofsrc/*andbuild/*.bryanforbes commented
on Oct 5, 2015 ContributorMore actionsMartin Probst (@mprobst) Thanks for clarifying! I didn't understand the
generateddirectory 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.vladima commented
on Oct 5, 2015 ContributorAuthorMore actionsMy 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?
bryanforbes commented
on Oct 20, 2015 ContributorMore actionsVladimir Matveev (@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),
pathsis basically SystemJS'smap, androotDirsis basically like SystemJS'spaths. 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?Bryan Forbes (@bryanforbes) I'm not sure how well these map with SystemJS, but for what it's worth,
rootDirsis a much more specific and understandable name than justpaths. I'd takerootDirsoverpathsany time.110 remaining items
Is there a way to recreate this directory structure using TS: https://github.com/proxy/gist.github.com/ryanflorence/daafb1e3cb8ad740b346
In short:
Webpack allows you to do this:resolve: { extensions: ['', '.js', '.json', '.ts', '.tsx'], modulesDirectories: ['shared', 'node_modules'] },Which means that Webpack will recursively look up the directory tree for the
shareddir (the same way it does with the node_modules dir).This allows us to import shared components even from deeply nested directories without referencing the relative path:
import Button from 'Button';This will work as long as Button lives in any
sharedfolder that is higher up in the directory tree.Is there a way to tell
tscto look for modules in this fashion?Is it intentional that the "paths" values are computed relative to "baseUrl" when baseUrl is set?
Is it intentional that the "paths" values are computed relative to "baseUrl" when baseUrl is set?
yes. this is the same behavior other module loaders like require follow as well.
asfernandes commented
on Oct 21, 2016 More actionsShould outDir prevent some usage of paths?
This was working:
{ "compilerOptions": { "module": "commonjs", "target": "es5", "noImplicitAny": false, "sourceMap": false, "rootDir": ".", "baseUrl": ".", "paths": { "util/*": [ "../../../../Util/src/main/ts/*" ] } } }Then when I add outDir:
{ "compilerOptions": { "module": "commonjs", "target": "es5", "noImplicitAny": false, "sourceMap": false, "rootDir": ".", "baseUrl": ".", "outDir": "../../target/ts", "paths": { "util/*": [ "../../../../Util/src/main/ts/*" ] } } }The compiler says: "error TS6059: File 'C:/tmp/ts/paths/Util/src/main/ts/Util.ts' is not under 'rootDir' 'C:/tmp/ts/paths/SisModules/src/main/ts'. 'rootDir' is expected to contain all source files."
Reacted by Roman Basinschiif you are using
outDir, the compiler needs to mirror the input folder structure to the output directory. the root of the input is defined byrootDir. if there are files in the input, regardless if you are using paths or not, are not underrootDir, the compiler does not know how to emit them, and thus the error.asfernandes commented
on Oct 24, 2016 More actionsMohamed Hegazy (@mhegazy) but "the compiler needs to mirror the input folder structure to the output directory" unfortunately creates the original paths in module names instead of the ones mapped to the virtual paths. It's weird at least.
unfortunately creates the original paths in module names instead of the ones mapped to the virtual paths. It's weird at least.
that is how it is meant to work. the compiler needs the
pathsto find the declaration of your module. module names are resource identifiers and should be emitted as is and not altered. please see #5039 (comment).Reacted by Remo H. JansenReacted by j-mcneil and PendletonJonesasfernandes commented
on Oct 24, 2016 More actionsI'm not experienced in TypeScript nor modules, but I think the vpaths names would also be good for resource identifiers. In the browser they make much more sense than the real paths.
This is not meant to be a substitute of your require.config files, or system.config. this is meant to be a mirror of them to tell the compiler what your loader already knows.
Reacted by Remo H. JansenMohamed Hegazy (@mhegazy) Why not add a compile option, so people can decide whether to rewrite module names or not?
I'm using react-native with Typescript, it's hard to resolve this problem.Reacted by PendletonJonesI'm using https://github.com/ds300/react-native-typescript-transformer now,
Use absolute pathsis works for me, it works withtsconfig pathsvery well.And it gives another advantage, we no need to use
tsc -watchto compile our code beforereact-nativepackage.Reacted by XaberReacted by XaberDamn! it worked!
Thank you sapjax (@sapjax)!
Reacted by Felipe Torres (fforres)Felipe Torres (fforres) (@fforres), follow the instructions it will work.
- locked and limited conversation to collaborators
on Jun 19, 2018
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:
baseUrlas a separate module resolution strategy we are introducing a set of propertiesthat will allow to customize resoluton process in existing resolution strategies but base strategy still is used as a fallback.
rootDirsare decoupled from thebaseUrland 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 usebaseUrlto refer to modules defined inside the projectand rely on
npmto 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,pathsandrootDirs(pathscan only be used ifbaseUrlis set). If at least one of these properties is defined then compiler will try touse 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:classicresolver will check for the presense of files/a/b/c.ts,/a/b/c.tsxand/a/b/c.d.ts.noderesolver will first try to load module as file by probing the same files asclassicand then try to load module from directory(check
/a/b/c/indexwith supported extension, then peek intopackage.jsonetc. 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:
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:
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)
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.
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:
Logically files in
userFiles/projectandshared/projects/projectbelong to the same project andafter 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
rootDirsthat 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.jsonConfiguration for the example above:
{ "rootDirs": [ "userFiles/project/", "/shared/projects/project/" ] }Example 1
the location of tsconfig.json ->
projectRootprojectRoot/folder2/file2.tsrelative to the location of containing file: resolved module file name =
projectRoot/folder2/file3.tsExample 2
the location of tsconfig.json ->
projectRootfolder1/file2projectRoot/folder1/file2.ts.This file exists.
the location of tsconfig.json and will be folder that contains tsconfig.json
folder2/file3projectRoot/folder2/file3.ts.File does not exists, move to the second substitution
generated/folder2/file3projectRoot/generated/folder2/file3.ts.File exists
Example 3
All non-rooted entries in
rootDirsare expanded using location of tsconfig.json as base location so after expansionrootDirswilllook like this:
rootDir/folder1/file2rootDirs-rootDir/and for this prefix compute as suffix -folder1/file2rootDirswas found try to resolve module usingrootDir- first check ifrootDir/folder1/file2can be resolved as module - such module does not exist
rootDirs- check if modulerootDir/generated/folder1/file2exists - yes.rootDir/generated/folder1/file1rootDirs-rootDir/generatedand for this prefix compute as suffix -folder1/file1rootDirswas found try to resolve module usingrootDir- first check ifrootDir/generated/folder1/file1can be resolved as module - such module does not exist
rootDirs- check if modulerootDir/folder1/file1exists - yes.