-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
resolve-module.mts
177 lines (164 loc) · 4.53 KB
/
resolve-module.mts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/**
* @file resolveModule
* @module mlly/lib/resolveModule
*/
import defaultExtensions from '#lib/default-extensions'
import resolveAlias from '#lib/resolve-alias'
import { moduleResolve } from '#lib/resolver'
import {
codes,
isNodeError,
type Code,
type ErrModuleNotFound,
type NodeError
} from '@flex-development/errnode'
import type {
ChangeExtFn,
ModuleId,
ResolveModuleOptions
} from '@flex-development/mlly'
import pathe from '@flex-development/pathe'
export default resolveModule
/**
* Resolve a module `specifier` according to the [ESM Resolver algorithm][esm],
* mostly 😉.
*
* Adds support for:
*
* - Changing file extensions
* - Directory index resolution
* - Extensionless file resolution
* - Path alias resolution
* - Scopeless `@types/*` resolution (i.e. `unist` -> `@types/unist`)
*
* [esm]: https://nodejs.org/api/esm.html#esm_resolver_algorithm
*
* @see {@linkcode ModuleId}
* @see {@linkcode NodeError}
* @see {@linkcode ResolveModuleOptions}
*
* @async
*
* @param {string} specifier
* The module specifier to resolve
* @param {ModuleId} parent
* URL of parent module
* @param {ResolveModuleOptions | null | undefined} [options]
* Resolution options
* @return {Promise<URL>}
* Resolved URL
* @throws {NodeError}
*/
async function resolveModule(
specifier: string,
parent: ModuleId,
options?: ResolveModuleOptions | null | undefined
): Promise<URL> {
try {
return changeExt(await moduleResolve(
resolveAlias(specifier, { ...options, parent }) ?? specifier,
parent,
options?.conditions,
options?.mainFields,
options?.preserveSymlinks,
options?.fs
), specifier, options?.ext)
} catch (e: unknown) {
/**
* Error codes to ignore when attempting to resolve {@linkcode specifier}.
*
* @const {Set<Code>} ignore
*/
const ignore: Set<Code> = new Set<Code>([
codes.ERR_MODULE_NOT_FOUND,
codes.ERR_UNSUPPORTED_DIR_IMPORT
])
if (isNodeError(e) && ignore.has(e.code)) {
/**
* Module extensions to probe for.
*
* @const {string[]} extensions
*/
const extensions: string[] = [
...(options?.extensions ?? defaultExtensions)
]
/**
* Module specifiers to try resolving.
*
* @var {string[]} tries
*/
let tries: string[] = []
// add @types resolution attempts if package resolution failed.
if (
e.code === codes.ERR_MODULE_NOT_FOUND &&
!(e as ErrModuleNotFound).url
) {
tries = [
specifier.startsWith('@types/') ? specifier : '@types/' + specifier
].flatMap(specifier => [
specifier,
specifier + pathe.sep + 'index.d.ts',
specifier + pathe.sep + 'index.d.mts',
specifier + pathe.sep + 'index.d.cts'
])
}
// add extensionless file resolution attempts if file resolution failed.
if (
e.code === codes.ERR_MODULE_NOT_FOUND &&
(e as ErrModuleNotFound).url
) {
tries = extensions.map(ext => specifier + pathe.formatExt(ext))
}
// add directory index resolution attempts if directory resolution failed.
if (e.code === codes.ERR_UNSUPPORTED_DIR_IMPORT) {
tries = extensions.map(ext => {
return specifier + pathe.sep + 'index' + pathe.formatExt(ext)
})
}
// try module resolution attempts.
for (const attempt of tries) {
try {
return changeExt(await moduleResolve(
attempt,
parent,
options?.conditions,
options?.mainFields,
options?.preserveSymlinks,
options?.fs
), specifier, options?.ext)
} catch {
// swallow error to continue resolution attempts.
continue
}
}
}
throw e
}
}
/**
* Change the file extension of `url`.
*
* @internal
*
* @param {URL} url
* The resolved module URL
* @param {string} specifier
* The module specifier being resolved
* @param {ChangeExtFn | string | null | undefined} [ext]
* Replacement file extension or function that returns a file extension
* @return {URL}
* `url`
*/
function changeExt(
url: URL,
specifier: string,
ext?: ChangeExtFn | string | null | undefined
): URL {
if (url.protocol === 'file:' && ext !== undefined) {
url.href = typeof ext === 'function'
? pathe.changeExt(url.href, ext(url, specifier))
: pathe.changeExt(url.href, ext)
url = new URL(pathe.toPosix(url.href).replace(/\/index$/, pathe.sep))
}
return url
}