Skip to content

Commit ac0640a

Browse files
committed
Add path mapping for TypeScript and node imports
Closes tapjs#11 tapjs#12
1 parent 8baac64 commit ac0640a

File tree

7 files changed

+159
-28
lines changed

7 files changed

+159
-28
lines changed

package-lock.json

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@
5656
"@isaacs/cached": "^1.0.1",
5757
"@isaacs/catcher": "^1.0.4",
5858
"foreground-child": "^3.1.1",
59+
"get-tsconfig": "^4.7.2",
5960
"mkdirp": "^3.0.1",
6061
"pirates": "^4.0.6",
62+
"resolve-pkg-maps": "^1.0.0",
6163
"rimraf": "^5.0.5",
6264
"signal-exit": "^4.1.0",
6365
"sock-daemon": "^1.4.2",

src/hooks/hooks.mts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { MessagePort } from 'node:worker_threads'
1313
import { classifyModule } from '../classify-module.js'
1414
import { DaemonClient } from '../client.js'
1515
import { getDiagMode } from '../diagnostic-mode.js'
16+
import { resolveMapping } from '../service/resolve-mapping.js'
1617

1718
// in some cases on the loader thread, console.error doesn't actually
1819
// print. sync write to fd 1 instead.
@@ -54,6 +55,7 @@ export const resolve: ResolveHook = async (
5455
context,
5556
nextResolve
5657
) => {
58+
url = resolveMapping(url, context.parentURL)
5759
const { parentURL } = context
5860
const target =
5961
/* c8 ignore start */

src/service/resolve-mapping.ts

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { tsconfig } from './tsconfig.js'
2+
import { relative, dirname, join } from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
import { createPathsMatcher, type TsConfigResult } from 'get-tsconfig'
5+
import {
6+
resolveImports,
7+
type PathConditionsMap,
8+
} from 'resolve-pkg-maps'
9+
import { walkUp } from 'walk-up-path'
10+
import { catcher } from '@isaacs/catcher'
11+
import { readFile } from '../ts-sys-cached.js'
12+
import { classifyModule } from '../classify-module.js'
13+
14+
const resolveTypescriptMapping = (
15+
url: string,
16+
parent: string | undefined
17+
): string | undefined => {
18+
if (!url || url.startsWith('file:///') || !parent) return undefined
19+
const options = tsconfig().options
20+
21+
const relativePath = url
22+
const tsconfigArg = {
23+
config: { compilerOptions: { ...options, baseUrl: './' } },
24+
path: options.configFilePath as string,
25+
} as TsConfigResult
26+
27+
const pathMather = createPathsMatcher(tsconfigArg)
28+
29+
if (!pathMather) return undefined
30+
const found = pathMather(relativePath)
31+
if (found.length === 0) return undefined
32+
return relative(
33+
dirname(fileURLToPath(parent)),
34+
found[0] as string
35+
).toString()
36+
}
37+
38+
const getPackageJson = (
39+
from: string
40+
):
41+
| ({ imports: PathConditionsMap } & { path: string })
42+
| undefined => {
43+
for (const d of walkUp(from)) {
44+
const pj = catcher(() => {
45+
const json = readFile(d + '/package.json')
46+
if (!json) return undefined
47+
const pj = JSON.parse(json) as { imports: PathConditionsMap }
48+
return pj
49+
})
50+
if (pj) {
51+
return { imports: pj?.imports, path: d }
52+
}
53+
}
54+
return undefined
55+
}
56+
57+
const resolvePackageJsonMapping = (
58+
url: string,
59+
parent: string | undefined
60+
): string | undefined => {
61+
if (
62+
!url ||
63+
url.startsWith('file:///') ||
64+
!url.startsWith('#') ||
65+
!parent
66+
)
67+
return undefined
68+
69+
const options = getPackageJson(dirname(fileURLToPath(parent)))
70+
if (options === undefined) return undefined
71+
72+
const found = resolveImports(options.imports, url, [
73+
'default',
74+
'node',
75+
classifyModule(url) === 'commonjs' ? 'require' : 'import',
76+
])
77+
if (found.length === 0) return undefined
78+
return relative(
79+
dirname(fileURLToPath(parent)),
80+
join(options.path, found[0] as string)
81+
).toString()
82+
}
83+
84+
export const resolveMapping = (
85+
url: string,
86+
parent: string | undefined
87+
): string => {
88+
return (
89+
resolvePackageJsonMapping(url, parent) ??
90+
resolveTypescriptMapping(url, parent) ??
91+
url
92+
)
93+
}

src/service/transpile-only.ts

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const createTsTranspileModule = ({
6868
) {
6969
continue
7070
}
71+
if (option.name === 'paths') continue
7172

7273
options[option.name] = option.transpileOptionValue
7374
}

src/service/tsconfig.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// return the same object if it parses to the same values.
66
import { catcher } from '@isaacs/catcher'
77
import { statSync } from 'fs'
8-
import { resolve } from 'path'
8+
import { dirname, resolve } from 'node:path'
99
import ts from 'typescript'
1010
import { walkUp } from 'walk-up-path'
1111
import { error, warn } from '../debug.js'
@@ -73,7 +73,7 @@ export const tsconfig = () => {
7373
// also default to recommended setting for node programs
7474
{
7575
compilerOptions: {
76-
rootDir: dir,
76+
rootDir: dirname(configPath),
7777
skipLibCheck: true,
7878
isolatedModules: true,
7979
esModuleInterop: true,
@@ -106,7 +106,11 @@ export const tsconfig = () => {
106106
noEmit: false,
107107
},
108108
})
109-
const newConfig = ts.parseJsonConfigFileContent(res, ts.sys, dir)
109+
const newConfig = ts.parseJsonConfigFileContent(
110+
res,
111+
ts.sys,
112+
dirname(configPath)
113+
)
110114
const newConfigJSON = JSON.stringify(newConfig)
111115
if (loadedConfig && newConfigJSON === loadedConfigJSON) {
112116
// no changes, keep the old one

test/bin.ts

+49-20
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ const bin = fileURLToPath(
88
new URL('../dist/esm/bin.mjs', import.meta.url)
99
)
1010

11-
const run = (args: string[]) =>
12-
spawnSync(process.execPath, [bin, ...args], { encoding: 'utf8' })
11+
const run = (args: string[], tsconfigPath?: string) =>
12+
spawnSync(process.execPath, [bin, ...args], {
13+
encoding: 'utf8',
14+
env: {
15+
TSIMP_PROJECT: tsconfigPath,
16+
},
17+
})
1318

1419
t.teardown(() => run(['--stop']))
1520

@@ -65,30 +70,33 @@ t.test('actually run a program', async t => {
6570

6671
// subpath import tests
6772
'package.json': JSON.stringify({
68-
"type": "module",
69-
"imports": {
70-
"#utilities/*": "./utilities/*"
71-
}
73+
type: 'module',
74+
imports: {
75+
'#utilities/*': './utilities/*',
76+
},
7277
}),
7378
'tsconfig.json': JSON.stringify({
74-
"compilerOptions": {
75-
"baseUrl": ".",
76-
"paths": {
77-
"#utilities/*": ["./utilities/*"],
78-
}
79+
compilerOptions: {
80+
baseUrl: '.',
81+
resolvePackageJsonImports: true,
82+
paths: {
83+
'+utilities/*': ['./utilities/*'],
84+
},
7985
},
8086
}),
8187

82-
'utilities': {
83-
'source': {
84-
'constants.ts': 'enum Constants { one = "one", two = "two" }; export default Constants;'
85-
}
88+
utilities: {
89+
source: {
90+
'constants.ts':
91+
'enum Constants { one = "one", two = "two", three = "three" }; export default Constants;',
92+
},
8693
},
8794

88-
"test": {
95+
test: {
8996
'getOne.ts': `import Constants from "../utilities/source/constants.js"; console.log(Constants.one);`,
90-
'getTwo.ts': `import Constants from "#utilities/source/constants.js"; console.log(Constants.two);`
91-
}
97+
'getTwo.ts': `import Constants from "#utilities/source/constants.ts"; console.log(Constants.two);`,
98+
'getThree.ts': `import Constants from "+utilities/source/constants.js"; console.log(Constants.three);`,
99+
},
92100
})
93101
const rel = relative(process.cwd(), dir).replace(/\\/g, '/')
94102

@@ -151,18 +159,39 @@ t.test('actually run a program', async t => {
151159
})
152160

153161
t.test('run file with subpath imports', async t => {
162+
run(['--restart'], `./${rel}/tsconfig.json`)
163+
154164
{
155165
const pathToFile = `./${rel}/test/getOne.ts`
156-
const { stdout, status } = run([pathToFile])
166+
const { stdout, status } = run(
167+
[pathToFile],
168+
`./${rel}/tsconfig.json`
169+
)
170+
157171
t.equal(status, 0)
158172
t.equal(stdout, 'one\n')
159173
}
160174

161175
{
162176
const pathToFile = `./${rel}/test/getTwo.ts`
163-
const { stdout, status } = run([pathToFile])
177+
const { stdout, status } = run(
178+
[pathToFile],
179+
`./${rel}/tsconfig.json`
180+
)
181+
164182
t.equal(status, 0)
165183
t.equal(stdout, 'two\n')
166184
}
185+
186+
{
187+
const pathToFile = `./${rel}/test/getThree.ts`
188+
const { stdout, status } = run(
189+
[pathToFile],
190+
`./${rel}/tsconfig.json`
191+
)
192+
193+
t.equal(status, 0)
194+
t.equal(stdout, 'three\n')
195+
}
167196
})
168197
})

0 commit comments

Comments
 (0)