Skip to content

Commit 2c0a147

Browse files
committedMar 25, 2022
fix: inline import-meta-resolve
1 parent bb6d214 commit 2c0a147

File tree

8 files changed

+1548
-3
lines changed

8 files changed

+1548
-3
lines changed
 

‎README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Several utilities to make ESM resolution easier:
4444
### `resolve`
4545

4646
Resolve a module by respecting [ECMAScript Resolver algorithm](https://nodejs.org/dist/latest-v14.x/docs/api/esm.html#esm_resolver_algorithm)
47-
(internally using [wooorm/import-meta-resolve](https://github.com/wooorm/import-meta-resolve) that exposes Node.js implementation).
47+
(based on experimental Node.js implementation extracted from [wooorm/import-meta-resolve](https://github.com/wooorm/import-meta-resolve)).
4848

4949
Additionally supports resolving without extension and `/index` similar to CommonJS.
5050

‎lib/import-meta-resolve/errors.js

+352
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
// Manually “tree shaken” from:
2+
// <https://github.com/nodejs/node/blob/89f592c/lib/internal/errors.js>
3+
import assert from 'assert'
4+
// Needed for types.
5+
// eslint-disable-next-line no-unused-vars
6+
import { format, inspect } from 'util'
7+
8+
const isWindows = process.platform === 'win32'
9+
10+
const own = {}.hasOwnProperty
11+
12+
export const codes = {}
13+
14+
/**
15+
* @typedef {(...args: unknown[]) => string} MessageFunction
16+
*/
17+
18+
/** @type {Map<string, MessageFunction|string>} */
19+
const messages = new Map()
20+
const nodeInternalPrefix = '__node_internal_'
21+
/** @type {number} */
22+
let userStackTraceLimit
23+
24+
codes.ERR_INVALID_MODULE_SPECIFIER = createError(
25+
'ERR_INVALID_MODULE_SPECIFIER',
26+
/**
27+
* @param {string} request
28+
* @param {string} reason
29+
* @param {string} [base]
30+
*/
31+
(request, reason, base = undefined) => {
32+
return `Invalid module "${request}" ${reason}${
33+
base ? ` imported from ${base}` : ''
34+
}`
35+
},
36+
TypeError
37+
)
38+
39+
codes.ERR_INVALID_PACKAGE_CONFIG = createError(
40+
'ERR_INVALID_PACKAGE_CONFIG',
41+
/**
42+
* @param {string} path
43+
* @param {string} [base]
44+
* @param {string} [message]
45+
*/
46+
(path, base, message) => {
47+
return `Invalid package config ${path}${
48+
base ? ` while importing ${base}` : ''
49+
}${message ? `. ${message}` : ''}`
50+
},
51+
Error
52+
)
53+
54+
codes.ERR_INVALID_PACKAGE_TARGET = createError(
55+
'ERR_INVALID_PACKAGE_TARGET',
56+
/**
57+
* @param {string} pkgPath
58+
* @param {string} key
59+
* @param {unknown} target
60+
* @param {boolean} [isImport=false]
61+
* @param {string} [base]
62+
*/
63+
(pkgPath, key, target, isImport = false, base = undefined) => {
64+
const relError =
65+
typeof target === 'string' &&
66+
!isImport &&
67+
target.length > 0 &&
68+
!target.startsWith('./')
69+
if (key === '.') {
70+
assert(isImport === false)
71+
return (
72+
`Invalid "exports" main target ${JSON.stringify(target)} defined ` +
73+
`in the package config ${pkgPath}package.json${
74+
base ? ` imported from ${base}` : ''
75+
}${relError ? '; targets must start with "./"' : ''}`
76+
)
77+
}
78+
79+
return `Invalid "${
80+
isImport ? 'imports' : 'exports'
81+
}" target ${JSON.stringify(
82+
target
83+
)} defined for '${key}' in the package config ${pkgPath}package.json${
84+
base ? ` imported from ${base}` : ''
85+
}${relError ? '; targets must start with "./"' : ''}`
86+
},
87+
Error
88+
)
89+
90+
codes.ERR_MODULE_NOT_FOUND = createError(
91+
'ERR_MODULE_NOT_FOUND',
92+
/**
93+
* @param {string} path
94+
* @param {string} base
95+
* @param {string} [type]
96+
*/
97+
(path, base, type = 'package') => {
98+
return `Cannot find ${type} '${path}' imported from ${base}`
99+
},
100+
Error
101+
)
102+
103+
codes.ERR_PACKAGE_IMPORT_NOT_DEFINED = createError(
104+
'ERR_PACKAGE_IMPORT_NOT_DEFINED',
105+
/**
106+
* @param {string} specifier
107+
* @param {string} packagePath
108+
* @param {string} base
109+
*/
110+
(specifier, packagePath, base) => {
111+
return `Package import specifier "${specifier}" is not defined${
112+
packagePath ? ` in package ${packagePath}package.json` : ''
113+
} imported from ${base}`
114+
},
115+
TypeError
116+
)
117+
118+
codes.ERR_PACKAGE_PATH_NOT_EXPORTED = createError(
119+
'ERR_PACKAGE_PATH_NOT_EXPORTED',
120+
/**
121+
* @param {string} pkgPath
122+
* @param {string} subpath
123+
* @param {string} [base]
124+
*/
125+
(pkgPath, subpath, base = undefined) => {
126+
if (subpath === '.') {
127+
return `No "exports" main defined in ${pkgPath}package.json${
128+
base ? ` imported from ${base}` : ''
129+
}`
130+
}
131+
return `Package subpath '${subpath}' is not defined by "exports" in ${pkgPath}package.json${
132+
base ? ` imported from ${base}` : ''
133+
}`
134+
},
135+
Error
136+
)
137+
138+
codes.ERR_UNSUPPORTED_DIR_IMPORT = createError(
139+
'ERR_UNSUPPORTED_DIR_IMPORT',
140+
"Directory import '%s' is not supported " +
141+
'resolving ES modules imported from %s',
142+
Error
143+
)
144+
145+
codes.ERR_UNKNOWN_FILE_EXTENSION = createError(
146+
'ERR_UNKNOWN_FILE_EXTENSION',
147+
'Unknown file extension "%s" for %s',
148+
TypeError
149+
)
150+
151+
codes.ERR_INVALID_ARG_VALUE = createError(
152+
'ERR_INVALID_ARG_VALUE',
153+
/**
154+
* @param {string} name
155+
* @param {unknown} value
156+
* @param {string} [reason='is invalid']
157+
*/
158+
(name, value, reason = 'is invalid') => {
159+
let inspected = inspect(value)
160+
161+
if (inspected.length > 128) {
162+
inspected = `${inspected.slice(0, 128)}...`
163+
}
164+
165+
const type = name.includes('.') ? 'property' : 'argument'
166+
167+
return `The ${type} '${name}' ${reason}. Received ${inspected}`
168+
},
169+
TypeError
170+
// Note: extra classes have been shaken out.
171+
// , RangeError
172+
)
173+
174+
codes.ERR_UNSUPPORTED_ESM_URL_SCHEME = createError(
175+
'ERR_UNSUPPORTED_ESM_URL_SCHEME',
176+
/**
177+
* @param {URL} url
178+
*/
179+
(url) => {
180+
let message =
181+
'Only file and data URLs are supported by the default ESM loader'
182+
183+
if (isWindows && url.protocol.length === 2) {
184+
message += '. On Windows, absolute paths must be valid file:// URLs'
185+
}
186+
187+
message += `. Received protocol '${url.protocol}'`
188+
return message
189+
},
190+
Error
191+
)
192+
193+
/**
194+
* Utility function for registering the error codes. Only used here. Exported
195+
* *only* to allow for testing.
196+
* @param {string} sym
197+
* @param {MessageFunction|string} value
198+
* @param {ErrorConstructor} def
199+
* @returns {new (...args: unknown[]) => Error}
200+
*/
201+
function createError (sym, value, def) {
202+
// Special case for SystemError that formats the error message differently
203+
// The SystemErrors only have SystemError as their base classes.
204+
messages.set(sym, value)
205+
206+
return makeNodeErrorWithCode(def, sym)
207+
}
208+
209+
/**
210+
* @param {ErrorConstructor} Base
211+
* @param {string} key
212+
* @returns {ErrorConstructor}
213+
*/
214+
function makeNodeErrorWithCode (Base, key) {
215+
// @ts-expect-error It’s a Node error.
216+
return NodeError
217+
/**
218+
* @param {unknown[]} args
219+
*/
220+
function NodeError (...args) {
221+
const limit = Error.stackTraceLimit
222+
if (isErrorStackTraceLimitWritable()) { Error.stackTraceLimit = 0 }
223+
const error = new Base()
224+
// Reset the limit and setting the name property.
225+
if (isErrorStackTraceLimitWritable()) { Error.stackTraceLimit = limit }
226+
const message = getMessage(key, args, error)
227+
Object.defineProperty(error, 'message', {
228+
value: message,
229+
enumerable: false,
230+
writable: true,
231+
configurable: true
232+
})
233+
Object.defineProperty(error, 'toString', {
234+
/** @this {Error} */
235+
value () {
236+
return `${this.name} [${key}]: ${this.message}`
237+
},
238+
enumerable: false,
239+
writable: true,
240+
configurable: true
241+
})
242+
addCodeToName(error, Base.name, key)
243+
// @ts-expect-error It’s a Node error.
244+
error.code = key
245+
return error
246+
}
247+
}
248+
249+
const addCodeToName = hideStackFrames(
250+
/**
251+
* @param {Error} error
252+
* @param {string} name
253+
* @param {string} code
254+
* @returns {void}
255+
*/
256+
function (error, name, code) {
257+
// Set the stack
258+
error = captureLargerStackTrace(error)
259+
// Add the error code to the name to include it in the stack trace.
260+
error.name = `${name} [${code}]`
261+
// Access the stack to generate the error message including the error code
262+
// from the name.
263+
error.stack // eslint-disable-line no-unused-expressions
264+
// Reset the name to the actual name.
265+
if (name === 'SystemError') {
266+
Object.defineProperty(error, 'name', {
267+
value: name,
268+
enumerable: false,
269+
writable: true,
270+
configurable: true
271+
})
272+
} else {
273+
delete error.name
274+
}
275+
}
276+
)
277+
278+
/**
279+
* @returns {boolean}
280+
*/
281+
function isErrorStackTraceLimitWritable () {
282+
const desc = Object.getOwnPropertyDescriptor(Error, 'stackTraceLimit')
283+
if (desc === undefined) {
284+
return Object.isExtensible(Error)
285+
}
286+
287+
return own.call(desc, 'writable') ? desc.writable : desc.set !== undefined
288+
}
289+
290+
/**
291+
* This function removes unnecessary frames from Node.js core errors.
292+
* @template {(...args: unknown[]) => unknown} T
293+
* @type {(fn: T) => T}
294+
*/
295+
function hideStackFrames (fn) {
296+
// We rename the functions that will be hidden to cut off the stacktrace
297+
// at the outermost one
298+
const hidden = nodeInternalPrefix + fn.name
299+
Object.defineProperty(fn, 'name', { value: hidden })
300+
return fn
301+
}
302+
303+
const captureLargerStackTrace = hideStackFrames(
304+
/**
305+
* @param {Error} error
306+
* @returns {Error}
307+
*/
308+
function (error) {
309+
const stackTraceLimitIsWritable = isErrorStackTraceLimitWritable()
310+
if (stackTraceLimitIsWritable) {
311+
userStackTraceLimit = Error.stackTraceLimit
312+
Error.stackTraceLimit = Number.POSITIVE_INFINITY
313+
}
314+
315+
Error.captureStackTrace(error)
316+
317+
// Reset the limit
318+
if (stackTraceLimitIsWritable) { Error.stackTraceLimit = userStackTraceLimit }
319+
320+
return error
321+
}
322+
)
323+
324+
/**
325+
* @param {string} key
326+
* @param {unknown[]} args
327+
* @param {Error} self
328+
* @returns {string}
329+
*/
330+
function getMessage (key, args, self) {
331+
const message = messages.get(key)
332+
333+
if (typeof message === 'function') {
334+
assert(
335+
message.length <= args.length, // Default options do not count.
336+
`Code: ${key}; The provided arguments length (${args.length}) does not ` +
337+
`match the required ones (${message.length}).`
338+
)
339+
return Reflect.apply(message, self, args)
340+
}
341+
342+
const expectedLength = (message.match(/%[dfijoOs]/g) || []).length
343+
assert(
344+
expectedLength === args.length,
345+
`Code: ${key}; The provided arguments length (${args.length}) does not ` +
346+
`match the required ones (${expectedLength}).`
347+
)
348+
if (args.length === 0) { return message }
349+
350+
args.unshift(message)
351+
return Reflect.apply(format, null, args)
352+
}

‎lib/import-meta-resolve/get-format.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Manually “tree shaken” from:
2+
// <https://github.com/nodejs/node/blob/89f592c/lib/internal/modules/esm/get_format.js>
3+
import path from 'path'
4+
import { URL, fileURLToPath } from 'url'
5+
import { getPackageType } from './resolve.js'
6+
import { codes } from './errors.js'
7+
8+
const { ERR_UNKNOWN_FILE_EXTENSION } = codes
9+
10+
const extensionFormatMap = {
11+
__proto__: null,
12+
'.cjs': 'commonjs',
13+
'.js': 'module',
14+
'.mjs': 'module'
15+
}
16+
17+
/**
18+
* @param {string} url
19+
* @returns {{format: string|null}}
20+
*/
21+
export function defaultGetFormat (url) {
22+
if (url.startsWith('node:')) {
23+
return { format: 'builtin' }
24+
}
25+
26+
const parsed = new URL(url)
27+
28+
if (parsed.protocol === 'data:') {
29+
const { 1: mime } = /^([^/]+\/[^;,]+)[^,]*?(;base64)?,/.exec(
30+
parsed.pathname
31+
) || [null, null]
32+
const format = mime === 'text/javascript' ? 'module' : null
33+
return { format }
34+
}
35+
36+
if (parsed.protocol === 'file:') {
37+
const ext = path.extname(parsed.pathname)
38+
/** @type {string} */
39+
let format
40+
if (ext === '.js') {
41+
format = getPackageType(parsed.href) === 'module' ? 'module' : 'commonjs'
42+
} else {
43+
format = extensionFormatMap[ext]
44+
}
45+
46+
if (!format) {
47+
throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url))
48+
}
49+
50+
return { format: format || null }
51+
}
52+
53+
return { format: null }
54+
}

‎lib/import-meta-resolve/index.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { moduleResolve, defaultResolve } from './resolve'
2+
3+
export { moduleResolve }
4+
5+
/**
6+
* Provides a module-relative resolution function scoped to each module,
7+
* returning the URL string.
8+
* `import.meta.resolve` also accepts a second argument which is the parent
9+
* module from which to resolve from.
10+
*
11+
* This function is asynchronous because the ES module resolver in Node.js is
12+
* allowed to be asynchronous.
13+
*
14+
* @param {string} specifier The module specifier to resolve relative to parent.
15+
* @param {string} parent The absolute parent module URL to resolve from.
16+
* You should pass `import.meta.url` or something else
17+
* @returns {Promise<string>}
18+
*/
19+
// eslint-disable-next-line require-await
20+
export async function resolve (specifier, parent) {
21+
if (!parent) {
22+
throw new Error(
23+
'Please pass `parent`: `import-meta-resolve` cannot ponyfill that'
24+
)
25+
}
26+
27+
try {
28+
return defaultResolve(specifier, { parentURL: parent }).url
29+
} catch (error) {
30+
return error.code === 'ERR_UNSUPPORTED_DIR_IMPORT'
31+
? error.url
32+
: Promise.reject(error)
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Manually “tree shaken” from:
2+
// <https://github.com/nodejs/node/blob/89f592c/lib/internal/modules/package_json_reader.js>
3+
// Removed the native dependency.
4+
// Also: no need to cache, we do that in resolve already.
5+
6+
import fs from 'fs'
7+
import path from 'path'
8+
9+
const reader = { read }
10+
export default reader
11+
12+
/**
13+
* @param {string} jsonPath
14+
* @returns {{string: string}}
15+
*/
16+
function read (jsonPath) {
17+
return find(path.dirname(jsonPath))
18+
}
19+
20+
/**
21+
* @param {string} dir
22+
* @returns {{string: string}}
23+
*/
24+
function find (dir) {
25+
try {
26+
const string = fs.readFileSync(
27+
path.toNamespacedPath(path.join(dir, 'package.json')),
28+
'utf8'
29+
)
30+
return { string }
31+
} catch (error) {
32+
if (error.code === 'ENOENT') {
33+
const parent = path.dirname(dir)
34+
if (dir !== parent) { return find(parent) }
35+
return { string: undefined }
36+
// Throw all other errors.
37+
/* c8 ignore next 4 */
38+
}
39+
40+
throw error
41+
}
42+
}

‎lib/import-meta-resolve/resolve.js

+1,064
Large diffs are not rendered by default.

‎package.json

-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"test": "pnpm lint && vitest run"
2525
},
2626
"dependencies": {
27-
"import-meta-resolve": "^1.1.1",
2827
"pathe": "^0.2.0",
2928
"pkg-types": "^0.3.2"
3029
},

‎src/resolve.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { existsSync, realpathSync } from 'fs'
22
import { pathToFileURL } from 'url'
33
import { isAbsolute } from 'pathe'
4-
import { moduleResolve } from 'import-meta-resolve'
4+
import { moduleResolve } from '../lib/import-meta-resolve'
55
import { fileURLToPath, normalizeid } from './utils'
66
import { pcall, BUILTIN_MODULES } from './_utils'
77

0 commit comments

Comments
 (0)
Please sign in to comment.