-
-
Notifications
You must be signed in to change notification settings - Fork 534
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
Add path mapping support to ESM and CJS loaders #1585
base: main
Are you sure you want to change the base?
Conversation
The ESM loader now resolves import paths using TypeScripts [path mapping][1] feature. [1]: https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping Signed-off-by: Thomas Scholtes <[email protected]>
Thanks for this. Because of the American holidays, I have somewhat of a backlog of issues and PRs which need attention before I'll be able to properly review this. There may be some delay. I see some refactoring in node-errors; can you explain the motivation for some of those changes? It's good to have an explanation for such changes when reviewing. Your description says this enables path mapping for the ESM loader. We'd like to also extend the same functionality to the CommonJS loader. I see that your Is this feature-flagged? We should decide if it's enabled or disabled by default, and decide how users can opt-in or opt-out. |
No problem.
The new resolving code needs to detect
I don’t think there will be any issues. The path mapping code is does not include any logic that is specific to ESM resolution. The only inputs to it are the TypeScript compiler options.
It’s not feature-flagged at the moment because I couldn’t think of a scenario where you would want the path mapping to be disabled. If you have configured the |
Good point. And I think someone could disable it like this:
|
Codecov ReportAttention: Patch coverage is
Additional details and impacted files
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left comments from an initial code review. I plan to implement the requested changes myself, but if you are feeling enthusiastic and would like to do them before me, I will certainly appreciate the assistance. No pressure though.
Overall this looks great to me. I definitely want to add CommonJS support which shouldn't be too difficult. I'm already familiar with the hooking API.
src/path-mapping.ts
Outdated
): PathMapper { | ||
if (compilerOptions.paths) { | ||
if (!compilerOptions.baseUrl) { | ||
throw new Error(`Compiler option 'baseUrl' required when 'paths' is set`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is compilerOptions.baseUrl
guaranteed to be an absolute path, or do we need to resolve it relative to the tsconfig's location?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I assumed that we always get the configuration from ts.readConfigFile()
in src/configuration.ts
and the function resolves the base URL relative to the config file. But there are probably other ways to set the base URL like through the command line, API, or environment.
To address this I think it make sense to resolve any base URL that is not a URL but a relative path to the current directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, we almost always get the configuration from ts.readConfigFile()
, so we should be good. I think the only way to set it otherwise is via the API? Since we don't support all of tsc
's command-line flags.
Maybe the correct approach is to resolve baseUrl
once within create()
so that we have a reliable, absolute baseUrl
to be used elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the correct approach is to resolve baseUrl once within create() so that we have a reliable, absolute baseUrl to be used elsewhere.
Alternatively, we could require baseUrl
to be absolute and throw an error otherwise. This would allow us to avoid implicit, environment-dependent behavior for users of the API. This might prevent some confusion when path mapping doesn’t work as expected but only fails at the resolution stage instead of the construction stage. (For example a user may think the base URL they set on the API is considered relative to tsconfig.json
and their scripts work whenever they are run from the project root. But then they run the script from a subdirectory and it fails.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, let's do that. In create()
we throw an error if baseUrl is not absolute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, scratch that, seems we always pass options through TypeScript's API to be normalized, even when provided via our API:
$ node
> require('ts-node').create({compilerOptions: {baseUrl: "foobar"}}).config.options.baseUrl
'/home/ubuntu/dev/ts-node/ts-node/path-mapping/foobar'
So I guess we're all good.
Signed-off-by: Thomas Scholtes <[email protected]>
Signed-off-by: Thomas Scholtes <[email protected]>
Signed-off-by: Thomas Scholtes <[email protected]>
I’d prefer to keep scope of this PR small and I’m not familiar enough with the CommonJS side of things so I’ll leave this to you. One thing to consider is that we’d probably want the users to explicitly opt in to path mapping for CommonJS. They’ll be using |
Fair point. Would it be a great hardship for those users to disable
tsconfig-paths? I hope they would appreciate the simplicity of removing a
component. I would like our default behaviour to be the best
forward-thinking behaviour, but perhaps we add an optional flag which can
be used to selectively disable it for commonjs? Or we somehow detect
tsconfig-paths and throw a warning when both mappers are competing?
…On Mon, Jan 24, 2022 at 6:15 AM Thomas Scholtes ***@***.***> wrote:
I definitely want to add CommonJS support which shouldn't be too
difficult. I'm already familiar with the hooking API.
I’d prefer to keep scope of this PR small and I’m not familiar enough with
the CommonJS side of things so I’ll leave this to you.
One thing to consider is that we’d probably want the users to explicitly
opt in to path mapping for CommonJS. They’ll be using tsconfig-paths at
the moment and it is unclear if the ts-node implementation can be used
side-by-side or compatible.
—
Reply to this email directly, view it on GitHub
<#1585 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAC35OGJBEFQKHZNAXQ5F33UXUYERANCNFSM5K6CBQ2Q>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
I made an attempt at adding the CommonJS resolver hook. Still some outstanding TODOs that I need to address, but this is looking good. |
Looks like we have test failures on Windows due to module specifiers using forward slashes and windows paths using backslashes. I haven't checked the code yet, but we probably have some logic assuming a module specifier and a path will always look the same. Should be an easy fix. https://github.com/TypeStrong/ts-node/runs/4932463748?check_suite_focus=true#step:15:167 |
I don’t think it would. I just wanted to point out, that this is a breaking change. I do believe most users will appreciate this replacement. Considering the release strategy it probably makes sense to have the commonjs mapping as opt-in and ask users to enable it to collect feedback. Once the feature is stable it can be enabled by default (or always) in a new major release.
This sounds complex and error-prone. I think I’d prefer a solution where it is explicitly configured |
I like that plan: opt-in via a flag today, enable by default in the next major version, get feedback in the meantime. How about |
Is this work still planned? |
some news of this pr? |
Hello! Do you know when this will be available? Thanks! |
some news about this PR? |
Sad that the
|
@mprinc The workaround is not yet clear to me from skimming through this discussion. Could you share the content of your |
suggesting tsx (and also esm/cjs loader in its description) for this feature |
This is what I'm using with node 16 /*
loader.js
Usage (I'm also using dotenv, but you can omit the dotenv parts if needed):
DOTENV_CONFIG_PATH=.env node -r dotenv/config --loader=./loader.js /bin/path/to/cli.ts
*/
import { pathToFileURL } from "node:url";
import { getFormat, load, resolve as resolveTs, transformSource } from "ts-node/esm";
import * as tsConfigPaths from "tsconfig-paths";
export { getFormat, transformSource, load };
const { absoluteBaseUrl, paths } = tsConfigPaths.loadConfig();
const matchPath = tsConfigPaths.createMatchPath(absoluteBaseUrl, paths);
export function resolve(specifier, context, defaultResolver) {
const mappedSpecifier = matchPath(specifier);
if (mappedSpecifier) {
specifier = `${mappedSpecifier}.js`;
/*
the resolve functionality can only work with file URLs, so we need to convert, this is especially
the case on windows, where the path might not be a valid URL.
*/
const url = specifier.startsWith("file:") ? specifier : pathToFileURL(specifier.toString());
return resolveTs(url.toString(), context, defaultResolver);
} else {
// If we can't find a mapping, just pass it on to the default resolver
return resolveTs(specifier, context, defaultResolver);
}
} |
@trim21 thanks, |
@trim21 I might be missing something... Why |
that's a link, click it |
@trim21 thanks. My client wasn't rendering the link. Boo. Will look in browser. |
Hi Thijs (@0x80 ) here is the code I am using // https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115
// "serve": "cross-env TS_NODE_PROJECT=\"tsconfig.build.json\" node --experimental-specifier-resolution=node --loader ./loader.js src/index.ts",
// TS_NODE_PROJECT=\"tsconfig.build.json\" node --experimental-specifier-resolution=node --loader ./loader.js src/index.ts"
import { resolve as resolveTs } from "ts-node/esm";
import * as tsConfigPaths from "tsconfig-paths";
import { pathToFileURL } from "url";
const { absoluteBaseUrl, paths } = tsConfigPaths.loadConfig();
const matchPath = tsConfigPaths.createMatchPath(absoluteBaseUrl, paths);
export function resolve(specifier, ctx, defaultResolve) {
const match = matchPath(specifier);
return match ? resolveTs(pathToFileURL(`${match}`).href, ctx, defaultResolve) : resolveTs(specifier, ctx, defaultResolve);
}
export { load, getFormat, transformSource } from 'ts-node/esm' together with the command again: # $COLABO is a path to where the file is tored
node --experimental-specifier-resolution=node --loader $COLABO/ts-esm-loader-with-tsconfig-paths.js index.ts |
NOTE: I am investigating |
@trim21 Coming from privatenumber/tsx#113 😞 Any news about this PR? |
exports.codes = {}; | ||
|
||
function defineError(code, buildMessage) { | ||
if (!buildMessage) { | ||
buildMessage = (...args) => args.join(' '); | ||
} | ||
} | ||
|
||
function createErrorCtor(errorMessageCreator) { | ||
return class CustomError extends Error { | ||
exports.codes[code] = class CustomError extends Error { | ||
constructor(...args) { | ||
super(errorMessageCreator(...args)) | ||
super(`${code}: ${buildMessage(...args)}`); | ||
this.code = code; | ||
} | ||
} | ||
} | ||
|
||
defineError("ERR_INPUT_TYPE_NOT_ALLOWED"); | ||
defineError("ERR_INVALID_ARG_VALUE"); | ||
defineError("ERR_INVALID_MODULE_SPECIFIER"); | ||
defineError("ERR_INVALID_PACKAGE_CONFIG"); | ||
defineError("ERR_INVALID_PACKAGE_TARGET"); | ||
defineError("ERR_MANIFEST_DEPENDENCY_MISSING"); | ||
defineError("ERR_MODULE_NOT_FOUND", (path, base, type = 'package') => { | ||
return `Cannot find ${type} '${path}' imported from ${base}`; | ||
}); | ||
defineError("ERR_PACKAGE_IMPORT_NOT_DEFINED"); | ||
defineError("ERR_PACKAGE_PATH_NOT_EXPORTED"); | ||
defineError("ERR_UNSUPPORTED_DIR_IMPORT"); | ||
defineError("ERR_UNSUPPORTED_ESM_URL_SCHEME"); | ||
defineError("ERR_UNKNOWN_FILE_EXTENSION"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this portion of the file exactly identical to dist-raw/node-errors.js
?
Any movement on this? |
Is this still in the works? |
no dear
…On Thu, Jan 4, 2024 at 8:57 PM Baterka ***@***.***> wrote:
Any movement on this?
—
Reply to this email directly, view it on GitHub
<#1585 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AWO46HSOCMJUSOCBYVE7RETYM3GOVAVCNFSM5K6CBQ22U5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCOBXG4ZTGNZSG44Q>
.
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|
|
we really need this :( |
Tried to run ts. After 4 hours of debugging I ended up here. Where even am I??? |
We're two and a half year into this PR, loader hooks for esm are on release condidate stage, can we push this the final mile? |
Really? 3 years in the making?? is this hard to push few lines of code and test it nowadays? |
The ESM loader now resolves import paths using TypeScripts path mapping feature.
EDIT:
Closes #1586