Skip to content

Commit

Permalink
feat(middleware): issues warnings when using node.js global APIs in m…
Browse files Browse the repository at this point in the history
…iddleware (#36980)
  • Loading branch information
feugy authored May 23, 2022
1 parent 6f2a8d3 commit 4e6b6a5
Show file tree
Hide file tree
Showing 9 changed files with 440 additions and 29 deletions.
86 changes: 78 additions & 8 deletions packages/next/build/webpack/plugins/middleware-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getSortedRoutes } from '../../../shared/lib/router/utils'
import { webpack, sources, webpack5 } from 'next/dist/compiled/webpack/webpack'
import {
EDGE_RUNTIME_WEBPACK,
EDGE_UNSUPPORTED_NODE_APIS,
MIDDLEWARE_BUILD_MANIFEST,
MIDDLEWARE_FLIGHT_MANIFEST,
MIDDLEWARE_MANIFEST,
Expand Down Expand Up @@ -113,7 +114,7 @@ function getCodeAnalizer(params: {
* but actually execute the expression.
*/
const handleWrapExpression = (expr: any) => {
if (parser.state.module?.layer !== 'middleware') {
if (!isInMiddlewareLayer(parser)) {
return
}

Expand Down Expand Up @@ -141,7 +142,7 @@ function getCodeAnalizer(params: {
* module path that is using it.
*/
const handleExpression = () => {
if (parser.state.module?.layer !== 'middleware') {
if (!isInMiddlewareLayer(parser)) {
return
}

Expand Down Expand Up @@ -175,7 +176,7 @@ function getCodeAnalizer(params: {
}

buildInfo.nextUsedEnvVars.add(members[1])
if (parser.state.module?.layer !== 'middleware') {
if (!isInMiddlewareLayer(parser)) {
return true
}
}
Expand All @@ -187,7 +188,7 @@ function getCodeAnalizer(params: {
const handleNewResponseExpression = (node: any) => {
const firstParameter = node?.arguments?.[0]
if (
isUserMiddlewareUserFile(parser.state.current) &&
isInMiddlewareFile(parser) &&
firstParameter &&
!isNullLiteral(firstParameter) &&
!isUndefinedIdentifier(firstParameter)
Expand All @@ -210,8 +211,7 @@ function getCodeAnalizer(params: {
* A noop handler to skip analyzing some cases.
* Order matters: for it to work, it must be registered first
*/
const skip = () =>
parser.state.module?.layer === 'middleware' ? true : undefined
const skip = () => (isInMiddlewareLayer(parser) ? true : undefined)

for (const prefix of ['', 'global.']) {
hooks.expression.for(`${prefix}Function.prototype`).tap(NAME, skip)
Expand All @@ -226,6 +226,7 @@ function getCodeAnalizer(params: {
hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression)
hooks.callMemberChain.for('process').tap(NAME, handleCallMemberChain)
hooks.expressionMemberChain.for('process').tap(NAME, handleCallMemberChain)
registerUnsupportedApiHooks(parser, compilation)
}
}

Expand Down Expand Up @@ -454,9 +455,78 @@ function getEntryFiles(entryFiles: string[], meta: EntryMetadata) {
return files
}

function isUserMiddlewareUserFile(module: any) {
function registerUnsupportedApiHooks(
parser: webpack5.javascript.JavascriptParser,
compilation: webpack5.Compilation
) {
const { WebpackError } = compilation.compiler.webpack
for (const expression of EDGE_UNSUPPORTED_NODE_APIS) {
const warnForUnsupportedApi = (node: any) => {
if (!isInMiddlewareLayer(parser)) {
return
}
compilation.warnings.push(
makeUnsupportedApiError(WebpackError, parser, expression, node.loc)
)
return true
}
parser.hooks.call.for(expression).tap(NAME, warnForUnsupportedApi)
parser.hooks.expression.for(expression).tap(NAME, warnForUnsupportedApi)
parser.hooks.callMemberChain
.for(expression)
.tap(NAME, warnForUnsupportedApi)
parser.hooks.expressionMemberChain
.for(expression)
.tap(NAME, warnForUnsupportedApi)
}

const warnForUnsupportedProcessApi = (node: any, [callee]: string[]) => {
if (!isInMiddlewareLayer(parser) || callee === 'env') {
return
}
compilation.warnings.push(
makeUnsupportedApiError(
WebpackError,
parser,
`process.${callee}`,
node.loc
)
)
return true
}

parser.hooks.callMemberChain
.for('process')
.tap(NAME, warnForUnsupportedProcessApi)
parser.hooks.expressionMemberChain
.for('process')
.tap(NAME, warnForUnsupportedProcessApi)
}

function makeUnsupportedApiError(
WebpackError: typeof webpack5.WebpackError,
parser: webpack5.javascript.JavascriptParser,
name: string,
loc: any
) {
const error = new WebpackError(
`You're using a Node.js API (${name} at line: ${loc.start.line}) which is not supported in the Edge Runtime that Middleware uses.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`
)
error.name = NAME
error.module = parser.state.current
error.loc = loc
return error
}

function isInMiddlewareLayer(parser: webpack5.javascript.JavascriptParser) {
return parser.state.module?.layer === 'middleware'
}

function isInMiddlewareFile(parser: webpack5.javascript.JavascriptParser) {
return (
module.layer === 'middleware' && /middleware\.\w+$/.test(module.rawRequest)
parser.state.current?.layer === 'middleware' &&
/middleware\.\w+$/.test(parser.state.current?.rawRequest)
)
}

Expand Down
106 changes: 85 additions & 21 deletions packages/next/server/web/sandbox/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from 'next/dist/compiled/abort-controller'
import vm from 'vm'
import type { WasmBinding } from '../../../build/webpack/loaders/get-module-build-info'
import { EDGE_UNSUPPORTED_NODE_APIS } from '../../../shared/lib/constants'

const WEBPACK_HASH_REGEX =
/__webpack_require__\.h = function\(\) \{ return "[0-9a-f]+"; \}/g
Expand Down Expand Up @@ -47,19 +48,21 @@ const caches = new Map<
}
>()

interface ModuleContextOptions {
module: string
onWarning: (warn: Error) => void
useCache: boolean
env: string[]
wasm: WasmBinding[]
}

/**
* 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[]
}) {
export async function getModuleContext(options: ModuleContextOptions) {
let moduleCache = options.useCache
? caches.get(options.module)
: await createModuleContext(options)
Expand Down Expand Up @@ -97,12 +100,7 @@ export async function getModuleContext(options: {
* 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[]
}) {
async function createModuleContext(options: ModuleContextOptions) {
const requireCache = new Map([
[require.resolve('next/dist/compiled/cookie'), { exports: cookie }],
])
Expand Down Expand Up @@ -181,11 +179,10 @@ async function createModuleContext(options: {
* 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 } = {
function createContext(
options: Pick<ModuleContextOptions, 'env' | 'onWarning'>
) {
const context: Context = {
_ENTRIES: {},
atob: polyfills.atob,
Blob,
Expand All @@ -207,19 +204,20 @@ function createContext(options: {
CryptoKey: polyfills.CryptoKey,
Crypto: polyfills.Crypto,
crypto: new polyfills.Crypto(),
DataView,
File,
FormData,
process: {
env: buildEnvironmentVariablesFrom(options.env),
},
process: createProcessPolyfill(options),
ReadableStream,
setInterval,
setTimeout,
queueMicrotask,
TextDecoder,
TextEncoder,
TransformStream,
URL,
URLSearchParams,
WebAssembly,

// Indexed collections
Array,
Expand All @@ -244,6 +242,18 @@ function createContext(options: {
// Structured data
ArrayBuffer,
SharedArrayBuffer,

// These APIs are supported by the Edge runtime, but not by the version of Node.js we're using
// Since we'll soon replace this sandbox with the edge-runtime itself, it's not worth polyfilling.
// ReadableStreamBYOBReader,
// ReadableStreamDefaultReader,
// structuredClone,
// SubtleCrypto,
// WritableStream,
// WritableStreamDefaultWriter,
}
for (const name of EDGE_UNSUPPORTED_NODE_APIS) {
addStub(context, name, options)
}

// Self references
Expand Down Expand Up @@ -286,3 +296,57 @@ async function loadWasm(

return modules
}

function createProcessPolyfill(
options: Pick<ModuleContextOptions, 'env' | 'onWarning'>
) {
const env = buildEnvironmentVariablesFrom(options.env)

const processPolyfill = { env }
const overridenValue: Record<string, any> = {}
for (const key of Object.keys(process)) {
if (key === 'env') continue
Object.defineProperty(processPolyfill, key, {
get() {
emitWarning(`process.${key}`, options)
return overridenValue[key]
},
set(value) {
overridenValue[key] = value
},
enumerable: false,
})
}
return processPolyfill
}

const warnedAlready = new Set<string>()

function addStub(
context: Context,
name: string,
contextOptions: Pick<ModuleContextOptions, 'onWarning'>
) {
Object.defineProperty(context, name, {
get() {
emitWarning(name, contextOptions)
return undefined
},
enumerable: false,
})
}

function emitWarning(
name: string,
contextOptions: Pick<ModuleContextOptions, 'onWarning'>
) {
if (!warnedAlready.has(name)) {
const warning =
new Error(`You're using a Node.js API (${name}) which is not supported in the Edge Runtime that Middleware uses.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`)
warning.name = 'NodejsRuntimeApiInMiddlewareWarning'
contextOptions.onWarning(warning)
console.warn(warning.message)
warnedAlready.add(name)
}
}
28 changes: 28 additions & 0 deletions packages/next/shared/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,31 @@ export const OPTIMIZED_FONT_PROVIDERS = [
]
export const STATIC_STATUS_PAGES = ['/500']
export const TRACE_OUTPUT_VERSION = 1

// comparing
// https://nextjs.org/docs/api-reference/edge-runtime
// with
// https://nodejs.org/docs/latest/api/globals.html
export const EDGE_UNSUPPORTED_NODE_APIS = [
'clearImmediate',
'setImmediate',
'BroadcastChannel',
'Buffer',
'ByteLengthQueuingStrategy',
'CompressionStream',
'CountQueuingStrategy',
'DecompressionStream',
'DomException',
'Event',
'EventTarget',
'MessageChannel',
'MessageEvent',
'MessagePort',
'ReadableByteStreamController',
'ReadableStreamBYOBRequest',
'ReadableStreamDefaultController',
'TextDecoderStream',
'TextEncoderStream',
'TransformStreamDefaultController',
'WritableStreamDefaultController',
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function middleware() {
process.cwd = () => 'fixed-value'
console.log(process.cwd(), process.env)
return new Response()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Home() {
return <div>A page</div>
}
Loading

0 comments on commit 4e6b6a5

Please sign in to comment.