diff --git a/package.json b/package.json index 1fd6c2b..14a6aa1 100644 --- a/package.json +++ b/package.json @@ -27,14 +27,14 @@ "main": "./dist/index.cjs", "exports": "./dist/index.cjs", "scripts": { - "build": "pkgroll --target node12.20 --minify", + "build": "pkgroll --target node12.20", "lint": "eslint .", "type-check": "tsc --noEmit", "test": "pnpm build && tsx tests/index.ts", "prepack": "pnpm build && clean-pkg-json" }, "dependencies": { - "@esbuild-kit/core-utils": "^3.3.2", + "@esbuild-kit/core-utils": "github:esbuild-kit/core-utils#npm/support-ts-extensions", "get-tsconfig": "^4.7.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e103b01..c2d5312 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@esbuild-kit/core-utils': - specifier: ^3.3.2 - version: 3.3.2 + specifier: github:esbuild-kit/core-utils#npm/support-ts-extensions + version: github.com/esbuild-kit/core-utils/5d90a0dd126755789a9ad47140c56d41cbae6c30 get-tsconfig: specifier: ^4.7.0 version: 4.7.0 @@ -103,6 +103,7 @@ packages: dependencies: esbuild: 0.18.20 source-map-support: 0.5.21 + dev: true /@esbuild-kit/esm-loader@2.5.5: resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==} @@ -3811,3 +3812,12 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true + + github.com/esbuild-kit/core-utils/5d90a0dd126755789a9ad47140c56d41cbae6c30: + resolution: {tarball: https://codeload.github.com/esbuild-kit/core-utils/tar.gz/5d90a0dd126755789a9ad47140c56d41cbae6c30} + name: '@esbuild-kit/core-utils' + version: 0.0.0-semantic-release + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + dev: false diff --git a/src/@types/module.d.ts b/src/@types/module.d.ts index 7155492..edac451 100644 --- a/src/@types/module.d.ts +++ b/src/@types/module.d.ts @@ -12,6 +12,16 @@ declare module 'module' { export const _extensions: NodeJS.RequireExtensions; export type Parent = { + /** + * I think filename is the more accurate but if it's not available, + * fallback to the id since is sometimes accurate. + * + * The filename is not available when a dynamic import is detected + * and it gets resolved before the parent module has finished ".load()" + * which is the method that sets the filename + */ + id: string; + /** * Can be null if the parent id is 'internal/preload' (e.g. via --require) * which doesn't have a file path. diff --git a/src/index.ts b/src/index.ts index c481e37..239ae89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import Module from 'module'; import { transformSync, installSourceMapSupport, - resolveTsPath, + // resolveTsPath, transformDynamicImport, compareNodeVersion, } from '@esbuild-kit/core-utils'; @@ -16,6 +16,40 @@ import { } from 'get-tsconfig'; import type { TransformOptions } from 'esbuild'; +// import path from 'path'; + +const tsExtensions: Record = Object.create(null); +tsExtensions[''] = ['.ts', '.tsx', '.js', '.jsx']; +tsExtensions['.js'] = ['.ts', '.tsx', '.js', '.jsx']; +tsExtensions['.jsx'] = ['.tsx', '.ts', '.jsx', '.js']; +tsExtensions['.cjs'] = ['.cts']; +tsExtensions['.mjs'] = ['.mts']; + +const resolveTsPath = ( + filePath: string, +) => { + const extension = path.extname(filePath); + const [extensionNoQuery, query] = path.extname(filePath).split('?'); + const possibleExtensions = tsExtensions[extensionNoQuery]; + + if (possibleExtensions) { + const extensionlessPath = filePath.slice( + 0, + // If there's no extension (0), slicing to 0 returns an empty path + -extension.length || undefined, + ); + return possibleExtensions.map( + tsExtension => ( + extensionlessPath + + tsExtension + + (query ? `?${query}` : '') + ), + ); + } +}; + + + const isRelativePathPattern = /^\.{1,2}\//; const isTsFilePatten = /\.[cm]?tsx?$/; const nodeModulesPath = `${path.sep}node_modules${path.sep}`; @@ -109,17 +143,31 @@ const transformer = ( */ '.js', - /** - * Loaders for implicitly resolvable extensions - * https://github.com/nodejs/node/blob/v12.16.0/lib/internal/modules/cjs/loader.js#L1166 - */ - '.ts', - '.tsx', - '.jsx', + // /** + // * Loaders for implicitly resolvable extensions + // * https://github.com/nodejs/node/blob/v12.16.0/lib/internal/modules/cjs/loader.js#L1166 + // */ + // '.ts', + // '.tsx', + // '.jsx', ].forEach((extension) => { extensions[extension] = transformer; }); +[ + '.ts', + '.tsx', + '.jsx' +].forEach((ext) => { + Object.defineProperty(extensions, ext, { + value: transformer, + + // Prevent Object.keys from detecting these extensions + // when CJS loader iterates over the possible extensions + enumerable: false, + }); +}); + /** * Loaders for explicitly resolvable extensions * (basically just .mjs because CJS loader has a special handler for it) @@ -146,7 +194,7 @@ const supportsNodePrefix = ( ); // Add support for "node:" protocol -const resolveFilename = Module._resolveFilename.bind(Module); +const defaultResolveFilename = Module._resolveFilename.bind(Module); Module._resolveFilename = (request, parent, isMain, options) => { // Added in v12.20.0 // https://nodejs.org/api/esm.html#esm_node_imports @@ -172,7 +220,7 @@ Module._resolveFilename = (request, parent, isMain, options) => { } try { - return resolveFilename( + return defaultResolveFilename( possiblePath, parent, isMain, @@ -187,7 +235,7 @@ Module._resolveFilename = (request, parent, isMain, options) => { return tsFilename; } - return resolveFilename(request, parent, isMain, options); + return defaultResolveFilename(request, parent, isMain, options); }; type NodeError = Error & { @@ -203,28 +251,35 @@ const resolveTsFilename = ( isMain: boolean, options?: Record, ) => { - const tsPath = resolveTsPath(request); + const parentFileName = parent?.filename ?? parent?.id; + if (!parentFileName || !isTsFilePatten.test(parentFileName)) { + return; + } - if ( - parent?.filename - && isTsFilePatten.test(parent.filename) - && tsPath - ) { - try { - return resolveFilename( - tsPath, - parent, - isMain, - options, - ); - } catch (error) { - const { code } = error as NodeError; - if ( - code !== 'MODULE_NOT_FOUND' - && code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' - ) { - throw error; + const tsPaths = resolveTsPath(request); + if (tsPaths) { + for (const tsPath of tsPaths) { + try { + return defaultResolveFilename( + tsPath, + parent, + isMain, + options, + ); + } catch (error) { + const { code } = error as NodeError; + if ( + code !== 'MODULE_NOT_FOUND' + && code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' + ) { + throw error; + } } } + + console.log('add index', { + request, + parent, + }); } }; diff --git a/tests/fixtures/test.ts b/tests/fixtures/test.ts new file mode 100644 index 0000000..18a467c --- /dev/null +++ b/tests/fixtures/test.ts @@ -0,0 +1 @@ +require('./lib/ts-ext-ts'); diff --git a/tests/index.ts b/tests/index.ts index 844c272..4fb8ad6 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -25,18 +25,18 @@ const nodeVersions = [ const node = await createNode(nodeVersion, './tests/fixtures'); await describe(`Node ${node.version}`, ({ runTestSuite }) => { - runTestSuite( - import('./specs/javascript/index.js'), - node, - ); + // runTestSuite( + // import('./specs/javascript/index.js'), + // node, + // ); runTestSuite( import('./specs/typescript/index.js'), node, ); - runTestSuite( - import('./specs/negative-tests.js'), - node, - ); + // runTestSuite( + // import('./specs/negative-tests.js'), + // node, + // ); }); } })(); diff --git a/tests/specs/typescript/index.ts b/tests/specs/typescript/index.ts index 104e1a8..af13d97 100644 --- a/tests/specs/typescript/index.ts +++ b/tests/specs/typescript/index.ts @@ -4,11 +4,11 @@ import type { NodeApis } from '../../utils/node-with-loader.js'; export default testSuite(async ({ describe }, node: NodeApis) => { describe('TypeScript', async ({ runTestSuite }) => { runTestSuite(import('./ts.js'), node); - runTestSuite(import('./tsx.js'), node); - runTestSuite(import('./jsx.js'), node); - runTestSuite(import('./mts.js'), node); - runTestSuite(import('./cts.js'), node); - runTestSuite(import('./tsconfig.js'), node); - runTestSuite(import('./dependencies.js'), node); + // runTestSuite(import('./tsx.js'), node); + // runTestSuite(import('./jsx.js'), node); + // runTestSuite(import('./mts.js'), node); + // runTestSuite(import('./cts.js'), node); + // runTestSuite(import('./tsconfig.js'), node); + // runTestSuite(import('./dependencies.js'), node); }); }); diff --git a/tests/specs/typescript/ts.ts b/tests/specs/typescript/ts.ts index 01c2df7..4608d48 100644 --- a/tests/specs/typescript/ts.ts +++ b/tests/specs/typescript/ts.ts @@ -87,11 +87,38 @@ export default testSuite(async ({ describe }, node: NodeApis) => { }); }); + describe('full path via .jsx', ({ test }) => { + const importPath = './lib/ts-ext-ts/index.jsx'; + + test('Load - should not work', async () => { + const nodeProcess = await node.load(importPath); + expect(nodeProcess.stderr).toMatch('Cannot find module'); + }); + + test('Import', async () => { + const nodeProcess = await node.importDynamic(importPath, { mode: 'typescript' }); + + if (semver.satisfies(node.version, nodeSupports.import)) { + expect(nodeProcess.stderr).toMatch('Cannot find module'); + } else { + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":1234}'); + } + }); + + test('Require', async () => { + const nodeProcess = await node.require(importPath, { typescript: true }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":1234}'); + }); + }); + describe('extensionless', ({ test }) => { const importPath = './lib/ts-ext-ts/index'; test('Load', async () => { const nodeProcess = await node.load(importPath); + console.log({ nodeProcess }); assertResults(nodeProcess.stdout); }); @@ -107,7 +134,10 @@ export default testSuite(async ({ describe }, node: NodeApis) => { }); test('Require', async () => { - const nodeProcess = await node.require(importPath); + const nodeProcess = await node.require(importPath, { + // Breaking change? + typescript: true, + }); assertResults(nodeProcess.stdout); expect(nodeProcess.stdout).toMatch('{"default":1234}'); }); @@ -133,7 +163,9 @@ export default testSuite(async ({ describe }, node: NodeApis) => { }); test('Require', async () => { - const nodeProcess = await node.require(importPath); + const nodeProcess = await node.require(importPath, { + typescript: true, + }); assertResults(nodeProcess.stdout, 'loaded ts-ext-ts/index.tsx.ts\n'); expect(nodeProcess.stdout).toMatch('{"default":1234}'); }); @@ -159,7 +191,9 @@ export default testSuite(async ({ describe }, node: NodeApis) => { }); test('Require', async () => { - const nodeProcess = await node.require(importPath); + const nodeProcess = await node.require(importPath, { + typescript: true, + }); assertResults(nodeProcess.stdout); expect(nodeProcess.stdout).toMatch('{"default":1234}'); }); diff --git a/tests/specs/typescript/tsx.ts b/tests/specs/typescript/tsx.ts index ae2a0fa..96bef25 100644 --- a/tests/specs/typescript/tsx.ts +++ b/tests/specs/typescript/tsx.ts @@ -47,6 +47,62 @@ export default testSuite(async ({ describe }, node: NodeApis) => { }); }); + describe('full path via js', ({ test }) => { + const importPath = './lib/ts-ext-tsx/index.js'; + + test('Load - should not work', async () => { + const nodeProcess = await node.load(importPath); + expect(nodeProcess.stderr).toMatch('Cannot find module'); + }); + + test('Import', async () => { + const nodeProcess = await node.importDynamic(importPath); + + if (semver.satisfies(node.version, nodeSupports.import)) { + expect(nodeProcess.stderr).toMatch('Cannot find module'); + } else { + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); + } + }); + + test('Require', async () => { + const nodeProcess = await node.require(importPath, { + typescript: true, + }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); + }); + }); + + describe('full path via jsx', ({ test }) => { + const importPath = './lib/ts-ext-tsx/index.jsx'; + + test('Load - should not work', async () => { + const nodeProcess = await node.load(importPath); + expect(nodeProcess.stderr).toMatch('Cannot find module'); + }); + + test('Import', async () => { + const nodeProcess = await node.importDynamic(importPath); + + if (semver.satisfies(node.version, nodeSupports.import)) { + expect(nodeProcess.stderr).toMatch('Cannot find module'); + } else { + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); + } + }); + + test('Require', async () => { + const nodeProcess = await node.require(importPath, { + typescript: true, + }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":["div",null,"hello world"]}'); + }); + }); + describe('extensionless', ({ test }) => { const importPath = './lib/ts-ext-tsx/index';