/
commonjs-executor.ts
254 lines (219 loc) · 8.24 KB
/
commonjs-executor.ts
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/* eslint-disable antfu/no-cjs-exports */
import vm from 'node:vm'
import { Module as _Module, createRequire } from 'node:module'
import { basename, dirname, extname } from 'pathe'
import { isNodeBuiltin } from 'vite-node/utils'
import type { ImportModuleDynamically, VMModule } from './types'
import type { FileMap } from './file-map'
interface CommonjsExecutorOptions {
fileMap: FileMap
context: vm.Context
importModuleDynamically: ImportModuleDynamically
}
const _require = createRequire(import.meta.url)
interface PrivateNodeModule extends NodeModule {
_compile(code: string, filename: string): void
}
const requiresCache = new WeakMap<NodeModule, NodeRequire>()
export class CommonjsExecutor {
private context: vm.Context
private requireCache = new Map<string, NodeModule>()
private publicRequireCache = this.createProxyCache()
private moduleCache = new Map<string, VMModule | Promise<VMModule>>()
private builtinCache: Record<string, NodeModule> = Object.create(null)
private extensions: Record<string, (m: NodeModule, filename: string) => unknown> = Object.create(null)
private fs: FileMap
private Module: typeof _Module
constructor(options: CommonjsExecutorOptions) {
this.context = options.context
this.fs = options.fileMap
const primitives = vm.runInContext('({ Object, Array, Error })', this.context) as {
Object: typeof Object
Array: typeof Array
Error: typeof Error
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const executor = this
this.Module = class Module {
exports: any
isPreloading = false
id: string
filename: string
loaded: boolean
parent: null | Module | undefined
children: Module[] = []
path: string
paths: string[] = []
constructor(id = '', parent?: Module) {
this.exports = primitives.Object.create(Object.prototype)
// in our case the path should always be resolved already
this.path = dirname(id)
this.id = id
this.filename = id
this.loaded = false
this.parent = parent
}
get require() {
const require = requiresCache.get(this)
if (require)
return require
const _require = Module.createRequire(this.id)
requiresCache.set(this, _require)
return _require
}
_compile(code: string, filename: string) {
const cjsModule = Module.wrap(code)
const script = new vm.Script(cjsModule, {
filename,
importModuleDynamically: options.importModuleDynamically,
} as any)
// @ts-expect-error mark script with current identifier
script.identifier = filename
const fn = script.runInContext(executor.context)
const __dirname = dirname(filename)
executor.requireCache.set(filename, this)
try {
fn(this.exports, this.require, this, filename, __dirname)
return this.exports
}
finally {
this.loaded = true
}
}
// exposed for external use, Node.js does the opposite
static _load = (request: string, parent: Module | undefined, _isMain: boolean) => {
const require = Module.createRequire(parent?.filename ?? request)
return require(request)
}
static wrap = (script: string) => {
return Module.wrapper[0] + script + Module.wrapper[1]
}
static wrapper = new primitives.Array(
'(function (exports, require, module, __filename, __dirname) { ',
'\n});',
)
static builtinModules = _Module.builtinModules
static findSourceMap = _Module.findSourceMap
static SourceMap = _Module.SourceMap
static syncBuiltinESMExports = _Module.syncBuiltinESMExports
static _cache = executor.moduleCache
static _extensions = executor.extensions
static createRequire = (filename: string) => {
return executor.createRequire(filename)
}
static runMain = () => {
throw new primitives.Error('[vitest] "runMain" is not implemented.')
}
// @ts-expect-error not typed
static _resolveFilename = _Module._resolveFilename
// @ts-expect-error not typed
static _findPath = _Module._findPath
// @ts-expect-error not typed
static _initPaths = _Module._initPaths
// @ts-expect-error not typed
static _preloadModules = _Module._preloadModules
// @ts-expect-error not typed
static _resolveLookupPaths = _Module._resolveLookupPaths
// @ts-expect-error not typed
static globalPaths = _Module.globalPaths
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore not typed in lower versions
static isBuiltin = _Module.isBuiltin
static Module = Module
}
this.extensions['.js'] = this.requireJs
this.extensions['.json'] = this.requireJson
}
private requireJs = (m: NodeModule, filename: string) => {
const content = this.fs.readFile(filename)
;(m as PrivateNodeModule)._compile(content, filename)
}
private requireJson = (m: NodeModule, filename: string) => {
const code = this.fs.readFile(filename)
m.exports = JSON.parse(code)
}
public createRequire = (filename: string) => {
const _require = createRequire(filename)
const require = ((id: string) => {
const resolved = _require.resolve(id)
const ext = extname(resolved)
if (ext === '.node' || isNodeBuiltin(resolved))
return this.requireCoreModule(resolved)
const module = new this.Module(resolved)
return this.loadCommonJSModule(module, resolved)
}) as NodeRequire
require.resolve = _require.resolve
Object.defineProperty(require, 'extensions', {
get: () => this.extensions,
set: () => {},
configurable: true,
})
require.main = undefined // there is no main, since we are running tests using ESM
require.cache = this.publicRequireCache
return require
}
private createProxyCache() {
return new Proxy(Object.create(null), {
defineProperty: () => true,
deleteProperty: () => true,
set: () => true,
get: (_, key: string) => this.requireCache.get(key),
has: (_, key: string) => this.requireCache.has(key),
ownKeys: () => Array.from(this.requireCache.keys()),
getOwnPropertyDescriptor() {
return {
configurable: true,
enumerable: true,
}
},
})
}
// very naive implementation for Node.js require
private loadCommonJSModule(module: NodeModule, filename: string): Record<string, unknown> {
const cached = this.requireCache.get(filename)
if (cached)
return cached.exports
const extension = this.findLongestRegisteredExtension(filename)
const loader = this.extensions[extension] || this.extensions['.js']
loader(module, filename)
return module.exports
}
private findLongestRegisteredExtension(filename: string) {
const name = basename(filename)
let currentExtension: string
let index: number
let startIndex = 0
// eslint-disable-next-line no-cond-assign
while ((index = name.indexOf('.', startIndex)) !== -1) {
startIndex = index + 1
if (index === 0)
continue // Skip dotfiles like .gitignore
currentExtension = (name.slice(index))
if (this.extensions[currentExtension])
return currentExtension
}
return '.js'
}
public require(identifier: string) {
const ext = extname(identifier)
if (ext === '.node' || isNodeBuiltin(identifier))
return this.requireCoreModule(identifier)
const module = new this.Module(identifier)
return this.loadCommonJSModule(module, identifier)
}
private requireCoreModule(identifier: string) {
const normalized = identifier.replace(/^node:/, '')
if (this.builtinCache[normalized])
return this.builtinCache[normalized].exports
const moduleExports = _require(identifier)
if (identifier === 'node:module' || identifier === 'module') {
const module = new this.Module('/module.js') // path should not matter
module.exports = this.Module
this.builtinCache[normalized] = module
return module.exports
}
this.builtinCache[normalized] = _require.cache[normalized]!
// TODO: should we wrapp module to rethrow context errors?
return moduleExports
}
}