diff --git a/README.md b/README.md index 60b9b0f..d999368 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Node.js `import` hook to instantaneously transform TypeScript to ESM on demand u - Cached for performance boost - Supports Node.js v12.20.0+ - Handles `node:` import prefixes +- Resolves `tsconfig.json` [`paths`](https://www.typescriptlang.org/tsconfig#paths) > **Tip:** > diff --git a/package.json b/package.json index e74e033..b8bdea3 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@esbuild-kit/core-utils": "^1.3.1", - "get-tsconfig": "^3.0.1" + "get-tsconfig": "^4.0.0" }, "devDependencies": { "@pvtnbr/eslint-config": "^0.22.0", @@ -58,6 +58,7 @@ "error", { "allow": [ + "test", "describe", "runTestSuite" ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e4c07c..237cfa0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,7 +8,7 @@ specifiers: eslint: ^8.15.0 execa: ^6.1.0 get-node: ^13.0.1 - get-tsconfig: ^3.0.1 + get-tsconfig: ^4.0.0 manten: ^0.1.0 pkgroll: ^1.3.0 semver: ^7.3.7 @@ -17,7 +17,7 @@ specifiers: dependencies: '@esbuild-kit/core-utils': 1.3.1 - get-tsconfig: 3.0.1 + get-tsconfig: 4.0.0 devDependencies: '@pvtnbr/eslint-config': 0.22.0_hcfsmds2fshutdssjqluwm76uu @@ -2002,8 +2002,8 @@ packages: get-intrinsic: 1.1.1 dev: true - /get-tsconfig/3.0.1: - resolution: {integrity: sha512-+m30eQjbcf3xMNdnacXH5IDAKUMbI7Mhbf3e1BHif1FzBlUhBzBlmOVc7kL4+kB035l8OCyBdI3dNXZ3of9HqA==} + /get-tsconfig/4.0.0: + resolution: {integrity: sha512-WvnskjM5QJbSjuZrJkG3CeIE/ywYjL9/UMYu5dpOa3iD6GjefbQq7J/nrZY4yo6QSYuYwmj7G7Xd9qzCjMMKUw==} dev: false /glob-parent/5.1.2: diff --git a/src/loaders-deprecated.ts b/src/loaders-deprecated.ts index fefa7d9..6ae438d 100644 --- a/src/loaders-deprecated.ts +++ b/src/loaders-deprecated.ts @@ -8,8 +8,8 @@ import { transformDynamicImport, } from '@esbuild-kit/core-utils'; import { - tsconfigRaw, sourcemaps, + tsconfigRaw, tsExtensionsPattern, getFormatFromExtension, type ModuleFormat, diff --git a/src/loaders.ts b/src/loaders.ts index 6e6fe1a..bb127ea 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -1,12 +1,14 @@ import path from 'path'; +import { pathToFileURL } from 'url'; import { transform, transformDynamicImport, resolveTsPath, } from '@esbuild-kit/core-utils'; import { - tsconfigRaw, sourcemaps, + tsconfigRaw, + tsconfigPathsMatcher, tsExtensionsPattern, getFormatFromExtension, type ModuleFormat, @@ -77,11 +79,14 @@ async function tryDirectory( } } +const fileProtocol = 'file://'; +const isPathPattern = /^\.{0,2}\//; + export const resolve: resolve = async function ( specifier, context, defaultResolve, - resursiveCall, + recursiveCall, ) { // Added in v12.20.0 // https://nodejs.org/api/esm.html#esm_node_imports @@ -94,6 +99,27 @@ export const resolve: resolve = async function ( return await tryDirectory(specifier, context, defaultResolve); } + const isPath = ( + specifier.startsWith(fileProtocol) + || isPathPattern.test(specifier) + ); + + if ( + tsconfigPathsMatcher + && !isPath // bare specifier + ) { + const possiblePaths = tsconfigPathsMatcher(specifier); + for (const possiblePath of possiblePaths) { + try { + return await resolve( + pathToFileURL(possiblePath).toString(), + context, + defaultResolve, + ); + } catch {} + } + } + /** * Typescript gives .ts, .cts, or .mts priority over actual .js, .cjs, or .mjs extensions */ @@ -117,7 +143,8 @@ export const resolve: resolve = async function ( } catch (error) { if ( (error instanceof Error) - && !resursiveCall + && isPath + && !recursiveCall ) { if ((error as any).code === 'ERR_UNSUPPORTED_DIR_IMPORT') { return await tryDirectory(specifier, context, defaultResolve); @@ -140,7 +167,7 @@ export const resolve: resolve = async function ( let { format } = resolved; - if (resolved.url.startsWith('file:')) { + if (resolved.url.startsWith(fileProtocol)) { format = getFormatFromExtension(resolved.url) ?? format; if (!format) { diff --git a/src/utils.ts b/src/utils.ts index 491a87a..bc474c5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,11 +1,13 @@ import path from 'path'; import { installSourceMapSupport } from '@esbuild-kit/core-utils'; -import getTsconfig from 'get-tsconfig'; +import { getTsconfig, createPathsMatcher } from 'get-tsconfig'; + +export const sourcemaps = installSourceMapSupport(); const tsconfig = getTsconfig(); -export const tsconfigRaw = tsconfig?.config; -export const sourcemaps = installSourceMapSupport(); +export const tsconfigRaw = tsconfig?.config; +export const tsconfigPathsMatcher = tsconfig && createPathsMatcher(tsconfig); export const tsExtensionsPattern = /\.([cm]?ts|[tj]sx)$/; diff --git a/tests/fixtures/package-module/tsconfig/src/base-url.ts b/tests/fixtures/package-module/tsconfig/src/base-url.ts new file mode 100644 index 0000000..4d49952 --- /dev/null +++ b/tests/fixtures/package-module/tsconfig/src/base-url.ts @@ -0,0 +1,3 @@ +import value from 'resolve-target'; + +console.log(value); diff --git a/tests/fixtures/package-module/tsconfig/src/paths-exact-match.ts b/tests/fixtures/package-module/tsconfig/src/paths-exact-match.ts new file mode 100644 index 0000000..1b4b714 --- /dev/null +++ b/tests/fixtures/package-module/tsconfig/src/paths-exact-match.ts @@ -0,0 +1,3 @@ +import value from 'paths-exact-match'; + +console.log(value); diff --git a/tests/fixtures/package-module/tsconfig/src/paths-prefix-match.ts b/tests/fixtures/package-module/tsconfig/src/paths-prefix-match.ts new file mode 100644 index 0000000..1b9fc00 --- /dev/null +++ b/tests/fixtures/package-module/tsconfig/src/paths-prefix-match.ts @@ -0,0 +1,3 @@ +import value from 'p/nested-resolve-target'; + +console.log(value); diff --git a/tests/fixtures/package-module/tsconfig/src/paths-suffix-match.ts b/tests/fixtures/package-module/tsconfig/src/paths-suffix-match.ts new file mode 100644 index 0000000..73cb173 --- /dev/null +++ b/tests/fixtures/package-module/tsconfig/src/paths-suffix-match.ts @@ -0,0 +1,3 @@ +import value from 'nested-resolve-target/s'; + +console.log(value); diff --git a/tests/fixtures/package-module/tsconfig/src/resolve-target.ts b/tests/fixtures/package-module/tsconfig/src/resolve-target.ts new file mode 100644 index 0000000..0c47d66 --- /dev/null +++ b/tests/fixtures/package-module/tsconfig/src/resolve-target.ts @@ -0,0 +1 @@ +export default 'resolved'; diff --git a/tests/fixtures/package-module/tsconfig/tsx.tsx b/tests/fixtures/package-module/tsconfig/src/tsx.tsx similarity index 100% rename from tests/fixtures/package-module/tsconfig/tsx.tsx rename to tests/fixtures/package-module/tsconfig/src/tsx.tsx diff --git a/tests/fixtures/package-module/tsconfig/src/utils/nested-resolve-target.ts b/tests/fixtures/package-module/tsconfig/src/utils/nested-resolve-target.ts new file mode 100644 index 0000000..0c47d66 --- /dev/null +++ b/tests/fixtures/package-module/tsconfig/src/utils/nested-resolve-target.ts @@ -0,0 +1 @@ +export default 'resolved'; diff --git a/tests/fixtures/package-module/tsconfig/tsconfig.json b/tests/fixtures/package-module/tsconfig/tsconfig.json index 2d08e7d..40c2faa 100644 --- a/tests/fixtures/package-module/tsconfig/tsconfig.json +++ b/tests/fixtures/package-module/tsconfig/tsconfig.json @@ -3,5 +3,11 @@ "jsx": "react", "jsxFactory": "console.log", "jsxFragmentFactory": "null", + "baseUrl": "./src", + "paths": { + "paths-exact-match": ["resolve-target"], + "p/*": ["utils/*"], + "*/s": ["utils/*"] + }, } } diff --git a/tests/specs/typescript/tsconfig.ts b/tests/specs/typescript/tsconfig.ts index a07e146..581edce 100644 --- a/tests/specs/typescript/tsconfig.ts +++ b/tests/specs/typescript/tsconfig.ts @@ -2,12 +2,42 @@ import { testSuite, expect } from 'manten'; import type { NodeApis } from '../../utils/node-with-loader'; export default testSuite(async ({ describe }, node: NodeApis) => { - describe('tsconfig', ({ test }) => { + describe('tsconfig', ({ test, describe }) => { test('jsxFactory & jsxFragmentFactory', async () => { - const nodeProcess = await node.load('./tsx.tsx', { + const nodeProcess = await node.load('./src/tsx.tsx', { cwd: './tsconfig', }); expect(nodeProcess.stdout).toBe('div null hello world\nnull null goodbye world'); }); + + describe('paths', ({ test }) => { + test('resolves baseUrl', async () => { + const nodeProcess = await node.load('./src/base-url.ts', { + cwd: './tsconfig', + }); + expect(nodeProcess.stdout).toBe('resolved'); + }); + + test('resolves paths exact match', async () => { + const nodeProcess = await node.load('./src/paths-exact-match.ts', { + cwd: './tsconfig', + }); + expect(nodeProcess.stdout).toBe('resolved'); + }); + + test('resolves paths prefix', async () => { + const nodeProcess = await node.load('./src/paths-prefix-match.ts', { + cwd: './tsconfig', + }); + expect(nodeProcess.stdout).toBe('resolved'); + }); + + test('resolves paths suffix', async () => { + const nodeProcess = await node.load('./src/paths-suffix-match.ts', { + cwd: './tsconfig', + }); + expect(nodeProcess.stdout).toBe('resolved'); + }); + }); }); });