Skip to content
This repository was archived by the owner on May 12, 2022. It is now read-only.

Commit e19b0a4

Browse files
authored
feat: use @rollup/plugin-typescript to resolve id (#22)
1 parent d900bb2 commit e19b0a4

File tree

8 files changed

+125
-56
lines changed

8 files changed

+125
-56
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ jobs:
2525
- run: npx conventional-github-releaser -p angular
2626
env:
2727
CONVENTIONAL_GITHUB_RELEASER_TOKEN: ${{secrets.GITHUB_TOKEN}}
28+

loader.mjs

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
import { URL, pathToFileURL, fileURLToPath } from 'url'
22
import fs from 'fs'
3+
import path from 'path'
34
import { transformSync } from 'esbuild'
4-
import { createMatchPath, loadConfig } from 'tsconfig-paths'
5+
import typescript from '@rollup/plugin-typescript'
6+
import JoyCon from 'joycon'
57

6-
const baseURL = pathToFileURL(`${process.cwd()}/`).href
78
const isWindows = process.platform === 'win32'
89

910
const extensionsRegex = /\.(m?tsx?|json)$/
10-
const excludeRegex = /^\w+:/
11-
const tsExtensions = ['.mts', '.ts', '.cts', '.tsx'] // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html
12-
const jsExtensions = ['.mjs', '.js', '.cjs', '.jsx']
13-
const extensions = [...tsExtensions, ...jsExtensions]
14-
15-
const tsconfig = loadConfig()
16-
17-
const matchPath = tsconfig.resultType === 'success' ? createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths) : undefined
11+
const tsconfigPath = new JoyCon({ parseJSON: () => {} }).loadSync(['tsconfig.json']).path
12+
const pluginTypescript = typescript({
13+
tsconfig: tsconfigPath,
14+
})
1815

1916
function esbuildTransformSync(rawSource, filename, url, format) {
2017
const {
@@ -39,49 +36,75 @@ function esbuildTransformSync(rawSource, filename, url, format) {
3936
return { js, jsSourceMap }
4037
}
4138

42-
export const tryPathWithExtensions = (path) => {
43-
for (const ext of extensions) {
44-
const p = `${path}${ext}`
45-
if (fs.existsSync(p))
46-
return p
39+
function getTsCompatSpecifier(parentURL, specifier) {
40+
let tsSpecifier
41+
let search
42+
43+
if (specifier.startsWith('./') || specifier.startsWith('../')) {
44+
// Relative import
45+
const url = new URL(specifier, parentURL)
46+
tsSpecifier = fileURLToPath(url).replace(/\.tsx?$/, '')
47+
search = url.search
48+
}
49+
else {
50+
// Bare import
51+
tsSpecifier = specifier
52+
search = ''
53+
}
54+
55+
return {
56+
tsSpecifier,
57+
search,
4758
}
48-
return null
4959
}
5060

51-
export function resolve(specifier, context, defaultResolve) {
52-
// baseUrl & paths takes the highest precedence, as TypeScript behaves.
53-
if (matchPath) {
54-
const nodePath = matchPath(specifier, undefined, undefined, extensions)
61+
function isValidURL(s) {
62+
try {
63+
return !!new URL(s)
64+
}
65+
catch (e) {
66+
if (e instanceof TypeError)
67+
return false
5568

56-
if (nodePath) {
57-
const foundPath = tryPathWithExtensions(nodePath)
58-
return {
59-
url: pathToFileURL(foundPath).href,
60-
format: extensionsRegex.test(foundPath) && 'module',
61-
}
69+
throw e
70+
}
71+
}
72+
73+
export async function resolve(specifier, context, defaultResolve) {
74+
const {
75+
parentURL,
76+
} = context
77+
78+
let url
79+
80+
// According to Node's algorithm, we first check if it is a valid URL.
81+
// When the module is the entry point, node will provides a file URL to it.
82+
if (isValidURL(specifier)) {
83+
url = new URL(specifier)
84+
}
85+
else {
86+
// Try to resolve the module according to typescript's algorithm,
87+
// and construct a valid url.
88+
const parsed = getTsCompatSpecifier(parentURL, specifier)
89+
const path = pluginTypescript.resolveId(parsed.tsSpecifier, fileURLToPath(parentURL))
90+
if (path) {
91+
url = pathToFileURL(path)
92+
url.search = parsed.search
6293
}
6394
}
6495

65-
const { parentURL = baseURL } = context
66-
const url = new URL(specifier, parentURL)
67-
if (extensionsRegex.test(url.pathname))
68-
return { url: url.href, format: 'module' }
69-
70-
// ignore `data:` and `node:` prefix etc.
71-
if (!excludeRegex.test(specifier)) {
72-
// Try to resolve extension
73-
const path = fileURLToPath(url.href)
74-
const foundPath = tryPathWithExtensions(path)
75-
if (foundPath) {
76-
url.pathname = foundPath
96+
if (url) {
97+
// If the resolved file is typescript
98+
if (extensionsRegex.test(url.pathname)) {
7799
return {
78100
url: url.href,
79-
format: extensionsRegex.test(url.pathname) && 'module',
101+
format: 'module',
80102
}
81103
}
104+
// Else, for other types, use default resolve with the valid path
105+
return defaultResolve(url.href, context, defaultResolve)
82106
}
83107

84-
// Let Node.js handle all other specifiers.
85108
return defaultResolve(specifier, context, defaultResolve)
86109
}
87110

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "esbuild-node-loader",
3-
"version": "0.5.0",
3+
"version": "0.4.3",
44
"description": "Transpile TypeScript to ESM with Node.js loader",
55
"keywords": [
66
"node-loader"
@@ -33,8 +33,9 @@
3333
"test": "node --experimental-loader ./loader.mjs test/entry.ts"
3434
},
3535
"dependencies": {
36+
"@rollup/plugin-typescript": "^8.3.0",
3637
"esbuild": "^0.13.3",
37-
"tsconfig-paths": "^3.11.0"
38+
"joycon": "^3.0.1"
3839
},
3940
"devDependencies": {
4041
"@antfu/eslint-config": "^0.9.0",

pnpm-lock.yaml

Lines changed: 46 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/entry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ test('import type module', async() => {
6666
const { stdout } = await execa('node', [
6767
'--experimental-loader',
6868
relativize(`${cwd}/loader.mjs`),
69-
relativize(`${cwd}/test/import-mjs/index.js`),
69+
relativize(`${cwd}/test/import-mjs/index.ts`),
7070
])
71-
assert(stdout === 'foo')
71+
assert(stdout === 'foo\nnot index\nexport')
7272
})
7373

7474
test('import with query', async() => {
@@ -108,7 +108,7 @@ test('import json', async() => {
108108
})
109109

110110
test('tsconfig-paths', async() => {
111-
const cwd2 = `${cwd}/test/tsconfig-paths`;
111+
const cwd2 = `${cwd}/test/tsconfig-paths`
112112
const { stdout } = await execa('node', [
113113
'--experimental-loader',
114114
relativize(`${cwd}/loader.mjs`, cwd2),

test/import-mjs/index.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

test/import-mjs/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import foo from 'foo'
2+
import notIndex from 'foo/not-index'
3+
import notIndex from 'foo/not-index.js'
4+
import { a } from '../fixture.export'
5+
6+
console.log(foo)
7+
console.log(notIndex)
8+
console.log(a)

test/import-mjs/node_modules/foo/not-index.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)