From ac0c8c8a5ad38cf57b5c62c274ef71b88da520d0 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Mon, 8 Aug 2022 01:19:19 -0400 Subject: [PATCH 1/9] wip: ts-resolve --- src/index.ts | 1 + src/ts-resolve.ts | 99 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/ts-resolve.ts diff --git a/src/index.ts b/src/index.ts index 01689be7..40cd87df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export * from './types'; export * from './get-tsconfig'; export * from './parse-tsconfig'; export * from './paths-matcher'; +export * from './ts-resolve'; diff --git a/src/ts-resolve.ts b/src/ts-resolve.ts new file mode 100644 index 00000000..de98776a --- /dev/null +++ b/src/ts-resolve.ts @@ -0,0 +1,99 @@ +import fs from 'fs'; +import { parse } from 'jsonc-parser'; +import type { PackageJson } from 'type-fest'; + +const extensionsJs = ['.js', '.jsx']; +const extensionsTs = ['.ts', '.tsx']; +const jsxExtensionPattern = /\.jsx?$/; + +type FsAPI = Pick; + +function tryExtensions( + request: string, + api: FsAPI, + extensions: string[], +) { + for (const extension of extensions) { + const checkPath = request + extension; + if (api.existsSync(checkPath)) { + return checkPath; + } + } +} + +function getPackageEntry( + request: string, + api: FsAPI, +) { + const packageJsonPath = `${request}/package.json`; + if (!api.existsSync(packageJsonPath)) { + return undefined; + } + + const packageJsonString = api.readFileSync(packageJsonPath, 'utf8'); + const packageJson = parse(packageJsonString) as PackageJson; + + if (!packageJson || typeof packageJson !== 'object') { + return undefined; + } + + return packageJson.main; +} + +function resolve( + request: string, + api: FsAPI, + extensions: string[], +): string | undefined { + let resolved = tryExtensions(request, api, extensions); + if (resolved) { + return resolved; + } + + // If it has .js, strip it off and try again from start + if (jsxExtensionPattern.test(request)) { + resolved = tryExtensions(request.replace(jsxExtensionPattern, ''), api, extensions); + + if (resolved) { + return resolved; + } + } + + const stat = api.statSync(request); + if (stat.isDirectory()) { + // Check if package.json exists + // try package.json#main + const hasMain = getPackageEntry(request, api); + if (hasMain) { + resolved = resolve(hasMain, api, extensions); + if (resolved) { + return resolved; + } + } + + // Fallback if main path doesnt exist + // Try index.ts, index.tsx + resolved = tryExtensions(`${request}/index`, api, extensions); + + if (resolved) { + return resolved; + } + } +} + +export function tsResolve( + request: string, + api: FsAPI = fs, +) { + // Try resolving in TypeScript mode + let resolved = resolve(request, api, extensionsTs); + if (resolved) { + return resolved; + } + + // Try resolving in JavaScript mode + resolved = resolve(request, api, extensionsJs); + if (resolved) { + return resolved; + } +} From fc05366385f170fef05b8766a321202d0ccbd964 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Mon, 8 Aug 2022 19:39:36 -0400 Subject: [PATCH 2/9] wip --- src/ts-resolve.ts | 30 ++- tests/index.ts | 7 +- tests/specs/ts-resolve.ts | 501 ++++++++++++++++++++++++++++++++++++++ tests/utils.ts | 25 +- 4 files changed, 547 insertions(+), 16 deletions(-) create mode 100644 tests/specs/ts-resolve.ts diff --git a/src/ts-resolve.ts b/src/ts-resolve.ts index de98776a..34ba1ffe 100644 --- a/src/ts-resolve.ts +++ b/src/ts-resolve.ts @@ -1,4 +1,5 @@ import fs from 'fs'; +import path from 'path'; import { parse } from 'jsonc-parser'; import type { PackageJson } from 'type-fest'; @@ -8,6 +9,11 @@ const jsxExtensionPattern = /\.jsx?$/; type FsAPI = Pick; +const safeStat = ( + api: FsAPI, + request: string, +) => (api.existsSync(request) && api.statSync(request)); + function tryExtensions( request: string, api: FsAPI, @@ -59,21 +65,27 @@ function resolve( } } - const stat = api.statSync(request); - if (stat.isDirectory()) { - // Check if package.json exists - // try package.json#main + const stat = safeStat(api, request); + if (stat && stat.isDirectory()) { + // Check if package.json#main exists const hasMain = getPackageEntry(request, api); if (hasMain) { - resolved = resolve(hasMain, api, extensions); + const mainPath = path.join(request, hasMain); + const mainStat = safeStat(api, mainPath); + if (mainStat && mainStat.isFile()) { + return mainPath; + } + + resolved = resolve(mainPath, api, extensions); + if (resolved) { - return resolved; + return resolved; } } // Fallback if main path doesnt exist // Try index.ts, index.tsx - resolved = tryExtensions(`${request}/index`, api, extensions); + resolved = tryExtensions(path.join(request, 'index'), api, extensions); if (resolved) { return resolved; @@ -85,6 +97,10 @@ export function tsResolve( request: string, api: FsAPI = fs, ) { + // enforce that path is absolute + // handle bare specifier (node_modules lookup) + // it's handle a package has a main field that needs to be resolved with js -> tsx + // Try resolving in TypeScript mode let resolved = resolve(request, api, extensionsTs); if (resolved) { diff --git a/tests/index.ts b/tests/index.ts index e77b312a..57c6b975 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,7 +1,8 @@ import { describe } from 'manten'; describe('get-tsconfig', ({ runTestSuite }) => { - runTestSuite(import('./specs/get-tsconfig.js')); - runTestSuite(import('./specs/parse-tsconfig/index.js')); - runTestSuite(import('./specs/create-paths-matcher.js')); + // runTestSuite(import('./specs/get-tsconfig.js')); + // runTestSuite(import('./specs/parse-tsconfig/index.js')); + // runTestSuite(import('./specs/create-paths-matcher.js')); + runTestSuite(import('./specs/ts-resolve.js')); }); diff --git a/tests/specs/ts-resolve.ts b/tests/specs/ts-resolve.ts new file mode 100644 index 00000000..75f7f1db --- /dev/null +++ b/tests/specs/ts-resolve.ts @@ -0,0 +1,501 @@ +import fs from 'fs'; +import path from 'path'; +import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; +import { getTscResolution } from '../utils'; +import { tsResolve } from '#get-tsconfig'; + +export default testSuite(({ describe }) => { + describe('ts-resolve', ({ test, describe }) => { + describe('error', ({ test }) => { + test('non existent path', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + }); + + const request = path.join(fixture.path, 'non-existent-path'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved).toBeUndefined(); + expect(tsResolved.resolved).toBeUndefined(); + + await fixture.rm(); + }); + + test('explicit .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.ts': '', + }); + + const request = path.join(fixture.path, 'some-file.ts'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved).toBeUndefined(); + expect(tsResolved.resolved).toBeUndefined(); + + await fixture.rm(); + }); + }); + + describe('resolves extensionless', ({ test }) => { + test('resolve .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.ts': '', + }); + + const request = path.join(fixture.path, 'some-file'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('resolve .tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.tsx': '', + }); + + const request = path.join(fixture.path, 'some-file'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('combination', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file': '', + 'some-file.ts': '', + 'some-file.tsx': '', + }); + + const request = path.join(fixture.path, 'some-file'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + }); + + describe('resolves via .js & .jsx', ({ describe }) => { + describe('append TS extensions', ({ test }) => { + test('resolve .js -> .js.ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.js.ts': '', + }); + + const request = path.join(fixture.path, 'some-file.js'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.js.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('resolve .js -> .js.tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.js.tsx': '', + }); + + const request = path.join(fixture.path, 'some-file.js'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.js.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + }); + + describe('resolves TS counter part', ({ test }) => { + test('.js -> .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.ts': '', + }); + + const request = path.join(fixture.path, 'some-file.js'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('.jsx -> .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.ts': '', + }); + + const request = path.join(fixture.path, 'some-file.jsx'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('.js -> .tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.tsx': '', + }); + + const request = path.join(fixture.path, 'some-file.js'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('.jsx -> .tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.tsx': '', + }); + + const request = path.join(fixture.path, 'some-file.jsx'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + }); + + describe('resolves as JS', ({ test }) => { + test('.js', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.js': '', + }); + + const request = path.join(fixture.path, 'some-file.js'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('.jsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.jsx': '', + }); + + const request = path.join(fixture.path, 'some-file.jsx'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.jsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + }); + + describe('combination', ({ test }) => { + test('.js.ts over .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.js.ts': '', + 'some-file.ts': '', + }); + + const request = path.join(fixture.path, 'some-file.js'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.js.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('.ts over .js', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.js': '', + 'some-file.ts': '', + }); + + const request = path.join(fixture.path, 'some-file.js'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('.ts over .tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.ts': '', + 'some-file.tsx': '', + }); + + const request = path.join(fixture.path, 'some-file.js'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + }); + }); + + describe('resolves directories', ({ test }) => { + test('directory -> directory/index.ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'directory/index.ts': '', + }); + + const request = path.join(fixture.path, 'directory'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('directory -> directory/index.tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'directory/index.tsx': '', + }); + + const request = path.join(fixture.path, 'directory'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('directory -> directory/index.js', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'directory/index.js': '', + }); + + const request = path.join(fixture.path, 'directory'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('directory -> directory/index.jsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'directory/index.jsx': '', + }); + + const request = path.join(fixture.path, 'directory'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.jsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + }); + + describe('packages (package.json)', ({ test, describe }) => { + describe('main', () => { + test('.ts -> .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'package.json': JSON.stringify({ + main: 'file.ts', + }), + 'file.ts': '', + }, + }); + + const request = path.join(fixture.path, 'directory'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('.js -> .js', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'package.json': JSON.stringify({ + main: 'main.js', + }), + 'main.js': '', + }, + }); + + const request = path.join(fixture.path, 'directory'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/main.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('.js -> ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'package.json': JSON.stringify({ + main: 'main.js', + }), + 'main.ts': '', + }, + }); + + const request = path.join(fixture.path, 'directory'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/main.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('.js -> tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'package.json': JSON.stringify({ + main: 'main.js', + }), + 'main.tsx': '', + }, + }); + + const request = path.join(fixture.path, 'directory'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/main.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('nonexistent main fallback to index', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'package.json': JSON.stringify({ + main: 'non-existent', + }), + 'index.tsx': '', + }, + }); + + const request = path.join(fixture.path, 'directory'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('relative path', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'index.ts': '', + directory: { + 'package.json': JSON.stringify({ + main: '..', + }), + }, + }); + + const request = path.join(fixture.path, 'directory'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + + test('package.json to be parsed as JSONC', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'file.ts': '', + 'package.json': `{ + // comment here + "main": "file.js", // dangling comma + }`, + }, + }); + + const request = path.join(fixture.path, 'directory'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved!.filePath); + + await fixture.rm(); + }); + }); + + // resolve export map + }); + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts index 708b6ab1..918c41ec 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -27,7 +27,7 @@ export async function getTscTsconfig( return JSON.parse(tscProcess.stdout); } -const resolveAttemptPattern = /^(File|Directory) '(.+)'/gm; +const resolveAttemptPattern = /^(File|Directory) '(.+)' (exist|does not exist).*?(use it as a name resolution result\.)?$/gm; const divider = '='.repeat(8); @@ -36,14 +36,27 @@ async function parseTscResolve( request: string, ) { const resolveLog = stdout.slice( - stdout.indexOf(`${divider} Resolving module '${request}'`), + stdout.indexOf( + '\n', + stdout.indexOf(`${divider} Resolving module '${request}'`), + ) + 1, stdout.indexOf(`${divider} Module name '${request}'`), ); - const resolveAttempts = resolveLog.matchAll(resolveAttemptPattern); - return Array.from(resolveAttempts).map(( - [, type, filePath], - ) => ({ type, filePath })); + const resolveAttempts = resolveLog.matchAll(resolveAttemptPattern); + const attempts = Array.from(resolveAttempts).map(( + [, type, filePath, exists, resolved], + ) => ({ + type, + filePath, + exists: exists === 'exist', + resolved: Boolean(resolved), + })); + + return { + resolved: attempts.find(({ resolved }) => resolved), + attempts, + }; } export async function getTscResolution( From 4f815e12c8caab8992c0cb94df3a2a5460315c9b Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Wed, 10 Aug 2022 23:09:43 -0400 Subject: [PATCH 3/9] wip: support cjs & mjs --- src/ts-resolve.ts | 39 ++++++-- tests/specs/ts-resolve.ts | 192 +++++++++++++++++++++++++++++++++----- tests/utils.ts | 24 +++-- 3 files changed, 215 insertions(+), 40 deletions(-) diff --git a/src/ts-resolve.ts b/src/ts-resolve.ts index 34ba1ffe..33579bc4 100644 --- a/src/ts-resolve.ts +++ b/src/ts-resolve.ts @@ -3,9 +3,7 @@ import path from 'path'; import { parse } from 'jsonc-parser'; import type { PackageJson } from 'type-fest'; -const extensionsJs = ['.js', '.jsx']; -const extensionsTs = ['.ts', '.tsx']; -const jsxExtensionPattern = /\.jsx?$/; +const stripExtensionPattern = /\.([mc]js|jsx?)$/; type FsAPI = Pick; @@ -50,6 +48,7 @@ function resolve( request: string, api: FsAPI, extensions: string[], + nextGenExtensions: Record, ): string | undefined { let resolved = tryExtensions(request, api, extensions); if (resolved) { @@ -57,8 +56,16 @@ function resolve( } // If it has .js, strip it off and try again from start - if (jsxExtensionPattern.test(request)) { - resolved = tryExtensions(request.replace(jsxExtensionPattern, ''), api, extensions); + const hasExtension = request.match(stripExtensionPattern); + if (hasExtension) { + resolved = tryExtensions( + request.slice(0, hasExtension.index), + api, + ( + nextGenExtensions[hasExtension[1] as string] + || extensions + ), + ); if (resolved) { return resolved; @@ -76,7 +83,7 @@ function resolve( return mainPath; } - resolved = resolve(mainPath, api, extensions); + resolved = resolve(mainPath, api, extensions, nextGenExtensions); if (resolved) { return resolved; @@ -102,13 +109,29 @@ export function tsResolve( // it's handle a package has a main field that needs to be resolved with js -> tsx // Try resolving in TypeScript mode - let resolved = resolve(request, api, extensionsTs); + let resolved = resolve( + request, + api, + ['.ts', '.tsx'], + { + mjs: ['.mts'], + cjs: ['.cts'], + }, + ); if (resolved) { return resolved; } // Try resolving in JavaScript mode - resolved = resolve(request, api, extensionsJs); + resolved = resolve( + request, + api, + ['.js', '.jsx'], + { + mjs: ['.mjs'], + cjs: ['.cjs'], + }, + ); if (resolved) { return resolved; } diff --git a/tests/specs/ts-resolve.ts b/tests/specs/ts-resolve.ts index 75f7f1db..b106b764 100644 --- a/tests/specs/ts-resolve.ts +++ b/tests/specs/ts-resolve.ts @@ -51,7 +51,7 @@ export default testSuite(({ describe }) => { const resolved = tsResolve(request, fs); const tsResolved = await getTscResolution(request, fixture.path); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -66,7 +66,7 @@ export default testSuite(({ describe }) => { const resolved = tsResolve(request, fs); const tsResolved = await getTscResolution(request, fixture.path); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -83,7 +83,7 @@ export default testSuite(({ describe }) => { const resolved = tsResolve(request, fs); const tsResolved = await getTscResolution(request, fixture.path); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -102,7 +102,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/some-file.js.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -118,7 +118,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/some-file.js.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -136,7 +136,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -152,7 +152,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -168,7 +168,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/some-file.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -184,7 +184,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/some-file.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -202,7 +202,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/some-file.js')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -218,7 +218,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/some-file.jsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -237,7 +237,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/some-file.js.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -254,7 +254,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -271,7 +271,147 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + }); + + describe('resolves via .mjs & .cjs', ({ describe }) => { + describe('append TS extensions', ({ test }) => { + test('resolve .mjs -> .mjs.ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.mjs.ts': '', + }); + + const request = path.join(fixture.path, 'some-file.mjs'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.mjs.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('resolve .cjs -> .cjs.tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.cjs.tsx': '', + }); + + const request = path.join(fixture.path, 'some-file.cjs'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.cjs.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + + describe('resolves TS counter part', ({ test }) => { + test('.mjs -> .mts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.mts': '', + }); + + const request = path.join(fixture.path, 'some-file.mjs'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.mts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.cjs -> .cts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.cts': '', + }); + + const request = path.join(fixture.path, 'some-file.cjs'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.cts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + + describe('resolves as JS', ({ test }) => { + test('.mjs', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.mjs': '', + }); + + const request = path.join(fixture.path, 'some-file.mjs'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.mjs')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.cjs', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.cjs': '', + }); + + const request = path.join(fixture.path, 'some-file.cjs'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.cjs')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + + describe('combination', ({ test }) => { + test('.mts over .mjs', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.mjs': '', + 'some-file.mts': '', + }); + + const request = path.join(fixture.path, 'some-file.mjs'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.mts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.mjs.jsx over .mjs', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.mjs.jsx': '', + 'some-file.mjs': '', + }); + + const request = path.join(fixture.path, 'some-file.mjs'); + const resolved = tsResolve(request, fs); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.mjs.jsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -290,7 +430,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/index.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -306,7 +446,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/index.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -322,7 +462,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/index.js')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -338,7 +478,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/index.jsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -362,7 +502,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -383,7 +523,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/main.js')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -404,7 +544,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/main.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -425,7 +565,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/main.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -446,7 +586,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/index.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -467,7 +607,7 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/index.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); @@ -489,13 +629,15 @@ export default testSuite(({ describe }) => { const tsResolved = await getTscResolution(request, fixture.path); expect(resolved?.endsWith('/file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved!.filePath); + expect(resolved).toBe(tsResolved.resolved); await fixture.rm(); }); }); // resolve export map + // cts, cjs -> cts + // mts, mjs -> mts }); }); }); diff --git a/tests/utils.ts b/tests/utils.ts index 918c41ec..6032c790 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -29,20 +29,30 @@ export async function getTscTsconfig( const resolveAttemptPattern = /^(File|Directory) '(.+)' (exist|does not exist).*?(use it as a name resolution result\.)?$/gm; +const resolvedPattern = /successfully resolved to '(.+)'/; + const divider = '='.repeat(8); async function parseTscResolve( stdout: string, request: string, ) { + const logStartIndex = stdout.indexOf( + '\n', + stdout.indexOf(`${divider} Resolving module '${request}'`), + ) + 1; + + const logEndIndex = stdout.indexOf(divider + ' Module name ', logStartIndex); + const resolvedToStartIndex = logEndIndex + divider.length; + const resolvedToEndIndex = stdout.indexOf(divider, resolvedToStartIndex); + const resolveLog = stdout.slice( - stdout.indexOf( - '\n', - stdout.indexOf(`${divider} Resolving module '${request}'`), - ) + 1, - stdout.indexOf(`${divider} Module name '${request}'`), + logStartIndex, + logEndIndex, ); - + const resolvedToMessage = stdout.slice(resolvedToStartIndex, resolvedToEndIndex); + const resolvedResult = resolvedToMessage.match(resolvedPattern); + const resolved = resolvedResult?.[1]; const resolveAttempts = resolveLog.matchAll(resolveAttemptPattern); const attempts = Array.from(resolveAttempts).map(( [, type, filePath, exists, resolved], @@ -54,7 +64,7 @@ async function parseTscResolve( })); return { - resolved: attempts.find(({ resolved }) => resolved), + resolved, attempts, }; } From dcacae0dfe32dd247d9c1d82c3f0d2cb16a52857 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Thu, 11 Aug 2022 23:02:00 -0400 Subject: [PATCH 4/9] wip --- package.json | 1 + pnpm-lock.yaml | 7 + src/index.ts | 2 +- src/parse-tsconfig/resolve-extends.ts | 3 + src/paths-matcher/index.ts | 4 +- src/resolver.ts | 245 +++++++ src/ts-resolve.ts | 138 ---- src/utils/find-up.ts | 39 +- tests/index.ts | 2 +- tests/specs/resolver/error-cases.ts | 39 ++ tests/specs/resolver/index.ts | 16 + tests/specs/resolver/resolve-cjs-mjs.ts | 146 ++++ .../resolve-directories-package-json.ts | 162 +++++ tests/specs/resolver/resolve-directories.ts | 72 ++ tests/specs/resolver/resolve-extensionless.ts | 55 ++ tests/specs/resolver/resolve-js-jsx.ts | 195 ++++++ tests/specs/ts-resolve.ts | 643 ------------------ 17 files changed, 981 insertions(+), 788 deletions(-) create mode 100644 src/resolver.ts delete mode 100644 src/ts-resolve.ts create mode 100644 tests/specs/resolver/error-cases.ts create mode 100644 tests/specs/resolver/index.ts create mode 100644 tests/specs/resolver/resolve-cjs-mjs.ts create mode 100644 tests/specs/resolver/resolve-directories-package-json.ts create mode 100644 tests/specs/resolver/resolve-directories.ts create mode 100644 tests/specs/resolver/resolve-extensionless.ts create mode 100644 tests/specs/resolver/resolve-js-jsx.ts delete mode 100644 tests/specs/ts-resolve.ts diff --git a/package.json b/package.json index c05316c4..0de0250e 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "jsonc-parser": "^3.0.0", "manten": "^0.2.1", "pkgroll": "^1.3.1", + "resolve.exports": "^1.1.0", "slash": "^4.0.0", "tsx": "^3.6.0", "type-fest": "^2.13.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71cbe851..45373660 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,7 @@ specifiers: jsonc-parser: ^3.0.0 manten: ^0.2.1 pkgroll: ^1.3.1 + resolve.exports: ^1.1.0 slash: ^4.0.0 tsx: ^3.6.0 type-fest: ^2.13.1 @@ -27,6 +28,7 @@ devDependencies: jsonc-parser: 3.0.0 manten: 0.2.1 pkgroll: 1.3.1_typescript@4.7.3 + resolve.exports: 1.1.0 slash: 4.0.0 tsx: 3.6.0 type-fest: 2.13.1 @@ -2834,6 +2836,11 @@ packages: engines: {node: '>=4'} dev: true + /resolve.exports/1.1.0: + resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==} + engines: {node: '>=10'} + dev: true + /resolve/1.22.0: resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} hasBin: true diff --git a/src/index.ts b/src/index.ts index 40cd87df..f9fdea05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,4 @@ export * from './types'; export * from './get-tsconfig'; export * from './parse-tsconfig'; export * from './paths-matcher'; -export * from './ts-resolve'; +export * from './resolver'; diff --git a/src/parse-tsconfig/resolve-extends.ts b/src/parse-tsconfig/resolve-extends.ts index 79cce213..782e0cfb 100644 --- a/src/parse-tsconfig/resolve-extends.ts +++ b/src/parse-tsconfig/resolve-extends.ts @@ -5,6 +5,9 @@ import { findUp } from '../utils/find-up'; const { existsSync } = fs; +// Test if tsc allows JSONC in parent +// If it does, we can abstract this out into a comon util +// also used in ts-resolve const safeJsonParse = (jsonString: string) => { try { return JSON.parse(jsonString); diff --git a/src/paths-matcher/index.ts b/src/paths-matcher/index.ts index e5aacdd7..725e0871 100644 --- a/src/paths-matcher/index.ts +++ b/src/paths-matcher/index.ts @@ -35,13 +35,15 @@ function parsePaths( }); } +export type PathsMatcher = (specifier: string) => string[]; + /** * Reference: * https://github.com/microsoft/TypeScript/blob/3ccbe804f850f40d228d3c875be952d94d39aa1d/src/compiler/moduleNameResolver.ts#L2465 */ export function createPathsMatcher( tsconfig: TsConfigResult, -) { +): PathsMatcher | null { if (!tsconfig.config.compilerOptions) { return null; } diff --git a/src/resolver.ts b/src/resolver.ts new file mode 100644 index 00000000..6b435263 --- /dev/null +++ b/src/resolver.ts @@ -0,0 +1,245 @@ +import fs from 'fs'; +import path from 'path'; +import { parse } from 'jsonc-parser'; +import type { PackageJson } from 'type-fest'; +import { resolve as resolveExports } from 'resolve.exports'; +import { findUp } from './utils/find-up'; +import { createPathsMatcher, type PathsMatcher } from './paths-matcher/index'; +import type { TsConfigResult } from './types'; + +function readJson( + api: Pick, + jsonPath: string, +) { + const packageJsonString = api.readFileSync(jsonPath, 'utf8'); + return parse(packageJsonString); +} + +const stripExtensionPattern = /\.([mc]js|jsx?)$/; + +type FsAPI = Pick; + +const safeStat = ( + api: FsAPI, + request: string, +) => (api.existsSync(request) && api.statSync(request)); + +function tryExtensions( + request: string, + api: FsAPI, + extensions: string[], +) { + for (const extension of extensions) { + const checkPath = request + extension; + if (api.existsSync(checkPath)) { + return checkPath; + } + } +} + +function getPackageEntry( + request: string, + api: FsAPI, +) { + const packageJsonPath = `${request}/package.json`; + if (!api.existsSync(packageJsonPath)) { + return undefined; + } + + const packageJson = readJson(api, packageJsonPath) as PackageJson; + if (!packageJson || typeof packageJson !== 'object') { + return undefined; + } + + return packageJson.main; +} + +function resolve( + request: string, + api: FsAPI, + extensions: string[], + nextGenExtensions: Record, +): string | undefined { + let resolved = tryExtensions(request, api, extensions); + if (resolved) { + return resolved; + } + + // If it has .js, strip it off and try again from start + const hasExtension = request.match(stripExtensionPattern); + if (hasExtension) { + resolved = tryExtensions( + request.slice(0, hasExtension.index), + api, + ( + nextGenExtensions[hasExtension[1] as string] + || extensions + ), + ); + + if (resolved) { + return resolved; + } + } + + const stat = safeStat(api, request); + if (stat && stat.isDirectory()) { + + // Check if package.json#main exists + const hasMain = getPackageEntry(request, api); + if (hasMain) { + const mainPath = path.join(request, hasMain); + const mainStat = safeStat(api, mainPath); + if (mainStat && mainStat.isFile()) { + return mainPath; + } + + resolved = resolve(mainPath, api, extensions, nextGenExtensions); + + if (resolved) { + return resolved; + } + } + + // Fallback to index if main path doesnt exist + resolved = tryExtensions(path.join(request, 'index'), api, extensions); + + if (resolved) { + return resolved; + } + } +} + +function resolveExtensionlessPath( + request: string, + api: FsAPI, +) { + // Try resolving in TypeScript mode + let resolved = resolve( + request, + api, + ['.ts', '.tsx'], + { + mjs: ['.mts'], + cjs: ['.cts'], + }, + ); + + if (resolved) { + return resolved; + } + + // Try resolving in JavaScript mode + resolved = resolve( + request, + api, + ['.js', '.jsx'], + { + mjs: ['.mjs'], + cjs: ['.cjs'], + }, + ); + + if (resolved) { + return resolved; + } +} + +function resolveBareSpecifier( + request: string, + context: string, + pathsResolver: PathsMatcher | null | undefined, + api: FsAPI, +) { + // Try tsconfig.paths + if (pathsResolver) { + const possiblePaths = pathsResolver(request); + + for (const possiblePath of possiblePaths) { + + /** + * If a path resolves to a package, + * would it resolve the export maps? + * + * Also, what if we resolve a package path + * by absolute path? Would it resolve the export map? + * Or does it need to be resolved by bare specifier? + */ + const resolved = resolveExtensionlessPath(possiblePath, api); + if (resolved) { + return resolved; + } + } + } + + /* + 1. Find all node_module parent directories + 2. + + */ + + const nodeModuleDirectories = findUp(context, 'node_modules', true, api); + for (const nodeModuleDirectory of nodeModuleDirectories) { + const dependencyPath = path.join(nodeModuleDirectory, request); + const packageJsonPath = path.join(dependencyPath, 'package.json'); + + if (api.existsSync(packageJsonPath)) { + // test missing path in main + const hasMain = getPackageEntry(dependencyPath, api); + if (hasMain) { + const resolved = resolveExtensionlessPath(path.join(dependencyPath, hasMain), api); + if (resolved) { + return resolved; + } + } + } else { + console.log('package.json not found', packageJsonPath); + } + + const resolved = resolveExtensionlessPath(dependencyPath, api); + if (resolved) { + return resolved; + } + } +} + +export function createResolver( + tsconfig?: TsConfigResult, + api: FsAPI = fs, +) { + const preserveSymlinks = tsconfig?.config.compilerOptions?.preserveSymlinks; + const pathsResolver = tsconfig && createPathsMatcher(tsconfig); + + // TODO: include Node.js's --preserve-symlinks + const resolveSymlink = (path: string | undefined) => ( + (!path || preserveSymlinks) + ? path + : api.realpathSync(path) + ); + + return function tsResolve( + request: string, + context: string, + ): string | undefined { + + // Resolve relative specifier + if (request.startsWith('.')) { + request = path.join(context, request); + } + + // Absolute specifier + if (request.startsWith('/')) { + return resolveSymlink(resolveExtensionlessPath(request, api)); + } + + // Resolve bare specifier + return resolveSymlink( + resolveBareSpecifier( + request, + context, + pathsResolver, + api, + ), + ); + }; +} \ No newline at end of file diff --git a/src/ts-resolve.ts b/src/ts-resolve.ts deleted file mode 100644 index 33579bc4..00000000 --- a/src/ts-resolve.ts +++ /dev/null @@ -1,138 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { parse } from 'jsonc-parser'; -import type { PackageJson } from 'type-fest'; - -const stripExtensionPattern = /\.([mc]js|jsx?)$/; - -type FsAPI = Pick; - -const safeStat = ( - api: FsAPI, - request: string, -) => (api.existsSync(request) && api.statSync(request)); - -function tryExtensions( - request: string, - api: FsAPI, - extensions: string[], -) { - for (const extension of extensions) { - const checkPath = request + extension; - if (api.existsSync(checkPath)) { - return checkPath; - } - } -} - -function getPackageEntry( - request: string, - api: FsAPI, -) { - const packageJsonPath = `${request}/package.json`; - if (!api.existsSync(packageJsonPath)) { - return undefined; - } - - const packageJsonString = api.readFileSync(packageJsonPath, 'utf8'); - const packageJson = parse(packageJsonString) as PackageJson; - - if (!packageJson || typeof packageJson !== 'object') { - return undefined; - } - - return packageJson.main; -} - -function resolve( - request: string, - api: FsAPI, - extensions: string[], - nextGenExtensions: Record, -): string | undefined { - let resolved = tryExtensions(request, api, extensions); - if (resolved) { - return resolved; - } - - // If it has .js, strip it off and try again from start - const hasExtension = request.match(stripExtensionPattern); - if (hasExtension) { - resolved = tryExtensions( - request.slice(0, hasExtension.index), - api, - ( - nextGenExtensions[hasExtension[1] as string] - || extensions - ), - ); - - if (resolved) { - return resolved; - } - } - - const stat = safeStat(api, request); - if (stat && stat.isDirectory()) { - // Check if package.json#main exists - const hasMain = getPackageEntry(request, api); - if (hasMain) { - const mainPath = path.join(request, hasMain); - const mainStat = safeStat(api, mainPath); - if (mainStat && mainStat.isFile()) { - return mainPath; - } - - resolved = resolve(mainPath, api, extensions, nextGenExtensions); - - if (resolved) { - return resolved; - } - } - - // Fallback if main path doesnt exist - // Try index.ts, index.tsx - resolved = tryExtensions(path.join(request, 'index'), api, extensions); - - if (resolved) { - return resolved; - } - } -} - -export function tsResolve( - request: string, - api: FsAPI = fs, -) { - // enforce that path is absolute - // handle bare specifier (node_modules lookup) - // it's handle a package has a main field that needs to be resolved with js -> tsx - - // Try resolving in TypeScript mode - let resolved = resolve( - request, - api, - ['.ts', '.tsx'], - { - mjs: ['.mts'], - cjs: ['.cts'], - }, - ); - if (resolved) { - return resolved; - } - - // Try resolving in JavaScript mode - resolved = resolve( - request, - api, - ['.js', '.jsx'], - { - mjs: ['.mjs'], - cjs: ['.cjs'], - }, - ); - if (resolved) { - return resolved; - } -} diff --git a/src/utils/find-up.ts b/src/utils/find-up.ts index bee46d69..00b728e0 100644 --- a/src/utils/find-up.ts +++ b/src/utils/find-up.ts @@ -2,21 +2,52 @@ import path from 'path'; import fs from 'fs'; import slash from 'slash'; -export function findUp( +type FsAPI = Pick; + +function findUp( + searchPath: string, + fileName: string, + findAll?: false, + api?: FsAPI, +): string | undefined; + +function findUp( + searchPath: string, + fileName: string, + findAll: boolean, + api?: FsAPI, +): string[]; + +function findUp( searchPath: string, fileName: string, + findAll?: boolean, + api: FsAPI = fs, ) { + const foundPaths: string[] = []; + while (true) { const configPath = path.join(searchPath, fileName); - if (fs.existsSync(configPath)) { - return slash(configPath); + + if (api.existsSync(configPath)) { + const foundPath = slash(configPath); + + if (findAll) { + foundPaths.push(foundPath); + } else { + return foundPath; + } } const parentPath = path.dirname(searchPath); if (parentPath === searchPath) { - return; + break; } searchPath = parentPath; } + + return findAll ? foundPaths : undefined; } + +export { findUp }; diff --git a/tests/index.ts b/tests/index.ts index 57c6b975..e713d031 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -4,5 +4,5 @@ describe('get-tsconfig', ({ runTestSuite }) => { // runTestSuite(import('./specs/get-tsconfig.js')); // runTestSuite(import('./specs/parse-tsconfig/index.js')); // runTestSuite(import('./specs/create-paths-matcher.js')); - runTestSuite(import('./specs/ts-resolve.js')); + runTestSuite(import('./specs/resolver/index.js')); }); diff --git a/tests/specs/resolver/error-cases.ts b/tests/specs/resolver/error-cases.ts new file mode 100644 index 00000000..fc87977c --- /dev/null +++ b/tests/specs/resolver/error-cases.ts @@ -0,0 +1,39 @@ +import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; +import { getTscResolution } from '../../utils'; +import { createResolver } from '#get-tsconfig'; + +export default testSuite(({ describe }) => { + describe('error', ({ test }) => { + test('non existent path', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + }); + + const request = './non-existent-path'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved).toBeUndefined(); + expect(tsResolved.resolved).toBeUndefined(); + + await fixture.rm(); + }); + + test('explicit .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.ts': '', + }); + + const request = './some-file.ts'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved).toBeUndefined(); + expect(tsResolved.resolved).toBeUndefined(); + + await fixture.rm(); + }); + }); +}); diff --git a/tests/specs/resolver/index.ts b/tests/specs/resolver/index.ts new file mode 100644 index 00000000..a76f7f78 --- /dev/null +++ b/tests/specs/resolver/index.ts @@ -0,0 +1,16 @@ +import fs from 'fs'; +import path from 'path'; +import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; +import { getTscResolution } from '../../utils'; + +export default testSuite(({ describe }) => { + describe('resolver', async ({ test, describe, runTestSuite }) => { + runTestSuite(import('./error-cases.js')); + runTestSuite(import('./resolve-extensionless.js')); + runTestSuite(import('./resolve-js-jsx.js')); + runTestSuite(import('./resolve-cjs-mjs.js')); + runTestSuite(import('./resolve-directories.js')); + runTestSuite(import('./resolve-directories-package-json.js')); + }); +}); diff --git a/tests/specs/resolver/resolve-cjs-mjs.ts b/tests/specs/resolver/resolve-cjs-mjs.ts new file mode 100644 index 00000000..d0cc5235 --- /dev/null +++ b/tests/specs/resolver/resolve-cjs-mjs.ts @@ -0,0 +1,146 @@ +import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; +import { getTscResolution } from '../../utils'; +import { createResolver } from '#get-tsconfig'; + +export default testSuite(({ describe }) => { + describe('.mjs & .cjs', ({ describe }) => { + describe('append TS extensions', ({ test }) => { + test('resolve .mjs -> .mjs.ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.mjs.ts': '', + }); + + const request = './some-file.mjs'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.mjs.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('resolve .cjs -> .cjs.tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.cjs.tsx': '', + }); + + const request = './some-file.cjs'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.cjs.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + + describe('resolves TS counter part', ({ test }) => { + test('.mjs -> .mts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.mts': '', + }); + + const request = './some-file.mjs'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.mts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.cjs -> .cts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.cts': '', + }); + + const request = './some-file.cjs'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.cts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + + describe('resolves as JS', ({ test }) => { + test('.mjs', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.mjs': '', + }); + + const request = './some-file.mjs'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.mjs')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.cjs', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.cjs': '', + }); + + const request = './some-file.cjs'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.cjs')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + + describe('combination', ({ test }) => { + test('.mts over .mjs', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.mjs': '', + 'some-file.mts': '', + }); + + const request = './some-file.mjs'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.mts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.mjs.jsx over .mjs', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.mjs.jsx': '', + 'some-file.mjs': '', + }); + + const request = './some-file.mjs'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.mjs.jsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + }); +}); diff --git a/tests/specs/resolver/resolve-directories-package-json.ts b/tests/specs/resolver/resolve-directories-package-json.ts new file mode 100644 index 00000000..591d9c48 --- /dev/null +++ b/tests/specs/resolver/resolve-directories-package-json.ts @@ -0,0 +1,162 @@ +import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; +import { getTscResolution } from '../../utils'; +import { createResolver } from '#get-tsconfig'; + +export default testSuite(({ describe }) => { + describe('directories with package.json', ({ test, describe }) => { + describe('main', () => { + test('.ts -> .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'package.json': JSON.stringify({ + main: 'file.ts', + }), + 'file.ts': '', + }, + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.js -> .js', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'package.json': JSON.stringify({ + main: 'main.js', + }), + 'main.js': '', + }, + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/main.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.js -> ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'package.json': JSON.stringify({ + main: 'main.js', + }), + 'main.ts': '', + }, + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/main.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.js -> tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'package.json': JSON.stringify({ + main: 'main.js', + }), + 'main.tsx': '', + }, + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/main.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('nonexistent main fallback to index', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'package.json': JSON.stringify({ + main: 'non-existent', + }), + 'index.tsx': '', + }, + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('relative path', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'index.ts': '', + directory: { + 'package.json': JSON.stringify({ + main: '..', + }), + }, + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('package.json to be parsed as JSONC', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'file.ts': '', + 'package.json': `{ + // comment here + "main": "file.js", // dangling comma + }`, + }, + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + // shouldnt resolve export map + }); + + // resolve export map + }); +}); diff --git a/tests/specs/resolver/resolve-directories.ts b/tests/specs/resolver/resolve-directories.ts new file mode 100644 index 00000000..6721a5b3 --- /dev/null +++ b/tests/specs/resolver/resolve-directories.ts @@ -0,0 +1,72 @@ +import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; +import { getTscResolution } from '../../utils'; +import { createResolver } from '#get-tsconfig'; + +export default testSuite(({ describe }) => { + describe('resolves directories', ({ test }) => { + test('directory -> directory/index.ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'directory/index.ts': '', + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('directory -> directory/index.tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'directory/index.tsx': '', + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('directory -> directory/index.js', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'directory/index.js': '', + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('directory -> directory/index.jsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'directory/index.jsx': '', + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.jsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); +}); diff --git a/tests/specs/resolver/resolve-extensionless.ts b/tests/specs/resolver/resolve-extensionless.ts new file mode 100644 index 00000000..f1de1fb2 --- /dev/null +++ b/tests/specs/resolver/resolve-extensionless.ts @@ -0,0 +1,55 @@ +import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; +import { getTscResolution } from '../../utils'; +import { createResolver } from '#get-tsconfig'; + +export default testSuite(({ describe }) => { + describe('resolves extensionless', ({ test }) => { + test('resolve .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.ts': '', + }); + + const request = './some-file'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('resolve .tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.tsx': '', + }); + + const request = './some-file'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('combination', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file': '', + 'some-file.ts': '', + 'some-file.tsx': '', + }); + + const request = './some-file'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); +}); diff --git a/tests/specs/resolver/resolve-js-jsx.ts b/tests/specs/resolver/resolve-js-jsx.ts new file mode 100644 index 00000000..bd692ff1 --- /dev/null +++ b/tests/specs/resolver/resolve-js-jsx.ts @@ -0,0 +1,195 @@ +import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; +import { getTscResolution } from '../../utils'; +import { createResolver } from '#get-tsconfig'; + +export default testSuite(({ describe }) => { + describe('.js & .jsx', ({ describe }) => { + describe('append TS extensions', ({ test }) => { + test('resolve .js -> .js.ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.js.ts': '', + }); + + const request = './some-file.js'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.js.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('resolve .js -> .js.tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.js.tsx': '', + }); + + const request = './some-file.js'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.js.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + + describe('resolves TS counter part', ({ test }) => { + test('.js -> .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.ts': '', + }); + + const request = './some-file.js'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.jsx -> .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.ts': '', + }); + + const request = './some-file.jsx'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.js -> .tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.tsx': '', + }); + + const request = './some-file.js'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.jsx -> .tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.tsx': '', + }); + + const request = './some-file.jsx'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + + describe('resolves as JS', ({ test }) => { + test('.js', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.js': '', + }); + + const request = './some-file.js'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.jsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.jsx': '', + }); + + const request = './some-file.jsx'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.jsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + + describe('combination', ({ test }) => { + test('.js.ts over .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.js.ts': '', + 'some-file.ts': '', + }); + + const request = './some-file.js'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.js.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.ts over .js', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.js': '', + 'some-file.ts': '', + }); + + const request = './some-file.js'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.ts over .tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'some-file.ts': '', + 'some-file.tsx': '', + }); + + const request = './some-file.js'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + }); +}); diff --git a/tests/specs/ts-resolve.ts b/tests/specs/ts-resolve.ts deleted file mode 100644 index b106b764..00000000 --- a/tests/specs/ts-resolve.ts +++ /dev/null @@ -1,643 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { testSuite, expect } from 'manten'; -import { createFixture } from 'fs-fixture'; -import { getTscResolution } from '../utils'; -import { tsResolve } from '#get-tsconfig'; - -export default testSuite(({ describe }) => { - describe('ts-resolve', ({ test, describe }) => { - describe('error', ({ test }) => { - test('non existent path', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - }); - - const request = path.join(fixture.path, 'non-existent-path'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved).toBeUndefined(); - expect(tsResolved.resolved).toBeUndefined(); - - await fixture.rm(); - }); - - test('explicit .ts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.ts': '', - }); - - const request = path.join(fixture.path, 'some-file.ts'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved).toBeUndefined(); - expect(tsResolved.resolved).toBeUndefined(); - - await fixture.rm(); - }); - }); - - describe('resolves extensionless', ({ test }) => { - test('resolve .ts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.ts': '', - }); - - const request = path.join(fixture.path, 'some-file'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('resolve .tsx', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.tsx': '', - }); - - const request = path.join(fixture.path, 'some-file'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('combination', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file': '', - 'some-file.ts': '', - 'some-file.tsx': '', - }); - - const request = path.join(fixture.path, 'some-file'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - }); - - describe('resolves via .js & .jsx', ({ describe }) => { - describe('append TS extensions', ({ test }) => { - test('resolve .js -> .js.ts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.js.ts': '', - }); - - const request = path.join(fixture.path, 'some-file.js'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.js.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('resolve .js -> .js.tsx', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.js.tsx': '', - }); - - const request = path.join(fixture.path, 'some-file.js'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.js.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - }); - - describe('resolves TS counter part', ({ test }) => { - test('.js -> .ts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.ts': '', - }); - - const request = path.join(fixture.path, 'some-file.js'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.jsx -> .ts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.ts': '', - }); - - const request = path.join(fixture.path, 'some-file.jsx'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.js -> .tsx', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.tsx': '', - }); - - const request = path.join(fixture.path, 'some-file.js'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.jsx -> .tsx', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.tsx': '', - }); - - const request = path.join(fixture.path, 'some-file.jsx'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - }); - - describe('resolves as JS', ({ test }) => { - test('.js', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.js': '', - }); - - const request = path.join(fixture.path, 'some-file.js'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.js')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.jsx', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.jsx': '', - }); - - const request = path.join(fixture.path, 'some-file.jsx'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.jsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - }); - - describe('combination', ({ test }) => { - test('.js.ts over .ts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.js.ts': '', - 'some-file.ts': '', - }); - - const request = path.join(fixture.path, 'some-file.js'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.js.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.ts over .js', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.js': '', - 'some-file.ts': '', - }); - - const request = path.join(fixture.path, 'some-file.js'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.ts over .tsx', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.ts': '', - 'some-file.tsx': '', - }); - - const request = path.join(fixture.path, 'some-file.js'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - }); - }); - - describe('resolves via .mjs & .cjs', ({ describe }) => { - describe('append TS extensions', ({ test }) => { - test('resolve .mjs -> .mjs.ts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.mjs.ts': '', - }); - - const request = path.join(fixture.path, 'some-file.mjs'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.mjs.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('resolve .cjs -> .cjs.tsx', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.cjs.tsx': '', - }); - - const request = path.join(fixture.path, 'some-file.cjs'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.cjs.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - }); - - describe('resolves TS counter part', ({ test }) => { - test('.mjs -> .mts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.mts': '', - }); - - const request = path.join(fixture.path, 'some-file.mjs'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.mts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.cjs -> .cts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.cts': '', - }); - - const request = path.join(fixture.path, 'some-file.cjs'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.cts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - }); - - describe('resolves as JS', ({ test }) => { - test('.mjs', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.mjs': '', - }); - - const request = path.join(fixture.path, 'some-file.mjs'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.mjs')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.cjs', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.cjs': '', - }); - - const request = path.join(fixture.path, 'some-file.cjs'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.cjs')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - }); - - describe('combination', ({ test }) => { - test('.mts over .mjs', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.mjs': '', - 'some-file.mts': '', - }); - - const request = path.join(fixture.path, 'some-file.mjs'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.mts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.mjs.jsx over .mjs', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'some-file.mjs.jsx': '', - 'some-file.mjs': '', - }); - - const request = path.join(fixture.path, 'some-file.mjs'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/some-file.mjs.jsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - }); - }); - - describe('resolves directories', ({ test }) => { - test('directory -> directory/index.ts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'directory/index.ts': '', - }); - - const request = path.join(fixture.path, 'directory'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/index.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('directory -> directory/index.tsx', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'directory/index.tsx': '', - }); - - const request = path.join(fixture.path, 'directory'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/index.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('directory -> directory/index.js', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'directory/index.js': '', - }); - - const request = path.join(fixture.path, 'directory'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/index.js')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('directory -> directory/index.jsx', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'directory/index.jsx': '', - }); - - const request = path.join(fixture.path, 'directory'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/index.jsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - }); - - describe('packages (package.json)', ({ test, describe }) => { - describe('main', () => { - test('.ts -> .ts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - directory: { - 'package.json': JSON.stringify({ - main: 'file.ts', - }), - 'file.ts': '', - }, - }); - - const request = path.join(fixture.path, 'directory'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.js -> .js', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - directory: { - 'package.json': JSON.stringify({ - main: 'main.js', - }), - 'main.js': '', - }, - }); - - const request = path.join(fixture.path, 'directory'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/main.js')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.js -> ts', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - directory: { - 'package.json': JSON.stringify({ - main: 'main.js', - }), - 'main.ts': '', - }, - }); - - const request = path.join(fixture.path, 'directory'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/main.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('.js -> tsx', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - directory: { - 'package.json': JSON.stringify({ - main: 'main.js', - }), - 'main.tsx': '', - }, - }); - - const request = path.join(fixture.path, 'directory'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/main.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('nonexistent main fallback to index', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - directory: { - 'package.json': JSON.stringify({ - main: 'non-existent', - }), - 'index.tsx': '', - }, - }); - - const request = path.join(fixture.path, 'directory'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/index.tsx')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('relative path', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - 'index.ts': '', - directory: { - 'package.json': JSON.stringify({ - main: '..', - }), - }, - }); - - const request = path.join(fixture.path, 'directory'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/index.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - - test('package.json to be parsed as JSONC', async () => { - const fixture = await createFixture({ - 'tsconfig.json': '', - directory: { - 'file.ts': '', - 'package.json': `{ - // comment here - "main": "file.js", // dangling comma - }`, - }, - }); - - const request = path.join(fixture.path, 'directory'); - const resolved = tsResolve(request, fs); - const tsResolved = await getTscResolution(request, fixture.path); - - expect(resolved?.endsWith('/file.ts')).toBeTruthy(); - expect(resolved).toBe(tsResolved.resolved); - - await fixture.rm(); - }); - }); - - // resolve export map - // cts, cjs -> cts - // mts, mjs -> mts - }); - }); -}); From a5b99435cce0ed5dd3c34e3137e91f5204f2498f Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Thu, 11 Aug 2022 23:05:08 -0400 Subject: [PATCH 5/9] wip --- tests/index.ts | 6 ++-- tests/specs/create-paths-matcher.ts | 36 +++++++++---------- .../resolve-directories-package-json.ts | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/index.ts b/tests/index.ts index e713d031..6a41ab03 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,8 +1,8 @@ import { describe } from 'manten'; describe('get-tsconfig', ({ runTestSuite }) => { - // runTestSuite(import('./specs/get-tsconfig.js')); - // runTestSuite(import('./specs/parse-tsconfig/index.js')); - // runTestSuite(import('./specs/create-paths-matcher.js')); + runTestSuite(import('./specs/get-tsconfig.js')); + runTestSuite(import('./specs/parse-tsconfig/index.js')); + runTestSuite(import('./specs/create-paths-matcher.js')); runTestSuite(import('./specs/resolver/index.js')); }); diff --git a/tests/specs/create-paths-matcher.ts b/tests/specs/create-paths-matcher.ts index 0c32bcd9..1a6da5f7 100644 --- a/tests/specs/create-paths-matcher.ts +++ b/tests/specs/create-paths-matcher.ts @@ -117,9 +117,9 @@ export default testSuite(({ describe }) => { const matcher = createPathsMatcher(tsconfig!)!; expect(matcher).not.toBeNull(); - const resolvedAttempts = await getTscResolution('exactMatch', fixture.path); + const resolved = await getTscResolution('exactMatch', fixture.path); expect(matcher('exactMatch')).toStrictEqual([ - resolvedAttempts[0].filePath.slice(0, -3), + resolved.attempts[0].filePath.slice(0, -3), ]); await fixture.rm(); @@ -152,9 +152,9 @@ export default testSuite(({ describe }) => { const matcher = createPathsMatcher(tsconfig!)!; expect(matcher).not.toBeNull(); - const resolvedAttempts = await getTscResolution('$lib', fixture.path); + const resolved = await getTscResolution('$lib', fixture.path); expect(matcher('$lib')).toStrictEqual([ - resolvedAttempts[0].filePath.slice(0, -3), + resolved.attempts[0].filePath.slice(0, -3), ]); await fixture.rm(); @@ -177,9 +177,9 @@ export default testSuite(({ describe }) => { const matcher = createPathsMatcher(tsconfig!)!; expect(matcher).not.toBeNull(); - const resolvedAttempts = await getTscResolution('exactMatch', fixture.path); + const resolved = await getTscResolution('exactMatch', fixture.path); expect(matcher('exactMatch')).toStrictEqual([ - resolvedAttempts[0].filePath.slice(0, -3), + resolved.attempts[0].filePath.slice(0, -3), ]); await fixture.rm(); @@ -203,9 +203,9 @@ export default testSuite(({ describe }) => { const matcher = createPathsMatcher(tsconfig!)!; expect(matcher).not.toBeNull(); - const resolvedAttempts = await getTscResolution('exactMatch', fixture.path); + const resolved = await getTscResolution('exactMatch', fixture.path); expect(matcher('exactMatch')).toStrictEqual([ - resolvedAttempts[0].filePath.slice(0, -3), + resolved.attempts[0].filePath.slice(0, -3), ]); await fixture.rm(); @@ -229,9 +229,9 @@ export default testSuite(({ describe }) => { const matcher = createPathsMatcher(tsconfig!)!; expect(tsconfig).not.toBeNull(); - const resolvedAttempts = await getTscResolution('exactMatch', fixture.path); + const resolved = await getTscResolution('exactMatch', fixture.path); expect(matcher('exactMatch')).toStrictEqual([ - resolvedAttempts[0].filePath.slice(0, -3), + resolved.attempts[0].filePath.slice(0, -3), ]); await fixture.rm(); @@ -255,9 +255,9 @@ export default testSuite(({ describe }) => { const matcher = createPathsMatcher(tsconfig!)!; expect(tsconfig).not.toBeNull(); - const resolvedAttempts = await getTscResolution('prefix-specifier', fixture.path); + const resolved = await getTscResolution('prefix-specifier', fixture.path); expect(matcher('prefix-specifier')).toStrictEqual([ - resolvedAttempts[0].filePath.slice(0, -3), + resolved.attempts[0].filePath.slice(0, -3), ]); await fixture.rm(); @@ -281,9 +281,9 @@ export default testSuite(({ describe }) => { const matcher = createPathsMatcher(tsconfig!)!; expect(tsconfig).not.toBeNull(); - const resolvedAttempts = await getTscResolution('specifier-suffix', fixture.path); + const resolved = await getTscResolution('specifier-suffix', fixture.path); expect(matcher('specifier-suffix')).toStrictEqual([ - resolvedAttempts[0].filePath.slice(0, -3), + resolved.attempts[0].filePath.slice(0, -3), ]); await fixture.rm(); @@ -372,9 +372,9 @@ export default testSuite(({ describe }) => { const matcher = createPathsMatcher(tsconfig!)!; expect(tsconfig).not.toBeNull(); - const resolvedAttempts = await getTscResolution('/absolute', fixture.path); + const resolved = await getTscResolution('/absolute', fixture.path); expect(matcher('/absolute')).toStrictEqual([ - resolvedAttempts[0].filePath.slice(0, -3), + resolved.attempts[0].filePath.slice(0, -3), ]); await fixture.rm(); @@ -397,9 +397,9 @@ export default testSuite(({ describe }) => { const matcher = createPathsMatcher(tsconfig!)!; expect(tsconfig).not.toBeNull(); - const resolvedAttempts = await getTscResolution('.src', fixture.path); + const resolved = await getTscResolution('.src', fixture.path); expect(matcher('.src')).toStrictEqual([ - resolvedAttempts[0].filePath.slice(0, -3), + resolved.attempts[0].filePath.slice(0, -3), ]); await fixture.rm(); diff --git a/tests/specs/resolver/resolve-directories-package-json.ts b/tests/specs/resolver/resolve-directories-package-json.ts index 591d9c48..9cd6328b 100644 --- a/tests/specs/resolver/resolve-directories-package-json.ts +++ b/tests/specs/resolver/resolve-directories-package-json.ts @@ -153,7 +153,7 @@ export default testSuite(({ describe }) => { await fixture.rm(); }); - + // shouldnt resolve export map }); From 99ca6d47b798a58d3f5e0c94fe233dba3db03400 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Thu, 11 Aug 2022 23:17:55 -0400 Subject: [PATCH 6/9] wip --- src/parse-tsconfig/resolve-extends.ts | 5 ++-- src/resolver.ts | 11 ++------- src/utils/read-jsonc.ts | 10 ++++++++ .../parse-tsconfig/extends/extends.spec.ts | 23 +++++++++++++++++++ 4 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 src/utils/read-jsonc.ts diff --git a/src/parse-tsconfig/resolve-extends.ts b/src/parse-tsconfig/resolve-extends.ts index 782e0cfb..a19c2edf 100644 --- a/src/parse-tsconfig/resolve-extends.ts +++ b/src/parse-tsconfig/resolve-extends.ts @@ -2,6 +2,7 @@ import path from 'path'; import fs from 'fs'; import Module from 'module'; import { findUp } from '../utils/find-up'; +import { readJsonc } from '../utils/read-jsonc'; const { existsSync } = fs; @@ -22,9 +23,7 @@ const getPnpApi = () => { }; function resolveFromPackageJsonPath(packageJsonPath: string) { - const packageJson = safeJsonParse( - fs.readFileSync(packageJsonPath, 'utf8'), - ); + const packageJson = readJsonc(packageJsonPath); return path.join( packageJsonPath, diff --git a/src/resolver.ts b/src/resolver.ts index 6b435263..51b81992 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -1,19 +1,12 @@ import fs from 'fs'; import path from 'path'; -import { parse } from 'jsonc-parser'; import type { PackageJson } from 'type-fest'; import { resolve as resolveExports } from 'resolve.exports'; import { findUp } from './utils/find-up'; +import { readJsonc } from './utils/read-jsonc'; import { createPathsMatcher, type PathsMatcher } from './paths-matcher/index'; import type { TsConfigResult } from './types'; -function readJson( - api: Pick, - jsonPath: string, -) { - const packageJsonString = api.readFileSync(jsonPath, 'utf8'); - return parse(packageJsonString); -} const stripExtensionPattern = /\.([mc]js|jsx?)$/; @@ -46,7 +39,7 @@ function getPackageEntry( return undefined; } - const packageJson = readJson(api, packageJsonPath) as PackageJson; + const packageJson = readJsonc(packageJsonPath, api) as PackageJson; if (!packageJson || typeof packageJson !== 'object') { return undefined; } diff --git a/src/utils/read-jsonc.ts b/src/utils/read-jsonc.ts new file mode 100644 index 00000000..389e4923 --- /dev/null +++ b/src/utils/read-jsonc.ts @@ -0,0 +1,10 @@ +import fs from 'fs'; +import { parse } from 'jsonc-parser'; + +export function readJsonc( + jsonPath: string, + api: Pick = fs, +) { + const packageJsonString = api.readFileSync(jsonPath, 'utf8'); + return parse(packageJsonString); +} diff --git a/tests/specs/parse-tsconfig/extends/extends.spec.ts b/tests/specs/parse-tsconfig/extends/extends.spec.ts index 2252e87b..fe4910ae 100644 --- a/tests/specs/parse-tsconfig/extends/extends.spec.ts +++ b/tests/specs/parse-tsconfig/extends/extends.spec.ts @@ -424,6 +424,29 @@ export default testSuite(({ describe }) => { await fixture.rm(); }); + test('jsonc', async () => { + const fixture = await createFixture({ + 'file.ts': '', + 'tsconfig.base.json': `{ + // comment + "compilerOptions": { + "jsx": "react", // dangling comma + }, + }`, + 'tsconfig.json': tsconfigJson({ + extends: './tsconfig.base.json', + }), + }); + + const expectedTsconfig = await getTscTsconfig(fixture.path); + delete expectedTsconfig.files; + + const tsconfig = getTsconfig(fixture.path); + expect(tsconfig!.config).toStrictEqual(expectedTsconfig); + + await fixture.rm(); + }); + test('references is ignored', async () => { const fixture = await createFixture({ 'tsconfig.base.json': tsconfigJson({ From 971f387df15932dc31b57ff3ac17f067cdc71a8e Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Fri, 12 Aug 2022 12:18:12 -0400 Subject: [PATCH 7/9] wip --- src/resolver.ts | 12 +-- src/utils/find-up.ts | 2 +- .../parse-tsconfig/extends/extends.spec.ts | 2 +- tests/specs/resolver/index.ts | 6 +- tests/specs/resolver/resolve-cjs-mjs.ts | 62 ++++++------- .../resolve-directories-package-json.ts | 56 ++++++------ tests/specs/resolver/resolve-directories.ts | 30 +++---- tests/specs/resolver/resolve-js-jsx.ts | 86 +++++++++---------- tests/utils.ts | 2 +- 9 files changed, 125 insertions(+), 133 deletions(-) diff --git a/src/resolver.ts b/src/resolver.ts index 51b81992..9d7c18b7 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -7,7 +7,6 @@ import { readJsonc } from './utils/read-jsonc'; import { createPathsMatcher, type PathsMatcher } from './paths-matcher/index'; import type { TsConfigResult } from './types'; - const stripExtensionPattern = /\.([mc]js|jsx?)$/; type FsAPI = Pick; @@ -77,7 +76,6 @@ function resolve( const stat = safeStat(api, request); if (stat && stat.isDirectory()) { - // Check if package.json#main exists const hasMain = getPackageEntry(request, api); if (hasMain) { @@ -149,11 +147,10 @@ function resolveBareSpecifier( const possiblePaths = pathsResolver(request); for (const possiblePath of possiblePaths) { - /** * If a path resolves to a package, * would it resolve the export maps? - * + * * Also, what if we resolve a package path * by absolute path? Would it resolve the export map? * Or does it need to be resolved by bare specifier? @@ -167,7 +164,7 @@ function resolveBareSpecifier( /* 1. Find all node_module parent directories - 2. + 2. */ @@ -214,12 +211,11 @@ export function createResolver( request: string, context: string, ): string | undefined { - // Resolve relative specifier if (request.startsWith('.')) { request = path.join(context, request); } - + // Absolute specifier if (request.startsWith('/')) { return resolveSymlink(resolveExtensionlessPath(request, api)); @@ -235,4 +231,4 @@ export function createResolver( ), ); }; -} \ No newline at end of file +} diff --git a/src/utils/find-up.ts b/src/utils/find-up.ts index 00b728e0..ecb31f75 100644 --- a/src/utils/find-up.ts +++ b/src/utils/find-up.ts @@ -33,7 +33,7 @@ function findUp( const foundPath = slash(configPath); if (findAll) { - foundPaths.push(foundPath); + foundPaths.push(foundPath); } else { return foundPath; } diff --git a/tests/specs/parse-tsconfig/extends/extends.spec.ts b/tests/specs/parse-tsconfig/extends/extends.spec.ts index c3c90440..d46e06ad 100644 --- a/tests/specs/parse-tsconfig/extends/extends.spec.ts +++ b/tests/specs/parse-tsconfig/extends/extends.spec.ts @@ -451,7 +451,7 @@ export default testSuite(({ describe }) => { await fixture.rm(); }); - + test('references is ignored', async () => { const fixture = await createFixture({ 'tsconfig.base.json': tsconfigJson({ diff --git a/tests/specs/resolver/index.ts b/tests/specs/resolver/index.ts index a76f7f78..45f57e95 100644 --- a/tests/specs/resolver/index.ts +++ b/tests/specs/resolver/index.ts @@ -1,8 +1,4 @@ -import fs from 'fs'; -import path from 'path'; -import { testSuite, expect } from 'manten'; -import { createFixture } from 'fs-fixture'; -import { getTscResolution } from '../../utils'; +import { testSuite } from 'manten'; export default testSuite(({ describe }) => { describe('resolver', async ({ test, describe, runTestSuite }) => { diff --git a/tests/specs/resolver/resolve-cjs-mjs.ts b/tests/specs/resolver/resolve-cjs-mjs.ts index d0cc5235..a01bf486 100644 --- a/tests/specs/resolver/resolve-cjs-mjs.ts +++ b/tests/specs/resolver/resolve-cjs-mjs.ts @@ -11,102 +11,102 @@ export default testSuite(({ describe }) => { 'tsconfig.json': '', 'some-file.mjs.ts': '', }); - + const request = './some-file.mjs'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.mjs.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('resolve .cjs -> .cjs.tsx', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.cjs.tsx': '', }); - + const request = './some-file.cjs'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.cjs.tsx')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); }); - + describe('resolves TS counter part', ({ test }) => { test('.mjs -> .mts', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.mts': '', }); - + const request = './some-file.mjs'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.mts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.cjs -> .cts', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.cts': '', }); - + const request = './some-file.cjs'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.cts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); }); - + describe('resolves as JS', ({ test }) => { test('.mjs', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.mjs': '', }); - + const request = './some-file.mjs'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.mjs')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.cjs', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.cjs': '', }); - + const request = './some-file.cjs'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.cjs')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); }); - + describe('combination', ({ test }) => { test('.mts over .mjs', async () => { const fixture = await createFixture({ @@ -114,31 +114,31 @@ export default testSuite(({ describe }) => { 'some-file.mjs': '', 'some-file.mts': '', }); - + const request = './some-file.mjs'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.mts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.mjs.jsx over .mjs', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.mjs.jsx': '', 'some-file.mjs': '', }); - + const request = './some-file.mjs'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.mjs.jsx')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); }); diff --git a/tests/specs/resolver/resolve-directories-package-json.ts b/tests/specs/resolver/resolve-directories-package-json.ts index 9cd6328b..04bbe118 100644 --- a/tests/specs/resolver/resolve-directories-package-json.ts +++ b/tests/specs/resolver/resolve-directories-package-json.ts @@ -16,17 +16,17 @@ export default testSuite(({ describe }) => { 'file.ts': '', }, }); - + const request = './directory'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/file.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.js -> .js', async () => { const fixture = await createFixture({ 'tsconfig.json': '', @@ -37,17 +37,17 @@ export default testSuite(({ describe }) => { 'main.js': '', }, }); - + const request = './directory'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/main.js')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.js -> ts', async () => { const fixture = await createFixture({ 'tsconfig.json': '', @@ -58,17 +58,17 @@ export default testSuite(({ describe }) => { 'main.ts': '', }, }); - + const request = './directory'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/main.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.js -> tsx', async () => { const fixture = await createFixture({ 'tsconfig.json': '', @@ -79,17 +79,17 @@ export default testSuite(({ describe }) => { 'main.tsx': '', }, }); - + const request = './directory'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/main.tsx')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('nonexistent main fallback to index', async () => { const fixture = await createFixture({ 'tsconfig.json': '', @@ -100,17 +100,17 @@ export default testSuite(({ describe }) => { 'index.tsx': '', }, }); - + const request = './directory'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/index.tsx')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('relative path', async () => { const fixture = await createFixture({ 'tsconfig.json': '', @@ -121,17 +121,17 @@ export default testSuite(({ describe }) => { }), }, }); - + const request = './directory'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/index.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('package.json to be parsed as JSONC', async () => { const fixture = await createFixture({ 'tsconfig.json': '', @@ -143,20 +143,20 @@ export default testSuite(({ describe }) => { }`, }, }); - + const request = './directory'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/file.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); // shouldnt resolve export map }); - + // resolve export map }); }); diff --git a/tests/specs/resolver/resolve-directories.ts b/tests/specs/resolver/resolve-directories.ts index 6721a5b3..676ca3f6 100644 --- a/tests/specs/resolver/resolve-directories.ts +++ b/tests/specs/resolver/resolve-directories.ts @@ -10,62 +10,62 @@ export default testSuite(({ describe }) => { 'tsconfig.json': '', 'directory/index.ts': '', }); - + const request = './directory'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/index.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('directory -> directory/index.tsx', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'directory/index.tsx': '', }); - + const request = './directory'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/index.tsx')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('directory -> directory/index.js', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'directory/index.js': '', }); - + const request = './directory'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/index.js')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('directory -> directory/index.jsx', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'directory/index.jsx': '', }); - + const request = './directory'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/index.jsx')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); }); diff --git a/tests/specs/resolver/resolve-js-jsx.ts b/tests/specs/resolver/resolve-js-jsx.ts index bd692ff1..f42e6c47 100644 --- a/tests/specs/resolver/resolve-js-jsx.ts +++ b/tests/specs/resolver/resolve-js-jsx.ts @@ -11,134 +11,134 @@ export default testSuite(({ describe }) => { 'tsconfig.json': '', 'some-file.js.ts': '', }); - + const request = './some-file.js'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.js.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('resolve .js -> .js.tsx', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.js.tsx': '', }); - + const request = './some-file.js'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.js.tsx')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); }); - + describe('resolves TS counter part', ({ test }) => { test('.js -> .ts', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.ts': '', }); - + const request = './some-file.js'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.jsx -> .ts', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.ts': '', }); - + const request = './some-file.jsx'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.js -> .tsx', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.tsx': '', }); - + const request = './some-file.js'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.tsx')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.jsx -> .tsx', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.tsx': '', }); - + const request = './some-file.jsx'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.tsx')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); }); - + describe('resolves as JS', ({ test }) => { test('.js', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.js': '', }); - + const request = './some-file.js'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.js')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.jsx', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.jsx': '', }); - + const request = './some-file.jsx'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.jsx')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); }); - + describe('combination', ({ test }) => { test('.js.ts over .ts', async () => { const fixture = await createFixture({ @@ -146,48 +146,48 @@ export default testSuite(({ describe }) => { 'some-file.js.ts': '', 'some-file.ts': '', }); - + const request = './some-file.js'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.js.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.ts over .js', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.js': '', 'some-file.ts': '', }); - + const request = './some-file.js'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); - + test('.ts over .tsx', async () => { const fixture = await createFixture({ 'tsconfig.json': '', 'some-file.ts': '', 'some-file.tsx': '', }); - + const request = './some-file.js'; const resolved = createResolver()(request, fixture.path); const tsResolved = await getTscResolution(request, fixture.path); - + expect(resolved?.endsWith('/some-file.ts')).toBeTruthy(); expect(resolved).toBe(tsResolved.resolved); - + await fixture.rm(); }); }); diff --git a/tests/utils.ts b/tests/utils.ts index 6032c790..f8bd2fba 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -42,7 +42,7 @@ async function parseTscResolve( stdout.indexOf(`${divider} Resolving module '${request}'`), ) + 1; - const logEndIndex = stdout.indexOf(divider + ' Module name ', logStartIndex); + const logEndIndex = stdout.indexOf(`${divider} Module name `, logStartIndex); const resolvedToStartIndex = logEndIndex + divider.length; const resolvedToEndIndex = stdout.indexOf(divider, resolvedToStartIndex); From 6e0f90849d2c6d44329ac909b8039d0e919c4792 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Wed, 24 Aug 2022 17:25:32 -0400 Subject: [PATCH 8/9] wip --- src/parse-tsconfig/index.ts | 2 +- src/parse-tsconfig/resolve-extends.ts | 4 +- src/resolver.ts | 234 --------- src/resolver/index.ts | 49 ++ src/resolver/resolve-bare-specifier.ts | 215 ++++++++ src/resolver/resolve-path-extension.ts | 125 +++++ src/resolver/types.ts | 3 + src/utils/parse-package-name.ts | 14 + src/utils/read-jsonc.ts | 9 +- tests/index.ts | 6 +- tests/specs/resolver/index.ts | 16 +- tests/specs/resolver/resolve-dependency.ts | 472 ++++++++++++++++++ .../resolve-directories-package-json.ts | 24 +- 13 files changed, 921 insertions(+), 252 deletions(-) delete mode 100644 src/resolver.ts create mode 100644 src/resolver/index.ts create mode 100644 src/resolver/resolve-bare-specifier.ts create mode 100644 src/resolver/resolve-path-extension.ts create mode 100644 src/resolver/types.ts create mode 100644 src/utils/parse-package-name.ts create mode 100644 tests/specs/resolver/resolve-dependency.ts diff --git a/src/parse-tsconfig/index.ts b/src/parse-tsconfig/index.ts index 34935012..99123e8e 100644 --- a/src/parse-tsconfig/index.ts +++ b/src/parse-tsconfig/index.ts @@ -16,7 +16,7 @@ export function parseTsconfig( throw new Error(`Cannot resolve tsconfig at path: ${tsconfigPath}`); } const directoryPath = path.dirname(realTsconfigPath); - let config: TsConfigJson = readJsonc(realTsconfigPath) || {}; + let config = readJsonc(realTsconfigPath) || {}; if (typeof config !== 'object') { throw new SyntaxError(`Failed to parse tsconfig at: ${tsconfigPath}`); diff --git a/src/parse-tsconfig/resolve-extends.ts b/src/parse-tsconfig/resolve-extends.ts index 43a5190f..60d2cd47 100644 --- a/src/parse-tsconfig/resolve-extends.ts +++ b/src/parse-tsconfig/resolve-extends.ts @@ -3,6 +3,7 @@ import fs from 'fs'; import Module from 'module'; import { findUp } from '../utils/find-up'; import { readJsonc } from '../utils/read-jsonc'; +import { parsePackageName } from '../utils/parse-package-name'; const { existsSync } = fs; @@ -60,8 +61,7 @@ export function resolveExtends( const pnpApi = getPnpApi(); if (pnpApi) { const { resolveRequest } = pnpApi; - const [first, second] = filePath.split('/'); - const packageName = first.startsWith('@') ? `${first}/${second}` : first; + const { packageName } = parsePackageName(filePath); try { if (packageName === filePath) { diff --git a/src/resolver.ts b/src/resolver.ts deleted file mode 100644 index 9d7c18b7..00000000 --- a/src/resolver.ts +++ /dev/null @@ -1,234 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import type { PackageJson } from 'type-fest'; -import { resolve as resolveExports } from 'resolve.exports'; -import { findUp } from './utils/find-up'; -import { readJsonc } from './utils/read-jsonc'; -import { createPathsMatcher, type PathsMatcher } from './paths-matcher/index'; -import type { TsConfigResult } from './types'; - -const stripExtensionPattern = /\.([mc]js|jsx?)$/; - -type FsAPI = Pick; - -const safeStat = ( - api: FsAPI, - request: string, -) => (api.existsSync(request) && api.statSync(request)); - -function tryExtensions( - request: string, - api: FsAPI, - extensions: string[], -) { - for (const extension of extensions) { - const checkPath = request + extension; - if (api.existsSync(checkPath)) { - return checkPath; - } - } -} - -function getPackageEntry( - request: string, - api: FsAPI, -) { - const packageJsonPath = `${request}/package.json`; - if (!api.existsSync(packageJsonPath)) { - return undefined; - } - - const packageJson = readJsonc(packageJsonPath, api) as PackageJson; - if (!packageJson || typeof packageJson !== 'object') { - return undefined; - } - - return packageJson.main; -} - -function resolve( - request: string, - api: FsAPI, - extensions: string[], - nextGenExtensions: Record, -): string | undefined { - let resolved = tryExtensions(request, api, extensions); - if (resolved) { - return resolved; - } - - // If it has .js, strip it off and try again from start - const hasExtension = request.match(stripExtensionPattern); - if (hasExtension) { - resolved = tryExtensions( - request.slice(0, hasExtension.index), - api, - ( - nextGenExtensions[hasExtension[1] as string] - || extensions - ), - ); - - if (resolved) { - return resolved; - } - } - - const stat = safeStat(api, request); - if (stat && stat.isDirectory()) { - // Check if package.json#main exists - const hasMain = getPackageEntry(request, api); - if (hasMain) { - const mainPath = path.join(request, hasMain); - const mainStat = safeStat(api, mainPath); - if (mainStat && mainStat.isFile()) { - return mainPath; - } - - resolved = resolve(mainPath, api, extensions, nextGenExtensions); - - if (resolved) { - return resolved; - } - } - - // Fallback to index if main path doesnt exist - resolved = tryExtensions(path.join(request, 'index'), api, extensions); - - if (resolved) { - return resolved; - } - } -} - -function resolveExtensionlessPath( - request: string, - api: FsAPI, -) { - // Try resolving in TypeScript mode - let resolved = resolve( - request, - api, - ['.ts', '.tsx'], - { - mjs: ['.mts'], - cjs: ['.cts'], - }, - ); - - if (resolved) { - return resolved; - } - - // Try resolving in JavaScript mode - resolved = resolve( - request, - api, - ['.js', '.jsx'], - { - mjs: ['.mjs'], - cjs: ['.cjs'], - }, - ); - - if (resolved) { - return resolved; - } -} - -function resolveBareSpecifier( - request: string, - context: string, - pathsResolver: PathsMatcher | null | undefined, - api: FsAPI, -) { - // Try tsconfig.paths - if (pathsResolver) { - const possiblePaths = pathsResolver(request); - - for (const possiblePath of possiblePaths) { - /** - * If a path resolves to a package, - * would it resolve the export maps? - * - * Also, what if we resolve a package path - * by absolute path? Would it resolve the export map? - * Or does it need to be resolved by bare specifier? - */ - const resolved = resolveExtensionlessPath(possiblePath, api); - if (resolved) { - return resolved; - } - } - } - - /* - 1. Find all node_module parent directories - 2. - - */ - - const nodeModuleDirectories = findUp(context, 'node_modules', true, api); - for (const nodeModuleDirectory of nodeModuleDirectories) { - const dependencyPath = path.join(nodeModuleDirectory, request); - const packageJsonPath = path.join(dependencyPath, 'package.json'); - - if (api.existsSync(packageJsonPath)) { - // test missing path in main - const hasMain = getPackageEntry(dependencyPath, api); - if (hasMain) { - const resolved = resolveExtensionlessPath(path.join(dependencyPath, hasMain), api); - if (resolved) { - return resolved; - } - } - } else { - console.log('package.json not found', packageJsonPath); - } - - const resolved = resolveExtensionlessPath(dependencyPath, api); - if (resolved) { - return resolved; - } - } -} - -export function createResolver( - tsconfig?: TsConfigResult, - api: FsAPI = fs, -) { - const preserveSymlinks = tsconfig?.config.compilerOptions?.preserveSymlinks; - const pathsResolver = tsconfig && createPathsMatcher(tsconfig); - - // TODO: include Node.js's --preserve-symlinks - const resolveSymlink = (path: string | undefined) => ( - (!path || preserveSymlinks) - ? path - : api.realpathSync(path) - ); - - return function tsResolve( - request: string, - context: string, - ): string | undefined { - // Resolve relative specifier - if (request.startsWith('.')) { - request = path.join(context, request); - } - - // Absolute specifier - if (request.startsWith('/')) { - return resolveSymlink(resolveExtensionlessPath(request, api)); - } - - // Resolve bare specifier - return resolveSymlink( - resolveBareSpecifier( - request, - context, - pathsResolver, - api, - ), - ); - }; -} diff --git a/src/resolver/index.ts b/src/resolver/index.ts new file mode 100644 index 00000000..734facc3 --- /dev/null +++ b/src/resolver/index.ts @@ -0,0 +1,49 @@ +import fs from 'fs'; +import path from 'path'; +import { createPathsMatcher } from '../paths-matcher/index'; +import type { TsConfigResult } from '../types'; +import { resolveBareSpecifier } from './resolve-bare-specifier'; +import type { FsAPI } from './types'; +import { resolvePathExtension } from './resolve-path-extension'; + +export function createResolver( + tsconfig?: TsConfigResult, + api: FsAPI = fs, +) { + const preserveSymlinks = tsconfig?.config.compilerOptions?.preserveSymlinks; + const pathsResolver = tsconfig && createPathsMatcher(tsconfig); + + // TODO: include Node.js's --preserve-symlinks + const resolveSymlink = (path: string | undefined) => ( + (!path || preserveSymlinks) + ? path + : api.realpathSync(path) + ); + + return function resolver( + request: string, + context: string, + conditions?: string[], + ): string | undefined { + // Resolve relative specifier + if (request.startsWith('.')) { + request = path.join(context, request); + } + + // Absolute specifier + if (request.startsWith('/')) { + return resolveSymlink(resolvePathExtension(request, api)); + } + + // Resolve bare specifier + return resolveSymlink( + resolveBareSpecifier( + request, + context, + conditions, + pathsResolver, + api, + ), + ); + }; +} diff --git a/src/resolver/resolve-bare-specifier.ts b/src/resolver/resolve-bare-specifier.ts new file mode 100644 index 00000000..f22f0300 --- /dev/null +++ b/src/resolver/resolve-bare-specifier.ts @@ -0,0 +1,215 @@ +import path from 'path'; +import type { PackageJson } from 'type-fest'; +import { type PathsMatcher } from '../paths-matcher/index'; +import { findUp } from '../utils/find-up'; +import { parsePackageName } from '../utils/parse-package-name'; +import { readJsonc } from '../utils/read-jsonc'; +import type { FsAPI } from './types'; +import { resolvePathExtension } from './resolve-path-extension'; + + +// Export maps +// https://github.com/microsoft/TypeScript/blob/71e852922888337ef51a0e48416034a94a6c34d9/src/compiler/moduleSpecifiers.ts#L663 + +// Import maps +//https://github.com/microsoft/TypeScript/blob/71e852922888337ef51a0e48416034a94a6c34d9/src/compiler/moduleNameResolver.ts#L2101 + + +type ExportCondition = + | 'import' + | 'require' + | 'node' + | 'node-addons' + | 'deno' + | 'browser' + | 'electron' + | 'react-native' + | 'default'; + +/** +Entry points of a module, optionally with conditions and subpath exports. +*/ +type Exports = +| null +| string +| string[] +| {[key in ExportCondition]: Exports} +| {[key: string]: Exports}; // eslint-disable-line @typescript-eslint/consistent-indexed-object-style + + +function resolveWithExportMap( + request: string, + exports: Exports, + conditions: string[], + asterisk?: string, +): string | string[] | undefined { + + console.log('resolveWithExportMap', { + request, exports, conditions, asterisk, + }); + + if (typeof exports === 'string') { + if (asterisk) { + return exports.replaceAll('*', asterisk); + } else { + return exports; + } + } + + else if (Array.isArray(exports)) { + return exports.map( + exportPath => resolveWithExportMap( + request, + exportPath, + conditions, + asterisk, + ) as string, + ); + } + + else if ( + typeof exports === 'object' + && exports !== null + ) { + const objectKeys = Object.keys(exports) as (keyof typeof exports)[]; + const isPathsObject = objectKeys.every(key => key.startsWith('.')); + + if (isPathsObject) { + console.log({ isPathsObject, exports, request }); + + for (const key of objectKeys) { + console.log({ key }); + if (key.includes('*')) { + const [prefix, suffix] = key.split('*'); + + // Can * span multiple paths / ? + if (request.startsWith(prefix) && request.endsWith(suffix)) { + const matched = request.slice(prefix.length, -suffix.length || undefined); + + return resolveWithExportMap( + request, + exports[key], + conditions, + matched, + ); + } + } else { + if (key === request) { + return resolveWithExportMap( + request, + exports[key], + conditions, + ); + } + } + } + } else { + + console.log('conditions'); + } + + } +} + + +export function resolveBareSpecifier( + request: string, + context: string, + conditions: string[] | undefined, + pathsResolver: PathsMatcher | null | undefined, + api: FsAPI, +) { + // Try tsconfig.paths + if (pathsResolver) { + const possiblePaths = pathsResolver(request); + + for (const possiblePath of possiblePaths) { + /** + * If a path resolves to a package, + * would it resolve the export maps? + * + * Also, what if we resolve a package path + * by absolute path? Would it resolve the export map? + * Or does it need to be resolved by bare specifier? + */ + const resolved = resolvePathExtension(possiblePath, api); + if (resolved) { + return resolved; + } + } + } + + /* + 1. Find all node_module parent directories + 2. parse the package to find package directory (eg. dep/a -> dep) + 3. check exports map and check path against it + */ + const nodeModuleDirectories = findUp(context, 'node_modules', true, api); + const { packageName, packageSubpath } = parsePackageName(request); + + for (const nodeModuleDirectory of nodeModuleDirectories) { + const dependencyPath = path.join(nodeModuleDirectory, packageName); + const packageJsonPath = path.join(dependencyPath, 'package.json'); + + if (api.existsSync(packageJsonPath)) { + const packageJson = readJsonc(packageJsonPath, api); + + if (packageJson) { + const { exports } = packageJson + if (exports) { + // TODO: merge with below later + if (!packageSubpath && typeof exports === 'string') { + const resolved = resolvePathExtension( + path.join(dependencyPath, exports), + api, + ); + if (resolved) { + return resolved; + } + } + + // Check exports main sugar + const resolvedSubPath = resolveWithExportMap( + packageSubpath ? './' + packageSubpath : '.', + exports, + conditions || [], + ); + + console.log({ packageSubpath, resolvedSubPath }); + if (resolvedSubPath) { + const resolved = resolvePathExtension( + path.join(dependencyPath, resolvedSubPath), + api, + ); + + if (resolved) { + return resolved; + } + } + + // If not in export maps, dont allow lookups + continue; + } + + if (!packageSubpath && packageJson.main) { + const resolved = resolvePathExtension( + path.join(dependencyPath, packageJson.main), + api, + ); + if (resolved) { + return resolved; + } + } + } + } + + // Also resolves subpaths if packgae.json#main or #exports doesnt exist + const resolved = resolvePathExtension( + path.join(nodeModuleDirectory, request), + api, + ); + if (resolved) { + return resolved; + } + } +} diff --git a/src/resolver/resolve-path-extension.ts b/src/resolver/resolve-path-extension.ts new file mode 100644 index 00000000..ae1a826b --- /dev/null +++ b/src/resolver/resolve-path-extension.ts @@ -0,0 +1,125 @@ +import path from 'path'; +import type { PackageJson } from 'type-fest'; +import { readJsonc } from '../utils/read-jsonc'; +import type { FsAPI } from './types'; + +const stripExtensionPattern = /\.([mc]js|jsx?)$/; + +const safeStat = ( + api: FsAPI, + request: string, +) => (api.existsSync(request) && api.statSync(request)); + +function tryExtensions( + request: string, + api: FsAPI, + extensions: string[], +) { + for (const extension of extensions) { + const checkPath = request + extension; + if (api.existsSync(checkPath)) { + return checkPath; + } + } +} + +function getPackageEntry( + request: string, + api: FsAPI, +) { + const packageJsonPath = `${request}/package.json`; + if (api.existsSync(packageJsonPath)) { + const packageJson = readJsonc(packageJsonPath, api); + return packageJson?.main; + } +} + +function resolve( + request: string, + api: FsAPI, + extensions: string[], + nextGenExtensions: Record, +): string | undefined { + let resolved = tryExtensions(request, api, extensions); + if (resolved) { + return resolved; + } + + // If it has .js, strip it off and try again from start + const hasExtension = request.match(stripExtensionPattern); + if (hasExtension) { + resolved = tryExtensions( + request.slice(0, hasExtension.index), + api, + ( + nextGenExtensions[hasExtension[1] as string] + || extensions + ), + ); + + if (resolved) { + return resolved; + } + } + + const stat = safeStat(api, request); + if (stat && stat.isDirectory()) { + // Check if package.json#main exists + const hasMain = getPackageEntry(request, api); + if (hasMain) { + const mainPath = path.join(request, hasMain); + const mainStat = safeStat(api, mainPath); + if (mainStat && mainStat.isFile()) { + return mainPath; + } + + resolved = resolve(mainPath, api, extensions, nextGenExtensions); + + if (resolved) { + return resolved; + } + } + + // Fallback to index if main path doesnt exist + resolved = tryExtensions(path.join(request, 'index'), api, extensions); + + if (resolved) { + return resolved; + } + } +} + +export function resolvePathExtension( + request: string, + api: FsAPI, +) { + // Try resolving in TypeScript mode + let resolved = resolve( + request, + api, + ['.ts', '.tsx'], + { + mjs: ['.mts'], + cjs: ['.cts'], + }, + ); + + if (resolved) { + return resolved; + } + + // Try resolving in JavaScript mode + resolved = resolve( + request, + api, + ['.js', '.jsx'], + { + mjs: ['.mjs'], + cjs: ['.cjs'], + }, + ); + + if (resolved) { + return resolved; + } +} diff --git a/src/resolver/types.ts b/src/resolver/types.ts new file mode 100644 index 00000000..de62da0b --- /dev/null +++ b/src/resolver/types.ts @@ -0,0 +1,3 @@ +import fs from 'fs'; + +export type FsAPI = Pick; diff --git a/src/utils/parse-package-name.ts b/src/utils/parse-package-name.ts new file mode 100644 index 00000000..75ee2bc8 --- /dev/null +++ b/src/utils/parse-package-name.ts @@ -0,0 +1,14 @@ +export function parsePackageName( + request: string, +) { + const segments = request.split('/'); + let packageName = segments.shift()!; + if (packageName[0] === '@' && segments[0]) { + packageName += `/${segments.shift()}`; + } + + return { + packageName, + packageSubpath: segments.join('/'), + }; +} diff --git a/src/utils/read-jsonc.ts b/src/utils/read-jsonc.ts index 1057f21a..2873ca1c 100644 --- a/src/utils/read-jsonc.ts +++ b/src/utils/read-jsonc.ts @@ -1,7 +1,12 @@ import fs from 'fs'; import { parse } from 'jsonc-parser'; -export const readJsonc = ( +export const readJsonc = ( jsonPath: string, api: Pick = fs, -) => parse(api.readFileSync(jsonPath, 'utf8')); +): T | undefined => { + const parsed = parse(api.readFileSync(jsonPath, 'utf8')); + if (parsed && typeof parsed === 'object') { + return parsed; + } +}; diff --git a/tests/index.ts b/tests/index.ts index 6a41ab03..e713d031 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,8 +1,8 @@ import { describe } from 'manten'; describe('get-tsconfig', ({ runTestSuite }) => { - runTestSuite(import('./specs/get-tsconfig.js')); - runTestSuite(import('./specs/parse-tsconfig/index.js')); - runTestSuite(import('./specs/create-paths-matcher.js')); + // runTestSuite(import('./specs/get-tsconfig.js')); + // runTestSuite(import('./specs/parse-tsconfig/index.js')); + // runTestSuite(import('./specs/create-paths-matcher.js')); runTestSuite(import('./specs/resolver/index.js')); }); diff --git a/tests/specs/resolver/index.ts b/tests/specs/resolver/index.ts index 45f57e95..e0949812 100644 --- a/tests/specs/resolver/index.ts +++ b/tests/specs/resolver/index.ts @@ -1,12 +1,14 @@ import { testSuite } from 'manten'; export default testSuite(({ describe }) => { - describe('resolver', async ({ test, describe, runTestSuite }) => { - runTestSuite(import('./error-cases.js')); - runTestSuite(import('./resolve-extensionless.js')); - runTestSuite(import('./resolve-js-jsx.js')); - runTestSuite(import('./resolve-cjs-mjs.js')); - runTestSuite(import('./resolve-directories.js')); - runTestSuite(import('./resolve-directories-package-json.js')); + describe('resolver', async ({ runTestSuite }) => { + // runTestSuite(import('./error-cases.js')); + // runTestSuite(import('./resolve-extensionless.js')); + // runTestSuite(import('./resolve-js-jsx.js')); + // runTestSuite(import('./resolve-cjs-mjs.js')); + // runTestSuite(import('./resolve-directories.js')); + // runTestSuite(import('./resolve-directories-package-json.js')); + runTestSuite(import('./resolve-dependency.js')); + // resolves tsconfig.paths }); }); diff --git a/tests/specs/resolver/resolve-dependency.ts b/tests/specs/resolver/resolve-dependency.ts new file mode 100644 index 00000000..c6a42b18 --- /dev/null +++ b/tests/specs/resolver/resolve-dependency.ts @@ -0,0 +1,472 @@ +import path from 'path'; +import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; +import { getTscResolution } from '../../utils'; +import { createResolver } from '#get-tsconfig'; + +export default testSuite(({ describe }) => { + describe('dependencies', ({ test, describe }) => { + // describe('finds package', ({ test }) => { + // test('index.js', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/some-dependency': { + // 'package.json': JSON.stringify({ + // name: 'some-dependency', + // }), + // 'index.js': '', + // }, + // }); + + // const request = 'some-dependency'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/index.js')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('@org', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/@org.js': '', + // }); + + // const request = '@org'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/@org.js')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('@org/package', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/@org/package.js': '', + // }); + + // const request = '@org/package'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // // console.log({ resolved, tsResolved }); + + // expect(resolved?.endsWith('/@org/package.js')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('no package.json', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/some-dependency': { + // 'index.js': '', + // }, + // }); + + // const request = 'some-dependency'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/index.js')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('no directory', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/some-dependency.js': '', + // }); + + // const request = 'some-dependency'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/some-dependency.js')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('nested', async () => { + // const fixture = await createFixture({ + // 'a/b/c/d': { + // 'tsconfig.json': '', + // }, + // 'node_modules/some-dependency': { + // 'index.tsx': '', + // }, + // }); + + // const request = 'some-dependency'; + // const projectPath = path.join(fixture.path, 'a/b/c/d'); + // const resolved = createResolver()(request, projectPath); + // const tsResolved = await getTscResolution(request, projectPath); + + // expect(resolved?.endsWith('/some-dependency/index.tsx')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + // }); + + // describe('main', ({ test }) => { + // test('.ts -> .ts', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/dep': { + // 'package.json': JSON.stringify({ + // main: 'file.ts', + // }), + // 'file.ts': '', + // }, + // }); + + // const request = 'dep'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('.js -> .js', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/dep': { + // 'package.json': JSON.stringify({ + // main: 'file.js', + // }), + // 'file.js': '', + // }, + // }); + + // const request = 'dep'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/file.js')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('.js -> ts', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/dep': { + // 'package.json': JSON.stringify({ + // main: 'file.js', + // }), + // 'file.ts': '', + // }, + // }); + + // const request = 'dep'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('.js -> tsx', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/dep': { + // 'package.json': JSON.stringify({ + // main: 'file.js', + // }), + // 'file.tsx': '', + // }, + // }); + + // const request = 'dep'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/file.tsx')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('nonexistent main fallback to index', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/dep': { + // 'package.json': JSON.stringify({ + // main: 'non-existent', + // }), + // 'index.js': '', + // }, + // }); + + // const request = 'dep'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/index.js')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('relative path', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'index.ts': '', + // 'node_modules/dep': { + // 'package.json': JSON.stringify({ + // main: '../..', + // }), + // }, + // }); + + // const request = 'dep'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/index.ts')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('@scoped: .js -> ts', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/@org/dep': { + // 'package.json': JSON.stringify({ + // main: 'file.js', + // }), + // 'file.ts': '', + // }, + // }); + + // const request = '@org/dep'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + // }); + + describe('subpaths', ({ describe, test }) => { + // test('no package.json', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/dep': { + // 'file.ts': '', + // }, + // }); + + // const request = 'dep/file'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('package.json', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/dep': { + // 'package.json': JSON.stringify({}), + // 'file.js': '', + // }, + // }); + + // const request = 'dep/file.js'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/file.js')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('.js -> .ts', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/dep': { + // 'package.json': JSON.stringify({}), + // 'file.ts': '', + // }, + // }); + + // const request = 'dep/file.js'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('.mjs -> .mts', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': '', + // 'node_modules/dep': { + // 'package.json': JSON.stringify({}), + // 'file.mts': '', + // }, + // }); + + // const request = 'dep/file.mjs'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/file.mts')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + describe('export map', ({ test }) => { + // test('main', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': JSON.stringify({ + // compilerOptions: { + // moduleResolution: 'Node16', + // }, + // }), + // 'node_modules/dep': { + // 'package.json': JSON.stringify({ + // exports: './file.js', + // }), + // 'file.js': '', + // }, + // }); + + // const request = 'dep'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(resolved?.endsWith('/file.js')).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('main - should not work without ./', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': JSON.stringify({ + // compilerOptions: { + // moduleResolution: 'Node16', + // }, + // }), + // 'node_modules/dep': { + // 'package.json': JSON.stringify({ + // // require ./ + // exports: 'file.js', + // }), + // 'file.js': '', + // }, + // }); + + // const request = 'dep'; + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + + // expect(tsResolved.resolved).toBe(undefined); + // expect(resolved).toBe(tsResolved.resolved); + + // await fixture.rm(); + // }); + + // test('export map', async () => { + // const fixture = await createFixture({ + // 'tsconfig.json': JSON.stringify({ + // compilerOptions: { + // moduleResolution: 'Node16', + // }, + // }), + // 'node_modules/dep': { + // 'package.json': JSON.stringify({ + // exports: { + // '.': './entry.js', + // './a': './file-a.js', + // './b': './file-b.js', + // }, + // }), + // 'entry.js': '', + // 'file-a.js': '', + // 'file-b.js': '', + // }, + // }); + + // const map = { + // dep: '/entry.js', + // 'dep/a': '/file-a.js', + // 'dep/b': '/file-b.js', + // }; + + // for (const request in map) { + // const resolved = createResolver()(request, fixture.path); + // const tsResolved = await getTscResolution(request, fixture.path); + // expect(resolved?.endsWith(map[request as keyof typeof map])).toBeTruthy(); + // expect(resolved).toBe(tsResolved.resolved); + // } + + // await fixture.rm(); + // }); + test('export map with *', async () => { + const fixture = await createFixture({ + 'tsconfig.json': JSON.stringify({ + compilerOptions: { + moduleResolution: 'Node16', + }, + }), + 'node_modules/dep': { + 'package.json': JSON.stringify({ + exports: { + './file/*': './lib/*.js', + }, + }), + lib: { + 'a.js': '', + 'b.js': '', + }, + }, + }); + + const map = { + // dep: '/entry.js', + 'dep/file/a': '/lib/a.js', + 'dep/file/b': '/lib/b.js', + }; + + for (const request in map) { + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + expect(resolved?.endsWith(map[request as keyof typeof map])).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + } + + await fixture.rm(); + }); + }); + }); + }); +}); diff --git a/tests/specs/resolver/resolve-directories-package-json.ts b/tests/specs/resolver/resolve-directories-package-json.ts index 04bbe118..36c3cf6c 100644 --- a/tests/specs/resolver/resolve-directories-package-json.ts +++ b/tests/specs/resolver/resolve-directories-package-json.ts @@ -153,10 +153,28 @@ export default testSuite(({ describe }) => { await fixture.rm(); }); - - // shouldnt resolve export map }); - // resolve export map + test('export maps to not work', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + directory: { + 'file.ts': '', + 'package.json': JSON.stringify({ + name: 'package', + exports: './file.ts', + }), + }, + }); + + const request = './directory'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved).toBe(undefined); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); }); }); From b69d261d815140226033ccf5c8a5675844e6f338 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Thu, 22 Dec 2022 00:02:15 +0900 Subject: [PATCH 9/9] wip --- package.json | 3 + pnpm-lock.yaml | 8 + src/resolver/resolve-bare-specifier.ts | 123 +-- src/utils/parse-package-name.ts | 8 +- tests/specs/resolver/resolve-dependency.ts | 891 +++++++++++---------- tests/tsc-experiment.ts | 57 ++ 6 files changed, 553 insertions(+), 537 deletions(-) create mode 100644 tests/tsc-experiment.ts diff --git a/package.json b/package.json index 0de0250e..139da36c 100644 --- a/package.json +++ b/package.json @@ -80,5 +80,8 @@ } } ] + }, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45373660..171f77bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,16 @@ specifiers: jsonc-parser: ^3.0.0 manten: ^0.2.1 pkgroll: ^1.3.1 + resolve-pkg-maps: ^1.0.0 resolve.exports: ^1.1.0 slash: ^4.0.0 tsx: ^3.6.0 type-fest: ^2.13.1 typescript: ^4.7.3 +dependencies: + resolve-pkg-maps: 1.0.0 + devDependencies: '@pvtnbr/eslint-config': 0.26.1_7omqkkq7gtkix5mfmoxm235otq '@types/node': 17.0.41 @@ -2836,6 +2840,10 @@ packages: engines: {node: '>=4'} dev: true + /resolve-pkg-maps/1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: false + /resolve.exports/1.1.0: resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==} engines: {node: '>=10'} diff --git a/src/resolver/resolve-bare-specifier.ts b/src/resolver/resolve-bare-specifier.ts index f22f0300..6d60d9a0 100644 --- a/src/resolver/resolve-bare-specifier.ts +++ b/src/resolver/resolve-bare-specifier.ts @@ -6,7 +6,7 @@ import { parsePackageName } from '../utils/parse-package-name'; import { readJsonc } from '../utils/read-jsonc'; import type { FsAPI } from './types'; import { resolvePathExtension } from './resolve-path-extension'; - +import { resolveExports } from 'resolve-pkg-exports'; // Export maps // https://github.com/microsoft/TypeScript/blob/71e852922888337ef51a0e48416034a94a6c34d9/src/compiler/moduleSpecifiers.ts#L663 @@ -14,104 +14,6 @@ import { resolvePathExtension } from './resolve-path-extension'; // Import maps //https://github.com/microsoft/TypeScript/blob/71e852922888337ef51a0e48416034a94a6c34d9/src/compiler/moduleNameResolver.ts#L2101 - -type ExportCondition = - | 'import' - | 'require' - | 'node' - | 'node-addons' - | 'deno' - | 'browser' - | 'electron' - | 'react-native' - | 'default'; - -/** -Entry points of a module, optionally with conditions and subpath exports. -*/ -type Exports = -| null -| string -| string[] -| {[key in ExportCondition]: Exports} -| {[key: string]: Exports}; // eslint-disable-line @typescript-eslint/consistent-indexed-object-style - - -function resolveWithExportMap( - request: string, - exports: Exports, - conditions: string[], - asterisk?: string, -): string | string[] | undefined { - - console.log('resolveWithExportMap', { - request, exports, conditions, asterisk, - }); - - if (typeof exports === 'string') { - if (asterisk) { - return exports.replaceAll('*', asterisk); - } else { - return exports; - } - } - - else if (Array.isArray(exports)) { - return exports.map( - exportPath => resolveWithExportMap( - request, - exportPath, - conditions, - asterisk, - ) as string, - ); - } - - else if ( - typeof exports === 'object' - && exports !== null - ) { - const objectKeys = Object.keys(exports) as (keyof typeof exports)[]; - const isPathsObject = objectKeys.every(key => key.startsWith('.')); - - if (isPathsObject) { - console.log({ isPathsObject, exports, request }); - - for (const key of objectKeys) { - console.log({ key }); - if (key.includes('*')) { - const [prefix, suffix] = key.split('*'); - - // Can * span multiple paths / ? - if (request.startsWith(prefix) && request.endsWith(suffix)) { - const matched = request.slice(prefix.length, -suffix.length || undefined); - - return resolveWithExportMap( - request, - exports[key], - conditions, - matched, - ); - } - } else { - if (key === request) { - return resolveWithExportMap( - request, - exports[key], - conditions, - ); - } - } - } - } else { - - console.log('conditions'); - } - - } -} - - export function resolveBareSpecifier( request: string, context: string, @@ -155,30 +57,17 @@ export function resolveBareSpecifier( const packageJson = readJsonc(packageJsonPath, api); if (packageJson) { - const { exports } = packageJson + const { exports } = packageJson; if (exports) { - // TODO: merge with below later - if (!packageSubpath && typeof exports === 'string') { - const resolved = resolvePathExtension( - path.join(dependencyPath, exports), - api, - ); - if (resolved) { - return resolved; - } - } - - // Check exports main sugar - const resolvedSubPath = resolveWithExportMap( - packageSubpath ? './' + packageSubpath : '.', + const resolvedSubpaths = resolveExports( exports, + packageSubpath, conditions || [], ); - console.log({ packageSubpath, resolvedSubPath }); - if (resolvedSubPath) { + for (const possibleSubpath of resolvedSubpaths) { const resolved = resolvePathExtension( - path.join(dependencyPath, resolvedSubPath), + path.join(dependencyPath, possibleSubpath), api, ); diff --git a/src/utils/parse-package-name.ts b/src/utils/parse-package-name.ts index 75ee2bc8..2b28227c 100644 --- a/src/utils/parse-package-name.ts +++ b/src/utils/parse-package-name.ts @@ -1,14 +1,16 @@ +const SLASH = '/'; + export function parsePackageName( request: string, ) { - const segments = request.split('/'); + const segments = request.split(SLASH); let packageName = segments.shift()!; if (packageName[0] === '@' && segments[0]) { - packageName += `/${segments.shift()}`; + packageName += SLASH + segments.shift(); } return { packageName, - packageSubpath: segments.join('/'), + packageSubpath: segments.join(SLASH), }; } diff --git a/tests/specs/resolver/resolve-dependency.ts b/tests/specs/resolver/resolve-dependency.ts index c6a42b18..a5a1a91e 100644 --- a/tests/specs/resolver/resolve-dependency.ts +++ b/tests/specs/resolver/resolve-dependency.ts @@ -6,431 +6,429 @@ import { createResolver } from '#get-tsconfig'; export default testSuite(({ describe }) => { describe('dependencies', ({ test, describe }) => { - // describe('finds package', ({ test }) => { - // test('index.js', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/some-dependency': { - // 'package.json': JSON.stringify({ - // name: 'some-dependency', - // }), - // 'index.js': '', - // }, - // }); - - // const request = 'some-dependency'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/index.js')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('@org', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/@org.js': '', - // }); - - // const request = '@org'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/@org.js')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('@org/package', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/@org/package.js': '', - // }); - - // const request = '@org/package'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // // console.log({ resolved, tsResolved }); - - // expect(resolved?.endsWith('/@org/package.js')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('no package.json', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/some-dependency': { - // 'index.js': '', - // }, - // }); - - // const request = 'some-dependency'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/index.js')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('no directory', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/some-dependency.js': '', - // }); - - // const request = 'some-dependency'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/some-dependency.js')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('nested', async () => { - // const fixture = await createFixture({ - // 'a/b/c/d': { - // 'tsconfig.json': '', - // }, - // 'node_modules/some-dependency': { - // 'index.tsx': '', - // }, - // }); - - // const request = 'some-dependency'; - // const projectPath = path.join(fixture.path, 'a/b/c/d'); - // const resolved = createResolver()(request, projectPath); - // const tsResolved = await getTscResolution(request, projectPath); - - // expect(resolved?.endsWith('/some-dependency/index.tsx')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - // }); - - // describe('main', ({ test }) => { - // test('.ts -> .ts', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/dep': { - // 'package.json': JSON.stringify({ - // main: 'file.ts', - // }), - // 'file.ts': '', - // }, - // }); - - // const request = 'dep'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/file.ts')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('.js -> .js', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/dep': { - // 'package.json': JSON.stringify({ - // main: 'file.js', - // }), - // 'file.js': '', - // }, - // }); - - // const request = 'dep'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/file.js')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('.js -> ts', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/dep': { - // 'package.json': JSON.stringify({ - // main: 'file.js', - // }), - // 'file.ts': '', - // }, - // }); - - // const request = 'dep'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/file.ts')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('.js -> tsx', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/dep': { - // 'package.json': JSON.stringify({ - // main: 'file.js', - // }), - // 'file.tsx': '', - // }, - // }); - - // const request = 'dep'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/file.tsx')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('nonexistent main fallback to index', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/dep': { - // 'package.json': JSON.stringify({ - // main: 'non-existent', - // }), - // 'index.js': '', - // }, - // }); - - // const request = 'dep'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/index.js')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('relative path', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'index.ts': '', - // 'node_modules/dep': { - // 'package.json': JSON.stringify({ - // main: '../..', - // }), - // }, - // }); - - // const request = 'dep'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/index.ts')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('@scoped: .js -> ts', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/@org/dep': { - // 'package.json': JSON.stringify({ - // main: 'file.js', - // }), - // 'file.ts': '', - // }, - // }); - - // const request = '@org/dep'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/file.ts')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - // }); + describe('finds package', ({ test }) => { + test('index.js', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/some-dependency': { + 'package.json': JSON.stringify({ + name: 'some-dependency', + }), + 'index.js': '', + }, + }); + + const request = 'some-dependency'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('@org', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/@org.js': '', + }); + + const request = '@org'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/@org.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('@org/package', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/@org/package.js': '', + }); + + const request = '@org/package'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + // console.log({ resolved, tsResolved }); + + expect(resolved?.endsWith('/@org/package.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('no package.json', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/some-dependency': { + 'index.js': '', + }, + }); + + const request = 'some-dependency'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('no directory', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/some-dependency.js': '', + }); + + const request = 'some-dependency'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/some-dependency.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('nested', async () => { + const fixture = await createFixture({ + 'a/b/c/d': { + 'tsconfig.json': '', + }, + 'node_modules/some-dependency': { + 'index.tsx': '', + }, + }); + + const request = 'some-dependency'; + const projectPath = path.join(fixture.path, 'a/b/c/d'); + const resolved = createResolver()(request, projectPath); + const tsResolved = await getTscResolution(request, projectPath); + + expect(resolved?.endsWith('/some-dependency/index.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); + + describe('main', ({ test }) => { + test('.ts -> .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/dep': { + 'package.json': JSON.stringify({ + main: 'file.ts', + }), + 'file.ts': '', + }, + }); + + const request = 'dep'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.js -> .js', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/dep': { + 'package.json': JSON.stringify({ + main: 'file.js', + }), + 'file.js': '', + }, + }); + + const request = 'dep'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.js -> ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/dep': { + 'package.json': JSON.stringify({ + main: 'file.js', + }), + 'file.ts': '', + }, + }); + + const request = 'dep'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.js -> tsx', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/dep': { + 'package.json': JSON.stringify({ + main: 'file.js', + }), + 'file.tsx': '', + }, + }); + + const request = 'dep'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.tsx')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('nonexistent main fallback to index', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/dep': { + 'package.json': JSON.stringify({ + main: 'non-existent', + }), + 'index.js': '', + }, + }); + + const request = 'dep'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('relative path', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'index.ts': '', + 'node_modules/dep': { + 'package.json': JSON.stringify({ + main: '../..', + }), + }, + }); + + const request = 'dep'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/index.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('@scoped: .js -> ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/@org/dep': { + 'package.json': JSON.stringify({ + main: 'file.js', + }), + 'file.ts': '', + }, + }); + + const request = '@org/dep'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + }); describe('subpaths', ({ describe, test }) => { - // test('no package.json', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/dep': { - // 'file.ts': '', - // }, - // }); - - // const request = 'dep/file'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/file.ts')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('package.json', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/dep': { - // 'package.json': JSON.stringify({}), - // 'file.js': '', - // }, - // }); - - // const request = 'dep/file.js'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/file.js')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('.js -> .ts', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/dep': { - // 'package.json': JSON.stringify({}), - // 'file.ts': '', - // }, - // }); - - // const request = 'dep/file.js'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/file.ts')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); - - // test('.mjs -> .mts', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': '', - // 'node_modules/dep': { - // 'package.json': JSON.stringify({}), - // 'file.mts': '', - // }, - // }); - - // const request = 'dep/file.mjs'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - - // expect(resolved?.endsWith('/file.mts')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - - // await fixture.rm(); - // }); + test('no package.json', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/dep': { + 'file.ts': '', + }, + }); + + const request = 'dep/file'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('package.json', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/dep': { + 'package.json': JSON.stringify({}), + 'file.js': '', + }, + }); + + const request = 'dep/file.js'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.js -> .ts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/dep': { + 'package.json': JSON.stringify({}), + 'file.ts': '', + }, + }); + + const request = 'dep/file.js'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.ts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); + + test('.mjs -> .mts', async () => { + const fixture = await createFixture({ + 'tsconfig.json': '', + 'node_modules/dep': { + 'package.json': JSON.stringify({}), + 'file.mts': '', + }, + }); + + const request = 'dep/file.mjs'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + + expect(resolved?.endsWith('/file.mts')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + + await fixture.rm(); + }); describe('export map', ({ test }) => { - // test('main', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': JSON.stringify({ - // compilerOptions: { - // moduleResolution: 'Node16', - // }, - // }), - // 'node_modules/dep': { - // 'package.json': JSON.stringify({ - // exports: './file.js', - // }), - // 'file.js': '', - // }, - // }); - - // const request = 'dep'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); + test('main', async () => { + const fixture = await createFixture({ + 'tsconfig.json': JSON.stringify({ + compilerOptions: { + moduleResolution: 'Node16', + }, + }), + 'node_modules/dep': { + 'package.json': JSON.stringify({ + exports: './file.js', + }), + 'file.js': '', + }, + }); - // expect(resolved?.endsWith('/file.js')).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); + const request = 'dep'; + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); - // await fixture.rm(); - // }); - - // test('main - should not work without ./', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': JSON.stringify({ - // compilerOptions: { - // moduleResolution: 'Node16', - // }, - // }), - // 'node_modules/dep': { - // 'package.json': JSON.stringify({ - // // require ./ - // exports: 'file.js', - // }), - // 'file.js': '', - // }, - // }); - - // const request = 'dep'; - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); + expect(resolved?.endsWith('/file.js')).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); - // expect(tsResolved.resolved).toBe(undefined); - // expect(resolved).toBe(tsResolved.resolved); + await fixture.rm(); + }); + + test('main - should not work without ./', async () => { + const fixture = await createFixture({ + 'tsconfig.json': JSON.stringify({ + compilerOptions: { + moduleResolution: 'Node16', + }, + }), + 'node_modules/dep': { + 'package.json': JSON.stringify({ + // Should be prefixed by ./ + exports: 'file.js', + }), + 'file.js': '', + }, + }); + + expect(() => createResolver()('dep', fixture.path)).toThrow( + 'Invalid "exports" target "file.js" defined in the package config', + ); - // await fixture.rm(); - // }); - - // test('export map', async () => { - // const fixture = await createFixture({ - // 'tsconfig.json': JSON.stringify({ - // compilerOptions: { - // moduleResolution: 'Node16', - // }, - // }), - // 'node_modules/dep': { - // 'package.json': JSON.stringify({ - // exports: { - // '.': './entry.js', - // './a': './file-a.js', - // './b': './file-b.js', - // }, - // }), - // 'entry.js': '', - // 'file-a.js': '', - // 'file-b.js': '', - // }, - // }); - - // const map = { - // dep: '/entry.js', - // 'dep/a': '/file-a.js', - // 'dep/b': '/file-b.js', - // }; - - // for (const request in map) { - // const resolved = createResolver()(request, fixture.path); - // const tsResolved = await getTscResolution(request, fixture.path); - // expect(resolved?.endsWith(map[request as keyof typeof map])).toBeTruthy(); - // expect(resolved).toBe(tsResolved.resolved); - // } + await fixture.rm(); + }); + + test('export map', async () => { + const fixture = await createFixture({ + 'tsconfig.json': JSON.stringify({ + compilerOptions: { + moduleResolution: 'Node16', + }, + }), + 'node_modules/dep': { + 'package.json': JSON.stringify({ + exports: { + '.': './entry.js', + './a': './file-a.js', + './b': './file-b.js', + }, + }), + 'entry.js': '', + 'file-a.js': '', + 'file-b.js': '', + }, + }); + + const map = { + dep: '/entry.js', + 'dep/a': '/file-a.js', + 'dep/b': '/file-b.js', + }; + + for (const request in map) { + const resolved = createResolver()(request, fixture.path); + const tsResolved = await getTscResolution(request, fixture.path); + expect(resolved?.endsWith(map[request as keyof typeof map])).toBeTruthy(); + expect(resolved).toBe(tsResolved.resolved); + } - // await fixture.rm(); - // }); + await fixture.rm(); + }); + test('export map with *', async () => { const fixture = await createFixture({ 'tsconfig.json': JSON.stringify({ @@ -452,7 +450,6 @@ export default testSuite(({ describe }) => { }); const map = { - // dep: '/entry.js', 'dep/file/a': '/lib/a.js', 'dep/file/b': '/lib/b.js', }; @@ -466,6 +463,66 @@ export default testSuite(({ describe }) => { await fixture.rm(); }); + + test('export map block', async () => { + const fixture = await createFixture({ + 'tsconfig.json': JSON.stringify({ + compilerOptions: { + moduleResolution: 'Node16', + }, + }), + 'node_modules/dep': { + 'package.json': JSON.stringify({ + exports: {}, + }), + 'a.js': '', + 'b.js': '', + }, + }); + + const files = ['dep/a', 'dep/b']; + + for (const file of files) { + const resolved = createResolver()(file, fixture.path); + const tsResolved = await getTscResolution(file, fixture.path); + expect(resolved).toBeUndefined(); + expect(resolved).toBe(tsResolved.resolved); + } + + await fixture.rm(); + }); + }); + + test('self import', async () => { + const fixture = await createFixture({ + 'tsconfig.json': JSON.stringify({ + compilerOptions: { + moduleResolution: 'Node16', + }, + }), + 'node_modules/dep': { + 'package.json': JSON.stringify({ + name: 'dep', + exports: './entry.js', + }), + 'entry.js': '', + 'node_modules/dep': { + 'package.json': JSON.stringify({ + name: 'dep', + exports: { + '.': './entry.js', + './file': './file.js', + }, + }), + 'entry.js': '', + }, + }, + }); + + const resolved = createResolver()('dep', path.join(fixture.path, 'node_modules/dep/')); + expect(resolved?.endsWith('/node_modules/dep/node_modules/dep/entry.js')).toBeTruthy(); + + await fixture.rm(); }); }); }); diff --git a/tests/tsc-experiment.ts b/tests/tsc-experiment.ts new file mode 100644 index 00000000..64c84fcd --- /dev/null +++ b/tests/tsc-experiment.ts @@ -0,0 +1,57 @@ +import path from 'path'; +import fs from 'fs/promises'; +import { createFixture } from 'fs-fixture'; +import { execa } from 'execa'; + +const tscPath = path.resolve('node_modules/.bin/tsc'); + +const randomId = () => Math.random().toString(36).slice(2); + +async function tscResolve( + request: string, + fixturePath: string, +) { + const filePath = path.join(fixturePath, `${randomId()}.ts`); + await fs.writeFile( + filePath, + `import '${request}'`, + ); + const { stdout } = await execa( + tscPath, + [ + '--traceResolution', + '--noEmit', + ], + { cwd: fixturePath }, + ); + + await fs.rm(filePath, { + force: true, + }); + + return stdout; +} + +(async () => { + const fixture = await createFixture({ + 'tsconfig.json': JSON.stringify({ + compilerOptions: { + moduleResolution: 'Node16', + }, + }), + node_modules: { + 'dep': { + 'package.json': JSON.stringify({ + exports: ['./missing.ts', './asdf.ts'], + }), + 'asdf.ts': '', + }, + }, + }); + + const stdout = await tscResolve('dep', fixture.path); + + console.log(stdout); + + await fixture.rm(); +})();