Skip to content

Commit

Permalink
Basic support for subpath imports.
Browse files Browse the repository at this point in the history
  • Loading branch information
ajvincent committed Jun 16, 2024
1 parent a2e9aef commit 3439cf4
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 20 deletions.
37 changes: 30 additions & 7 deletions src/hooks/hooks.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import type {
LoadHook,
ResolveHook,
} from 'node:module'
import { resolve as pathResolve } from 'node:path'
import { resolve as pathResolve, dirname } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { format } from 'node:util'
import { MessagePort } from 'node:worker_threads'
import { classifyModule } from '../classify-module.js'
import { DaemonClient } from '../client.js'
import { getDiagMode } from '../diagnostic-mode.js'
import getPackageJSON from '../service/get-package-json.js'
import { relative } from 'path'

// in some cases on the loader thread, console.error doesn't actually
// print. sync write to fd 1 instead.
Expand Down Expand Up @@ -55,12 +57,33 @@ export const resolve: ResolveHook = async (
nextResolve
) => {
const { parentURL } = context
const target =
/* c8 ignore start */
parentURL && (url.startsWith('./') || url.startsWith('../'))
? /* c8 ignore stop */
String(new URL(url, parentURL))
: url

if (url.startsWith("#")) {
const { contents, pathToJSON } = getPackageJSON(process.cwd())!;
if (pathToJSON && contents) {
const { imports } = contents as { imports: Record<string, string> };
if (imports) {
for (let [importSubpath, relativeSubpath] of Object.entries(imports)) {
if (!importSubpath.startsWith("#") || !importSubpath.endsWith("/*") || !relativeSubpath.endsWith("/*"))
continue;
importSubpath = relative(dirname(importSubpath), url);
if (importSubpath.includes("#"))
continue;

url = pathResolve(dirname(pathToJSON), dirname(relativeSubpath), importSubpath)
break;
}
}
}
}

let target =
/* c8 ignore start */
parentURL && (url.startsWith('./') || url.startsWith('../'))
? /* c8 ignore stop */
String(new URL(url, parentURL))
: url

return nextResolve(
target.startsWith('file://') && !startsWithCS(target, nm)
? await getClient().resolve(url, parentURL)
Expand Down
21 changes: 21 additions & 0 deletions src/service/get-package-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { walkUp } from 'walk-up-path'
import { normalizePath, fileExists, readFile } from '../ts-sys-cached.js'

function getPackageJSONPath(dir: string): string | undefined {
for (const d of walkUp(dir)) {
let pjPath = normalizePath(d + "/package.json");
if (fileExists(pjPath))
return pjPath;
}
}

export default function getPackageJSON(dir: string): {contents: object, pathToJSON: string} | undefined {
const pathToJSON = getPackageJSONPath(dir);
if (pathToJSON) {
const json = readFile(pathToJSON) as string;
return {
contents: JSON.parse(json) as object,
pathToJSON,
};
}
}
19 changes: 6 additions & 13 deletions src/service/transpile-only.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ts from 'typescript'
import { walkUp } from 'walk-up-path'
import { normalizePath, readFile } from '../ts-sys-cached.js'
import { tsconfig } from './tsconfig.js'
import getPackageJSON from './get-package-json.js'

// Basic technique lifted from ts-node's transpileOnly method.
// Create a CompilerHost, and mock the loading of package.json
Expand Down Expand Up @@ -132,19 +133,11 @@ const createTsTranspileModule = ({
packageJsonFileName = dir + '/package.json'
if (pjType) packageJsonType = pjType
else {
for (const d of walkUp(dir)) {
const pj = catcher(() => {
const json = readFile(d + '/package.json')
if (!json) return undefined
const pj = JSON.parse(json) as {
type?: 'commonjs' | 'module'
}
return pj
})
if (pj?.type) {
packageJsonType = pj.type
break
}
const pj = (getPackageJSON(dir))?.contents as {
type?: 'commonjs' | 'module'
};
if (pj?.type) {
packageJsonType = pj.type
}
}

Expand Down
48 changes: 48 additions & 0 deletions test/hooks/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,54 @@ t.test('resolve', async t => {
})
})

t.test('resolve with subpath import', async t => {
const dir = t.testdir({
'package.json': JSON.stringify({
"type": "module",
"imports": {
"#source/*": "./my/source/*"
}
}),

"project": {
'foo.ts': 'console.log("feet")',
}
})

const cwd = process.cwd()
process.chdir(dir + '/project')

const hooks = (await t.mockImport(
'../../dist/esm/hooks/hooks.mjs',
{
'node:fs': t.createMock(FS, { writeSync: mockWriteSync }),
'../../dist/esm/client.js': {
DaemonClient: MockDaemonClient,
},
}
)) as typeof import('../../dist/esm/hooks/hooks.mjs')

const nextResolve = (url: string, context: ResolveHookContext | undefined) => {
console.log(url);
return { url, parentURL: context?.parentURL }
}

t.strictSame(
await hooks.resolve(
'#source/subdir/constant.js',
{
parentURL: 'file:///asdf/asdf.js',
conditions: [],
importAssertions: {}
},
nextResolve
),
{ url: dir + '/my/source/subdir/constant.js', parentURL: 'file:///asdf/asdf.js' }
)

process.chdir(cwd)
})

t.test('load', async t => {
t.test('not one of ours, just call nextLoad', async t => {
delete MockDaemonClient.compileRequest
Expand Down

0 comments on commit 3439cf4

Please sign in to comment.