From e9e94b320479767de7536c9885d2a2c6dba0c351 Mon Sep 17 00:00:00 2001 From: Fabian Meyer <3982806+meyfa@users.noreply.github.com> Date: Tue, 30 May 2023 19:30:44 +0200 Subject: [PATCH] feat(node-resolve): Resolve js/jsx/mjs/cjs imports from TypeScript files (#1498) --- packages/node-resolve/src/index.js | 17 +++-- .../import-ts-with-cjs-extension.ts | 4 + .../fixtures/ts-import-cjs-extension/main.cts | 11 +++ .../import-ts-with-mjs-extension.ts | 4 + .../fixtures/ts-import-mjs-extension/main.mts | 11 +++ .../tsx-import-js-extension/MyComponent.tsx | 7 ++ .../import-tsx-with-js-extension.ts | 4 + .../tsx-import-jsx-extension/MyComponent.tsx | 7 ++ .../import-tsx-with-jsx-extension.ts | 4 + packages/node-resolve/test/test.mjs | 76 +++++++++++++++++++ 10 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 packages/node-resolve/test/fixtures/ts-import-cjs-extension/import-ts-with-cjs-extension.ts create mode 100644 packages/node-resolve/test/fixtures/ts-import-cjs-extension/main.cts create mode 100644 packages/node-resolve/test/fixtures/ts-import-mjs-extension/import-ts-with-mjs-extension.ts create mode 100644 packages/node-resolve/test/fixtures/ts-import-mjs-extension/main.mts create mode 100644 packages/node-resolve/test/fixtures/tsx-import-js-extension/MyComponent.tsx create mode 100644 packages/node-resolve/test/fixtures/tsx-import-js-extension/import-tsx-with-js-extension.ts create mode 100644 packages/node-resolve/test/fixtures/tsx-import-jsx-extension/MyComponent.tsx create mode 100644 packages/node-resolve/test/fixtures/tsx-import-jsx-extension/import-tsx-with-jsx-extension.ts diff --git a/packages/node-resolve/src/index.js b/packages/node-resolve/src/index.js index 3e663bc0c..559ae1516 100644 --- a/packages/node-resolve/src/index.js +++ b/packages/node-resolve/src/index.js @@ -146,11 +146,18 @@ export function nodeResolve(opts = {}) { importSpecifierList.push(`./${importee}`); } - // TypeScript files may import '.js' to refer to either '.ts' or '.tsx' - if (importer && importee.endsWith('.js')) { - for (const ext of ['.ts', '.tsx']) { - if (importer.endsWith(ext) && extensions.includes(ext)) { - importSpecifierList.push(importee.replace(/.js$/, ext)); + // TypeScript files may import '.mjs' or '.cjs' to refer to either '.mts' or '.cts'. + // They may also import .js to refer to either .ts or .tsx, and .jsx to refer to .tsx. + if (importer && /\.(ts|mts|cts|tsx)$/.test(importer)) { + for (const [importeeExt, resolvedExt] of [ + ['.js', '.ts'], + ['.js', '.tsx'], + ['.jsx', '.tsx'], + ['.mjs', '.mts'], + ['.cjs', '.cts'] + ]) { + if (importee.endsWith(importeeExt) && extensions.includes(resolvedExt)) { + importSpecifierList.push(importee.slice(0, -importeeExt.length) + resolvedExt); } } } diff --git a/packages/node-resolve/test/fixtures/ts-import-cjs-extension/import-ts-with-cjs-extension.ts b/packages/node-resolve/test/fixtures/ts-import-cjs-extension/import-ts-with-cjs-extension.ts new file mode 100644 index 000000000..2567a0bf4 --- /dev/null +++ b/packages/node-resolve/test/fixtures/ts-import-cjs-extension/import-ts-with-cjs-extension.ts @@ -0,0 +1,4 @@ +import { main } from './main.cjs'; +// This resolves as main.cts and _not_ main.cjs, despite the extension +const mainResult = main(); +export default mainResult; diff --git a/packages/node-resolve/test/fixtures/ts-import-cjs-extension/main.cts b/packages/node-resolve/test/fixtures/ts-import-cjs-extension/main.cts new file mode 100644 index 000000000..d3ffd2d79 --- /dev/null +++ b/packages/node-resolve/test/fixtures/ts-import-cjs-extension/main.cts @@ -0,0 +1,11 @@ +// To make this very clearly TypeScript and not just CJS with a CTS extension +type TestType = string | string[]; +interface Main { + (): string; + propertyCall(input?: TestType): TestType; +} + +const main: Main = () => 'It works!'; +main.propertyCall = () => ''; + +export { main }; diff --git a/packages/node-resolve/test/fixtures/ts-import-mjs-extension/import-ts-with-mjs-extension.ts b/packages/node-resolve/test/fixtures/ts-import-mjs-extension/import-ts-with-mjs-extension.ts new file mode 100644 index 000000000..891ec8f4a --- /dev/null +++ b/packages/node-resolve/test/fixtures/ts-import-mjs-extension/import-ts-with-mjs-extension.ts @@ -0,0 +1,4 @@ +import { main } from './main.mjs'; +// This resolves as main.mts and _not_ main.mjs, despite the extension +const mainResult = main(); +export default mainResult; diff --git a/packages/node-resolve/test/fixtures/ts-import-mjs-extension/main.mts b/packages/node-resolve/test/fixtures/ts-import-mjs-extension/main.mts new file mode 100644 index 000000000..3cd86a3e6 --- /dev/null +++ b/packages/node-resolve/test/fixtures/ts-import-mjs-extension/main.mts @@ -0,0 +1,11 @@ +// To make this very clearly TypeScript and not just MJS with a MTS extension +type TestType = string | string[]; +interface Main { + (): string; + propertyCall(input?: TestType): TestType; +} + +const main: Main = () => 'It works!'; +main.propertyCall = () => ''; + +export { main }; diff --git a/packages/node-resolve/test/fixtures/tsx-import-js-extension/MyComponent.tsx b/packages/node-resolve/test/fixtures/tsx-import-js-extension/MyComponent.tsx new file mode 100644 index 000000000..d15c2f4cb --- /dev/null +++ b/packages/node-resolve/test/fixtures/tsx-import-js-extension/MyComponent.tsx @@ -0,0 +1,7 @@ +// To make this very clearly TypeScript and not just JS with a TS extension +type TestType = string | string[]; +function MyComponent() { + return 'It works!'; +} + +export { MyComponent }; diff --git a/packages/node-resolve/test/fixtures/tsx-import-js-extension/import-tsx-with-js-extension.ts b/packages/node-resolve/test/fixtures/tsx-import-js-extension/import-tsx-with-js-extension.ts new file mode 100644 index 000000000..ad8d42e89 --- /dev/null +++ b/packages/node-resolve/test/fixtures/tsx-import-js-extension/import-tsx-with-js-extension.ts @@ -0,0 +1,4 @@ +import { MyComponent } from './MyComponent.js'; +// This resolves as MyComponent.tsx and _not_ MyComponent.js, despite the extension +const componentResult = MyComponent(); +export default componentResult; diff --git a/packages/node-resolve/test/fixtures/tsx-import-jsx-extension/MyComponent.tsx b/packages/node-resolve/test/fixtures/tsx-import-jsx-extension/MyComponent.tsx new file mode 100644 index 000000000..d15c2f4cb --- /dev/null +++ b/packages/node-resolve/test/fixtures/tsx-import-jsx-extension/MyComponent.tsx @@ -0,0 +1,7 @@ +// To make this very clearly TypeScript and not just JS with a TS extension +type TestType = string | string[]; +function MyComponent() { + return 'It works!'; +} + +export { MyComponent }; diff --git a/packages/node-resolve/test/fixtures/tsx-import-jsx-extension/import-tsx-with-jsx-extension.ts b/packages/node-resolve/test/fixtures/tsx-import-jsx-extension/import-tsx-with-jsx-extension.ts new file mode 100644 index 000000000..4bc3a994b --- /dev/null +++ b/packages/node-resolve/test/fixtures/tsx-import-jsx-extension/import-tsx-with-jsx-extension.ts @@ -0,0 +1,4 @@ +import { MyComponent } from './MyComponent.jsx'; +// This resolves as MyComponent.tsx and _not_ MyComponent.jsx, despite the extension +const componentResult = MyComponent(); +export default componentResult; diff --git a/packages/node-resolve/test/test.mjs b/packages/node-resolve/test/test.mjs index 3bf5902ab..ff6fbb8b2 100755 --- a/packages/node-resolve/test/test.mjs +++ b/packages/node-resolve/test/test.mjs @@ -168,6 +168,82 @@ test('supports JS extensions in TS when referring to TS imports', async (t) => { t.is(module.exports, 'It works!'); }); +test('supports JS extensions in TS when referring to TSX imports', async (t) => { + const bundle = await rollup({ + input: 'tsx-import-js-extension/import-tsx-with-js-extension.ts', + onwarn: failOnWarn(t), + plugins: [ + nodeResolve({ + extensions: ['.js', '.ts', '.tsx'] + }), + babel({ + babelHelpers: 'bundled', + plugins: ['@babel/plugin-transform-typescript'], + extensions: ['.js', '.ts', '.tsx'] + }) + ] + }); + const { module } = await testBundle(t, bundle); + t.is(module.exports, 'It works!'); +}); + +test('supports JSX extensions in TS when referring to TSX imports', async (t) => { + const bundle = await rollup({ + input: 'tsx-import-jsx-extension/import-tsx-with-jsx-extension.ts', + onwarn: failOnWarn(t), + plugins: [ + nodeResolve({ + extensions: ['.js', '.ts', '.tsx'] + }), + babel({ + babelHelpers: 'bundled', + plugins: ['@babel/plugin-transform-typescript'], + extensions: ['.js', '.ts', '.tsx'] + }) + ] + }); + const { module } = await testBundle(t, bundle); + t.is(module.exports, 'It works!'); +}); + +test('supports MJS extensions in TS when referring to MTS imports', async (t) => { + const bundle = await rollup({ + input: 'ts-import-mjs-extension/import-ts-with-mjs-extension.ts', + onwarn: failOnWarn(t), + plugins: [ + nodeResolve({ + extensions: ['.js', '.ts', '.mjs', '.mts'] + }), + babel({ + babelHelpers: 'bundled', + plugins: ['@babel/plugin-transform-typescript'], + extensions: ['.js', '.ts', '.mjs', '.mts'] + }) + ] + }); + const { module } = await testBundle(t, bundle); + t.is(module.exports, 'It works!'); +}); + +test('supports CJS extensions in TS when referring to CTS imports', async (t) => { + const bundle = await rollup({ + input: 'ts-import-cjs-extension/import-ts-with-cjs-extension.ts', + onwarn: failOnWarn(t), + plugins: [ + nodeResolve({ + extensions: ['.js', '.ts', '.cjs', '.cts'] + }), + babel({ + babelHelpers: 'bundled', + plugins: ['@babel/plugin-transform-typescript'], + extensions: ['.js', '.ts', '.cjs', '.cts'] + }) + ] + }); + const { module } = await testBundle(t, bundle); + t.is(module.exports, 'It works!'); +}); + test('supports JS extensions in TS actually importing JS with export map', async (t) => { const bundle = await rollup({ input: 'ts-import-js-extension-for-js-file-export-map/import-js-with-js-extension.ts',