Skip to content
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

Open
wants to merge 79 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
3be1fde
Add path mapping support to ESM loader
geigerzaehler Dec 29, 2021
c542d98
fixup! Add path mapping support to ESM loader
geigerzaehler Dec 29, 2021
0cbfa6c
fixup! fixup! Add path mapping support to ESM loader
geigerzaehler Dec 30, 2021
e7082b1
fixup! fixup! fixup! Add path mapping support to ESM loader
geigerzaehler Dec 30, 2021
69a397b
Merge remote-tracking branch 'origin/main' into path-mapping
cspotcode Jan 24, 2022
2fcb982
address code review comments
geigerzaehler Jan 24, 2022
48f5262
map esm paths in all included files
geigerzaehler Jan 24, 2022
a19454b
improve error message when mapped module is not found
geigerzaehler Jan 24, 2022
32e26e8
Review changes; add CommonJS path mapping
cspotcode Jan 25, 2022
362935b
fix failing tests
cspotcode Jan 25, 2022
e573fd7
add path mapping to docs
cspotcode Jan 25, 2022
0012b22
add flag to enable/disable path mapping in the two loaders
cspotcode Jan 25, 2022
b6352e3
fix windows tests?
cspotcode Jan 25, 2022
85643fd
fix windows tests?
cspotcode Jan 25, 2022
9c46688
Merge remote-tracking branch 'origin/main' into path-mapping
cspotcode Jan 31, 2022
bede1b6
add path mapping option to docs
cspotcode Jan 31, 2022
80746c2
changes
cspotcode Jan 31, 2022
885b7b1
replace equal (deprecated) with strictEqual
charles-allen Feb 25, 2022
c3dbe73
extract shared tsconfig
charles-allen Feb 25, 2022
8c5fd23
move import targets 2-deep (to support combined baseUrl + * path)
charles-allen Feb 25, 2022
e66d236
test: baseUrl + no paths
charles-allen Feb 25, 2022
23f40b1
test: baseUrl + * path
charles-allen Feb 25, 2022
54fdbba
test: fallback to node_modules
charles-allen Feb 25, 2022
78652b2
clean up destructuring
charles-allen Feb 25, 2022
b8e6fb7
fix setting project
charles-allen Feb 25, 2022
2973399
test: fallback to built-in
charles-allen Feb 25, 2022
5ffd905
avoid space in command & add comment
charles-allen Feb 25, 2022
d200301
fix setting project and PATH
charles-allen Feb 27, 2022
c621af0
test: skip type-defs
charles-allen Feb 27, 2022
ec038c1
test: external imports ignore paths
charles-allen Feb 27, 2022
d471d1d
tests: relative/base-relative imports ignore paths
charles-allen Feb 27, 2022
d5728dc
Merge remote-tracking branch 'origin/main' into path-mapping
cspotcode Mar 1, 2022
5b686e1
Updates
cspotcode Mar 1, 2022
cb3706e
add missing package.json from node_modules
cspotcode Mar 1, 2022
d00bf73
rolling back a config change; I was wrong
cspotcode Mar 1, 2022
398db86
add helper execEsm(...)
charles-allen Mar 12, 2022
a91d5d7
test: decouple base-url-no-paths test (by extracting it)
charles-allen Mar 12, 2022
7829b54
test: decouple skip-type-definition test (by extracting it)
charles-allen Mar 12, 2022
9fd944e
test: refactor to apply tests across multiple module types & project …
charles-allen Mar 14, 2022
4e0d363
temporarily move old tests out of the way
charles-allen Mar 14, 2022
09cc514
test: refactor again to restore shared examples/node_modules (while m…
charles-allen Mar 14, 2022
70d9bf5
import `assert` async so we can assert pre-conditions (& so sut impor…
charles-allen Mar 14, 2022
e50abd4
sync tsconfigs
charles-allen Mar 14, 2022
73d6acc
fix error message in `import-node-built-in` (it's not a precondition)
charles-allen Mar 14, 2022
d02cbed
try to clean up cjs imports :/
charles-allen Mar 14, 2022
283309d
simplify tests
charles-allen Mar 14, 2022
f2e2f65
test: restore "ignore type definition" tests
charles-allen Mar 14, 2022
93ac2ac
delete obsolete examples
charles-allen Mar 14, 2022
bf7bfcd
fix star paths
charles-allen Mar 14, 2022
c26b9dd
move old tests a bit more out of the way
charles-allen Mar 14, 2022
09ebf63
Merge remote-tracking branch 'origin/main' into path-mapping
cspotcode Mar 15, 2022
e0af738
ensure ts-node is installed before running tests
cspotcode Mar 15, 2022
31acd50
disable typechecking in tests
cspotcode Mar 15, 2022
36377fc
test: add a case that imports an esm lib from node_modules
charles-allen Mar 28, 2022
56ef378
use require everywhere in cjs examples
charles-allen Mar 28, 2022
4dcff42
destructure import proxyLodash; align both depends-on-lodash deps as cjs
charles-allen Mar 28, 2022
ba176bf
fix baseUrl
charles-allen Mar 28, 2022
38ad3f5
revert assertions to use expect(err).toBeNull(); Clean up config cons…
charles-allen Mar 28, 2022
1687de9
prefer function declaration to arrow function
charles-allen Mar 28, 2022
8e9b117
move base 2 deep, so star path can be used in addition to base
charles-allen Mar 28, 2022
806ce74
tests: non-relative imports
charles-allen Mar 28, 2022
0fd8a5c
fix import extensions
charles-allen Mar 28, 2022
02818e3
tests: imports from js, jsx, tsx
charles-allen Mar 28, 2022
6faedd9
rename under-base to below-base (nicer lexical sort)
charles-allen Mar 28, 2022
64bb679
tests: relative imports
charles-allen Mar 28, 2022
058d2e2
tests: import invalid path
charles-allen Mar 28, 2022
4a3b6b4
test: should not use star-path to resolve relative import
charles-allen Mar 28, 2022
dc7e950
tests: basic path mapping
charles-allen Mar 28, 2022
c5ea9b0
fix import style
charles-allen Mar 28, 2022
7bfe31a
tests: map using first available candidate
charles-allen Mar 28, 2022
c8c35ca
tests: more specific path; static path
charles-allen Mar 28, 2022
99ca516
comment out file-system-base-relative import
charles-allen Mar 28, 2022
4fefc57
tests: mapping from js, jsx, tsx files
charles-allen Mar 28, 2022
3a0067e
clean up (delete all old tests)
charles-allen Mar 28, 2022
78e3eda
tweaks before pulling in the latest main branch
cspotcode May 18, 2022
4f5bc35
Merge remote-tracking branch 'origin/main' into path-mapping
cspotcode May 18, 2022
ef926b9
fix
cspotcode May 18, 2022
7552fc4
style tweak
cspotcode May 19, 2022
6632481
turn on experimental resolver; add required file extensions to esm tests
cspotcode May 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 36 additions & 12 deletions src/esm.ts
Expand Up @@ -11,11 +11,11 @@ import {
UrlWithStringQuery,
fileURLToPath,
pathToFileURL,
URL,
} from 'url';
import { extname } from 'path';
import * as assert from 'assert';
import { normalizeSlashes } from './util';
import { createPathMapper } from './path-mapping';
const {
createResolve,
} = require('../dist-raw/node-esm-resolve-implementation');
Expand Down Expand Up @@ -106,7 +106,6 @@ export function registerAndCreateEsmHooks(opts?: RegisterOptions) {

export function createEsmHooks(tsNodeService: Service) {
tsNodeService.enableExperimentalEsmLoaderInterop();
const mapPath = createPathMapper(tsNodeService.config.options);

// Custom implementation that considers additional file extensions and automatically adds file extensions
const nodeResolveImplementation = createResolve({
Expand Down Expand Up @@ -165,8 +164,10 @@ export function createEsmHooks(tsNodeService: Service) {

if (context.parentURL) {
const parentUrl = new URL(context.parentURL);
if (parentUrl.pathname && extname(parentUrl.pathname) === '.ts') {
const mappedSpecifiers = mapPath(specifier);
const parentPath =
parentUrl.protocol === 'file:' && fileURLToPath(parentUrl);
if (parentPath && !tsNodeService.ignored(parentPath)) {
const mappedSpecifiers = tsNodeService.mapPath(specifier);
if (mappedSpecifiers) {
candidateSpecifiers = mappedSpecifiers.map((path) =>
pathToFileURL(path).toString()
Expand All @@ -175,26 +176,33 @@ export function createEsmHooks(tsNodeService: Service) {
}
}

let candidateSpecifier: string | undefined;
while ((candidateSpecifier = candidateSpecifiers.shift())) {
for (let i = 0; i < candidateSpecifiers.length; i++) {
try {
return await nodeResolveImplementation.defaultResolve(
candidateSpecifier,
candidateSpecifiers[i],
context,
defaultResolve
);
} catch (err) {
const isNotFoundError = (<any>err).code === 'ERR_MODULE_NOT_FOUND';
if (isNotFoundError && candidateSpecifiers.length > 0) {
continue;
} else {
const isNotFoundError = (err as any).code === 'ERR_MODULE_NOT_FOUND';
if (!isNotFoundError) {
throw err;
cspotcode marked this conversation as resolved.
Show resolved Hide resolved
} else if (i == candidateSpecifiers.length - 1) {
cspotcode marked this conversation as resolved.
Show resolved Hide resolved
throw new MappedModuleNotFound(
specifier,
context.parentURL,
candidateSpecifiers
);
cspotcode marked this conversation as resolved.
Show resolved Hide resolved
} else {
continue;
}
}
}

// This code should be unreachable: The for-loop always returns or
// throws.
throw new Error(
`Empty mapped paths for ${specifier} imported from ${context.parentURL}`
`Unreachable code mapping ${specifier} in ${context.parentURL}`
cspotcode marked this conversation as resolved.
Show resolved Hide resolved
);
}

Expand Down Expand Up @@ -332,3 +340,19 @@ export function createEsmHooks(tsNodeService: Service) {
return { source: emittedJs };
}
}

class MappedModuleNotFound extends Error {
// Same code as other module not found errors.
static code = 'ERR_MODULE_NOT_FOUND';

constructor(specifier: string, base: string, candidates: string[]) {
super(
[
`Cannot find '${specifier}' imported from ${base} using TypeScript path mapping`,
'Candidates attempted:',
...candidates.map((candidate) => `- ${candidate}`),
].join('\n')
);
this.name = `Error [${MappedModuleNotFound.code}]`;
}
}
cspotcode marked this conversation as resolved.
Show resolved Hide resolved
12 changes: 12 additions & 0 deletions src/index.ts
Expand Up @@ -25,6 +25,7 @@ import {
} from './module-type-classifier';
import { createResolverFunctions } from './resolver-functions';
import type { createEsmHooks as createEsmHooksFn } from './esm';
import { createPathMapper } from './path-mapping';

export { TSCommon };
export {
Expand Down Expand Up @@ -482,6 +483,14 @@ export interface Service {
enableExperimentalEsmLoaderInterop(): void;
/** @internal */
transpileOnly: boolean;
/**
* @internal
*
* Map import paths to candidates according to the `paths` compiler
* option. Returns `null` if the specifier did not match and was not
* mapped.
*/
mapPath(specifier: string): string[] | null;
cspotcode marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -1318,6 +1327,8 @@ export function create(rawOptions: CreateOptions = {}): Service {
});
}

const mapPath = createPathMapper(config.options);

return {
[TS_NODE_SERVICE_BRAND]: true,
ts,
Expand All @@ -1334,6 +1345,7 @@ export function create(rawOptions: CreateOptions = {}): Service {
installSourceMapSupport,
enableExperimentalEsmLoaderInterop,
transpileOnly,
mapPath,
};
}

Expand Down
16 changes: 16 additions & 0 deletions src/test/index.spec.ts
Expand Up @@ -1234,6 +1234,22 @@ test.suite('ts-node', (test) => {
);
expect(err).toBe(null);
});

test('path mapping error candidates', async () => {
const { stderr, err } = await exec(
`${CMD_ESM_LOADER_WITHOUT_PROJECT} mapped-not-found.ts`,
{
cwd: join(TEST_DIR, './esm-path-mapping'),
}
);
expect(err).toBeTruthy();
expect(stderr).toMatch(
"[ERR_MODULE_NOT_FOUND]: Cannot find 'map2/does-not-exist.ts'"
);
// Expect tried candidates to be listed
expect(stderr).toMatch(/- file:\/\/.*mapped\/2-does-not-exist.ts/);
expect(stderr).toMatch(/- file:\/\/.*mapped\/2a-does-not-exist.ts/);
});
}

if (semver.gte(process.version, '12.0.0')) {
Expand Down
4 changes: 4 additions & 0 deletions tests/esm-path-mapping/index-js.js
@@ -0,0 +1,4 @@
import * as assert from 'assert';

import map1foo from 'map1/foo.js';
assert.equal(map1foo, 'mapped/1-foo');
4 changes: 4 additions & 0 deletions tests/esm-path-mapping/index-tsx.tsx
@@ -0,0 +1,4 @@
import * as assert from 'assert';

import map1foo from 'map1/foo.js';
assert.equal(map1foo, 'mapped/1-foo');
4 changes: 4 additions & 0 deletions tests/esm-path-mapping/index.ts
Expand Up @@ -23,6 +23,10 @@ import map2specific from 'map2/specific/foo.js';
// Path is mapped when using no wildcard
import mapStatic from 'static';

// Test path mapping in `.tsx` and `.js` files.
import './index-tsx.tsx';
import './index-js.js';

assert.equal(map1foo, 'mapped/1-foo');
assert.equal(map1jsx, 'mapped/1-jsx');
assert.equal(map2foo, 'mapped/2-foo');
Expand Down
1 change: 1 addition & 0 deletions tests/esm-path-mapping/mapped-not-found.ts
@@ -0,0 +1 @@
import 'map2/does-not-exist.ts';
1 change: 1 addition & 0 deletions tests/esm-path-mapping/tsconfig.json
@@ -1,4 +1,5 @@
{
"include": ["**/*.ts", "**/*.mts", "**/*.tsx", "**/*.js", "**/*.jsx"],
"compilerOptions": {
"module": "ESNext",
"allowJs": true,
Expand Down