forked from vercel/next.js
/
context.ts
287 lines (260 loc) · 7.55 KB
/
context.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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import type { Context } from 'vm'
import { Blob, File, FormData } from 'next/dist/compiled/formdata-node'
import { readFileSync, promises as fs } from 'fs'
import { requireDependencies } from './require'
import { TransformStream } from 'next/dist/compiled/web-streams-polyfill'
import cookie from 'next/dist/compiled/cookie'
import * as polyfills from './polyfills'
import {
AbortController,
AbortSignal,
} from 'next/dist/compiled/abort-controller'
import vm from 'vm'
import type { WasmBinding } from '../../../build/webpack/loaders/next-middleware-wasm-loader'
const WEBPACK_HASH_REGEX =
/__webpack_require__\.h = function\(\) \{ return "[0-9a-f]+"; \}/g
/**
* For a given path a context, this function checks if there is any module
* context that contains the path with an older content and, if that's the
* case, removes the context from the cache.
*/
export function clearModuleContext(path: string, content: Buffer | string) {
for (const [key, cache] of caches) {
const prev = cache?.paths.get(path)?.replace(WEBPACK_HASH_REGEX, '')
if (
typeof prev !== 'undefined' &&
prev !== content.toString().replace(WEBPACK_HASH_REGEX, '')
) {
caches.delete(key)
}
}
}
/**
* A Map of cached module contexts indexed by the module name. It allows
* to have a different cache scoped per module name or depending on the
* provided module key on creation.
*/
const caches = new Map<
string,
{
context: Context
paths: Map<string, string>
require: Map<string, any>
warnedEvals: Set<string>
}
>()
/**
* For a given module name this function will create a context for the
* runtime. It returns a function where we can provide a module path and
* run in within the context. It may or may not use a cache depending on
* the parameters.
*/
export async function getModuleContext(options: {
module: string
onWarning: (warn: Error) => void
useCache: boolean
env: string[]
wasm: WasmBinding[]
}) {
let moduleCache = options.useCache
? caches.get(options.module)
: await createModuleContext(options)
if (!moduleCache) {
moduleCache = await createModuleContext(options)
caches.set(options.module, moduleCache)
}
return {
context: moduleCache.context,
runInContext: (paramPath: string) => {
if (!moduleCache!.paths.has(paramPath)) {
const content = readFileSync(paramPath, 'utf-8')
try {
vm.runInNewContext(content, moduleCache!.context, {
filename: paramPath,
})
moduleCache!.paths.set(paramPath, content)
} catch (error) {
if (options.useCache) {
caches.delete(options.module)
}
throw error
}
}
},
}
}
/**
* Create a module cache specific for the provided parameters. It includes
* a context, require cache and paths cache and loads three types:
* 1. Dependencies that hold no runtime dependencies.
* 2. Dependencies that require runtime globals such as Blob.
* 3. Dependencies that are scoped for the provided parameters.
*/
async function createModuleContext(options: {
onWarning: (warn: Error) => void
module: string
env: string[]
wasm: WasmBinding[]
}) {
const requireCache = new Map([
[require.resolve('next/dist/compiled/cookie'), { exports: cookie }],
])
const context = createContext(options)
requireDependencies({
requireCache: requireCache,
context: context,
dependencies: [
{
path: require.resolve('../spec-compliant/headers'),
mapExports: { Headers: 'Headers' },
},
{
path: require.resolve('../spec-compliant/response'),
mapExports: { Response: 'Response' },
},
{
path: require.resolve('../spec-compliant/request'),
mapExports: { Request: 'Request' },
},
],
})
const moduleCache = {
context: context,
paths: new Map<string, string>(),
require: requireCache,
warnedEvals: new Set<string>(),
}
context.__next_eval__ = function __next_eval__(fn: Function) {
const key = fn.toString()
if (!moduleCache.warnedEvals.has(key)) {
const warning = new Error(
`Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware`
)
warning.name = 'DynamicCodeEvaluationWarning'
Error.captureStackTrace(warning, __next_eval__)
moduleCache.warnedEvals.add(key)
options.onWarning(warning)
}
return fn()
}
context.fetch = (input: RequestInfo, init: RequestInit = {}) => {
init.headers = new Headers(init.headers ?? {})
const prevs = init.headers.get(`x-middleware-subrequest`)?.split(':') || []
const value = prevs.concat(options.module).join(':')
init.headers.set('x-middleware-subrequest', value)
if (!init.headers.has('user-agent')) {
init.headers.set(`user-agent`, `Next.js Middleware`)
}
if (typeof input === 'object' && 'url' in input) {
return fetch(input.url, {
...init,
headers: {
...Object.fromEntries(input.headers),
...Object.fromEntries(init.headers),
},
})
}
return fetch(String(input), init)
}
Object.assign(context, await loadWasm(options.wasm))
return moduleCache
}
/**
* Create a base context with all required globals for the runtime that
* won't depend on any externally provided dependency.
*/
function createContext(options: {
/** Environment variables to be provided to the context */
env: string[]
}) {
const context: { [key: string]: unknown } = {
_ENTRIES: {},
atob: polyfills.atob,
Blob,
btoa: polyfills.btoa,
clearInterval,
clearTimeout,
console: {
assert: console.assert.bind(console),
error: console.error.bind(console),
info: console.info.bind(console),
log: console.log.bind(console),
time: console.time.bind(console),
timeEnd: console.timeEnd.bind(console),
timeLog: console.timeLog.bind(console),
warn: console.warn.bind(console),
},
AbortController: AbortController,
AbortSignal: AbortSignal,
CryptoKey: polyfills.CryptoKey,
Crypto: polyfills.Crypto,
crypto: new polyfills.Crypto(),
File,
FormData,
process: {
...polyfills.process,
env: buildEnvironmentVariablesFrom(options.env),
},
ReadableStream: polyfills.ReadableStream,
setInterval,
setTimeout,
TextDecoder,
TextEncoder,
TransformStream,
URL,
URLSearchParams,
// Indexed collections
Array,
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array,
BigInt64Array,
BigUint64Array,
// Keyed collections
Map,
Set,
WeakMap,
WeakSet,
// Structured data
ArrayBuffer,
SharedArrayBuffer,
}
// Self references
context.self = context
context.globalThis = context
return vm.createContext(context, {
codeGeneration:
process.env.NODE_ENV === 'production'
? {
strings: false,
wasm: false,
}
: undefined,
})
}
function buildEnvironmentVariablesFrom(
keys: string[]
): Record<string, string | undefined> {
const pairs = keys.map((key) => [key, process.env[key]])
return Object.fromEntries(pairs)
}
async function loadWasm(
wasm: WasmBinding[]
): Promise<Record<string, WebAssembly.Module>> {
const modules: Record<string, WebAssembly.Module> = {}
await Promise.all(
wasm.map(async (binding) => {
const module = await WebAssembly.compile(
await fs.readFile(binding.filePath)
)
modules[binding.name] = module
})
)
return modules
}