[edge] support Node.js core modules in edge runtime (#47191)
This PR enables Node.js core modules in edge runtime by leaving a `require` statement in the output source as externals - [x] buffer - [ ] async_hooks - [ ] util - [ ] assert - [ ] events
This commit is contained in:
parent
d760c00961
commit
524bcd563b
9 changed files with 209 additions and 1 deletions
|
@ -35,6 +35,7 @@ import { finalizeEntrypoint } from './entries'
|
|||
import * as Log from './output/log'
|
||||
import { buildConfiguration } from './webpack/config'
|
||||
import MiddlewarePlugin, {
|
||||
getEdgePolyfilledModules,
|
||||
handleWebpackExternalForEdgeRuntime,
|
||||
} from './webpack/plugins/middleware-plugin'
|
||||
import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin'
|
||||
|
@ -1460,6 +1461,7 @@ export default async function getBaseWebpackConfig(
|
|||
'./cjs/react-dom-server-legacy.browser.development.js':
|
||||
'{}',
|
||||
},
|
||||
getEdgePolyfilledModules(),
|
||||
handleWebpackExternalForEdgeRuntime,
|
||||
]
|
||||
: []),
|
||||
|
|
|
@ -858,6 +858,23 @@ export default class MiddlewarePlugin {
|
|||
}
|
||||
}
|
||||
|
||||
const supportedEdgePolyfills = new Set([
|
||||
'buffer',
|
||||
'events',
|
||||
'assert',
|
||||
'util',
|
||||
'async_hooks',
|
||||
])
|
||||
|
||||
export function getEdgePolyfilledModules() {
|
||||
const records: Record<string, string> = {}
|
||||
for (const mod of supportedEdgePolyfills) {
|
||||
records[mod] = `commonjs node:${mod}`
|
||||
records[`node:${mod}`] = `commonjs node:${mod}`
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
export async function handleWebpackExternalForEdgeRuntime({
|
||||
request,
|
||||
context,
|
||||
|
@ -869,7 +886,11 @@ export async function handleWebpackExternalForEdgeRuntime({
|
|||
contextInfo: any
|
||||
getResolve: () => any
|
||||
}) {
|
||||
if (contextInfo.issuerLayer === 'middleware' && isNodeJsModule(request)) {
|
||||
if (
|
||||
contextInfo.issuerLayer === 'middleware' &&
|
||||
isNodeJsModule(request) &&
|
||||
!supportedEdgePolyfills.has(request)
|
||||
) {
|
||||
// allows user to provide and use their polyfills, as we do with buffer.
|
||||
try {
|
||||
await getResolve()(context, request)
|
||||
|
|
|
@ -16,6 +16,11 @@ import { fetchInlineAsset } from './fetch-inline-assets'
|
|||
import type { EdgeFunctionDefinition } from '../../../build/webpack/plugins/middleware-plugin'
|
||||
import { UnwrapPromise } from '../../../lib/coalesced-function'
|
||||
import { runInContext } from 'vm'
|
||||
import BufferImplementation from 'node:buffer'
|
||||
import EventsImplementation from 'node:events'
|
||||
import AssertImplementation from 'node:assert'
|
||||
import UtilImplementation from 'node:util'
|
||||
import AsyncHooksImplementation from 'node:async_hooks'
|
||||
|
||||
const WEBPACK_HASH_REGEX =
|
||||
/__webpack_require__\.h = function\(\) \{ return "[0-9a-f]+"; \}/g
|
||||
|
@ -139,6 +144,45 @@ function getDecorateUnhandledRejection(runtime: EdgeRuntime) {
|
|||
}
|
||||
}
|
||||
|
||||
const NativeModuleMap = new Map<string, unknown>([
|
||||
[
|
||||
'node:buffer',
|
||||
pick(BufferImplementation, [
|
||||
'constants',
|
||||
'kMaxLength',
|
||||
'kStringMaxLength',
|
||||
'Buffer',
|
||||
'SlowBuffer',
|
||||
]),
|
||||
],
|
||||
[
|
||||
'node:events',
|
||||
pick(EventsImplementation, [
|
||||
'EventEmitter',
|
||||
'captureRejectionSymbol',
|
||||
'defaultMaxListeners',
|
||||
'errorMonitor',
|
||||
'listenerCount',
|
||||
'on',
|
||||
'once',
|
||||
]),
|
||||
],
|
||||
[
|
||||
'node:async_hooks',
|
||||
pick(AsyncHooksImplementation, ['AsyncLocalStorage', 'AsyncResource']),
|
||||
],
|
||||
[
|
||||
'node:assert',
|
||||
// TODO: check if need to pick specific properties
|
||||
AssertImplementation,
|
||||
],
|
||||
[
|
||||
'node:util',
|
||||
// TODO: check if need to pick specific properties
|
||||
UtilImplementation,
|
||||
],
|
||||
])
|
||||
|
||||
/**
|
||||
* Create a module cache specific for the provided parameters. It includes
|
||||
* a runtime context, require cache and paths cache.
|
||||
|
@ -155,6 +199,17 @@ async function createModuleContext(options: ModuleContextOptions) {
|
|||
extend: (context) => {
|
||||
context.process = createProcessPolyfill(options)
|
||||
|
||||
Object.defineProperty(context, 'require', {
|
||||
enumerable: false,
|
||||
value: (id: string) => {
|
||||
const value = NativeModuleMap.get(id)
|
||||
if (!value) {
|
||||
throw TypeError('Native module not found: ' + id)
|
||||
}
|
||||
return value
|
||||
},
|
||||
})
|
||||
|
||||
context.__next_eval__ = function __next_eval__(fn: Function) {
|
||||
const key = fn.toString()
|
||||
if (!warnedEvals.has(key)) {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import B from 'node:buffer'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* @param {Request} req
|
||||
*/
|
||||
export async function POST(req) {
|
||||
const text = await req.text()
|
||||
const buf = B.Buffer.from(text)
|
||||
return NextResponse.json({
|
||||
'Buffer === B.Buffer': B.Buffer === Buffer,
|
||||
encoded: buf.toString('base64'),
|
||||
exposedKeys: Object.keys(B),
|
||||
})
|
||||
}
|
||||
|
||||
export const runtime = 'edge'
|
|
@ -0,0 +1,7 @@
|
|||
export default function Root({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import { createNextDescribe } from 'e2e-utils'
|
||||
|
||||
createNextDescribe(
|
||||
'edge runtime node compatibility',
|
||||
{
|
||||
files: __dirname,
|
||||
},
|
||||
({ next }) => {
|
||||
it('[app] supports node:buffer', async () => {
|
||||
const res = await next.fetch('/buffer', {
|
||||
method: 'POST',
|
||||
body: 'Hello, world!',
|
||||
})
|
||||
const json = await res.json()
|
||||
expect(json).toEqual({
|
||||
'Buffer === B.Buffer': true,
|
||||
encoded: Buffer.from('Hello, world!').toString('base64'),
|
||||
exposedKeys: [
|
||||
'constants',
|
||||
'kMaxLength',
|
||||
'kStringMaxLength',
|
||||
'Buffer',
|
||||
'SlowBuffer',
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('[pages/api] supports node:buffer', async () => {
|
||||
const res = await next.fetch('/api/buffer', {
|
||||
method: 'POST',
|
||||
body: 'Hello, world!',
|
||||
})
|
||||
const json = await res.json()
|
||||
expect(json).toEqual({
|
||||
'B2.Buffer === B.Buffer': true,
|
||||
'Buffer === B.Buffer': true,
|
||||
'typeof B.Buffer': 'function',
|
||||
'typeof B2.Buffer': 'function',
|
||||
'typeof Buffer': 'function',
|
||||
encoded: 'SGVsbG8sIHdvcmxkIQ==',
|
||||
exposedKeys: [
|
||||
'constants',
|
||||
'kMaxLength',
|
||||
'kStringMaxLength',
|
||||
'Buffer',
|
||||
'SlowBuffer',
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* @type {import('next').NextConfig}
|
||||
*/
|
||||
const nextConfig = {
|
||||
experimental: { appDir: true },
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
|
@ -0,0 +1,22 @@
|
|||
import B from 'node:buffer'
|
||||
import B2 from 'buffer'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const config = { runtime: 'edge' }
|
||||
|
||||
/**
|
||||
* @param {Request} req
|
||||
*/
|
||||
export default async function (req) {
|
||||
const text = await req.text()
|
||||
const buf = B.Buffer.from(text)
|
||||
return NextResponse.json({
|
||||
'Buffer === B.Buffer': B.Buffer === Buffer,
|
||||
'B2.Buffer === B.Buffer': B.Buffer === B2.Buffer,
|
||||
'typeof Buffer': typeof Buffer,
|
||||
'typeof B.Buffer': typeof B.Buffer,
|
||||
'typeof B2.Buffer': typeof B2.Buffer,
|
||||
encoded: buf.toString('base64'),
|
||||
exposedKeys: Object.keys(B),
|
||||
})
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
Loading…
Reference in a new issue