From e2506411a1cf287326082d2c326e58e8cc27a7a9 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 5 Nov 2021 19:04:04 +0100 Subject: [PATCH 1/4] v12.0.3 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-next/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next/package.json | 12 ++++++------ packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- 14 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lerna.json b/lerna.json index cf47322600eeb40..5c86aada014bbaa 100644 --- a/lerna.json +++ b/lerna.json @@ -17,5 +17,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "12.0.3-canary.10" + "version": "12.0.3" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 952694a8e426a37..9875456635b489e 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "12.0.3-canary.10", + "version": "12.0.3", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 58693b1c5786aaa..533628057232fbe 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "12.0.3-canary.10", + "version": "12.0.3", "description": "ESLint configuration used by NextJS.", "main": "index.js", "license": "MIT", @@ -9,7 +9,7 @@ "directory": "packages/eslint-config-next" }, "dependencies": { - "@next/eslint-plugin-next": "12.0.3-canary.10", + "@next/eslint-plugin-next": "12.0.3", "@rushstack/eslint-patch": "^1.0.6", "@typescript-eslint/parser": "^4.20.0", "eslint-import-resolver-node": "^0.3.4", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index b5bc5ec3d4f88be..f72689e16eb8bc8 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "12.0.3-canary.10", + "version": "12.0.3", "description": "ESLint plugin for NextJS.", "main": "lib/index.js", "license": "MIT", diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 333f61e7b1742ac..e92c386705e72f4 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "12.0.3-canary.10", + "version": "12.0.3", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 4d658ea6271d755..162a4df5ee114b1 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "12.0.3-canary.10", + "version": "12.0.3", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index f3339a942dc4076..c42c4a1bc461f6a 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "12.0.3-canary.10", + "version": "12.0.3", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 968eb86ceba1f79..0fd0abf0b1d2dcb 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "12.0.3-canary.10", + "version": "12.0.3", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index eb3bf709eb76063..801d5e45a8081aa 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "12.0.3-canary.10", + "version": "12.0.3", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index e757cb91e9a694a..dcf8f87916fbe96 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "12.0.3-canary.10", + "version": "12.0.3", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index dba69a8f3bcb2a0..3ae8b07287d414d 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "12.0.3-canary.10", + "version": "12.0.3", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next/package.json b/packages/next/package.json index 3d76338db196eea..c42efe848472526 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "12.0.3-canary.10", + "version": "12.0.3", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -68,10 +68,10 @@ "@babel/runtime": "7.15.4", "@hapi/accept": "5.0.2", "@napi-rs/triples": "1.0.3", - "@next/env": "12.0.3-canary.10", - "@next/polyfill-module": "12.0.3-canary.10", - "@next/react-dev-overlay": "12.0.3-canary.10", - "@next/react-refresh-utils": "12.0.3-canary.10", + "@next/env": "12.0.3", + "@next/polyfill-module": "12.0.3", + "@next/react-dev-overlay": "12.0.3", + "@next/react-refresh-utils": "12.0.3", "acorn": "8.5.0", "assert": "2.0.0", "browserify-zlib": "0.2.0", @@ -154,7 +154,7 @@ "@babel/traverse": "7.15.0", "@babel/types": "7.15.0", "@napi-rs/cli": "1.2.1", - "@next/polyfill-nomodule": "12.0.3-canary.10", + "@next/polyfill-nomodule": "12.0.3", "@peculiar/webcrypto": "1.1.7", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index b541d0edb1defb7..90143912e97ed08 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "12.0.3-canary.10", + "version": "12.0.3", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index f50bb23451c3eb8..e4e2ef489d9f4c4 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "12.0.3-canary.10", + "version": "12.0.3", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", From 6e081e175f2abd50ac52895b4c8bb29352888e1f Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Fri, 5 Nov 2021 21:48:43 +0100 Subject: [PATCH 2/4] Update middleware eval checks (#30883) Co-authored-by: Tobias Koppers With this PR we are updating the way we check the usage of `eval` and other dynamic code evaluation (like `new Function`) for middleware. Now instead of simply showing a warning it will behave differently depending on if we are building or in development. - Development: we replace the dynamic code with a wrapper so that we print a warning only when the code is used. We don't fail in this scenario as it is possible that once the application is built the code that uses `eval` is left out. - Build: we detect with tree shaking if the code that will be bundled into the middleware includes any dynamic code and in such scenario we make the build fail as don't want to allow it for the production environment. Closes #30674 ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint` --- .../webpack/plugins/middleware-plugin.ts | 142 +++++++++++++++--- packages/next/server/dev/next-dev-server.ts | 22 ++- packages/next/server/next-server.ts | 7 + packages/next/server/web/sandbox/sandbox.ts | 34 ++++- test/integration/middleware/core/lib/utils.js | 8 + .../core}/next.config.js | 0 .../core}/pages/interface/[id]/index.js | 0 .../core}/pages/interface/_middleware.js | 0 .../core}/pages/interface/static.js | 0 .../core}/pages/redirects/_middleware.js | 0 .../core}/pages/redirects/header.js | 0 .../core}/pages/redirects/index.js | 0 .../core}/pages/redirects/new-home.js | 0 .../core}/pages/redirects/old-home.js | 0 .../core}/pages/responses/_middleware.js | 4 +- .../core}/pages/responses/deep/_middleware.js | 0 .../core}/pages/responses/deep/index.js | 0 .../core}/pages/responses/index.js | 0 .../core}/pages/rewrites/_middleware.js | 0 .../core}/pages/rewrites/a.js | 0 .../core}/pages/rewrites/about.js | 0 .../core}/pages/rewrites/b.js | 0 .../core}/pages/rewrites/index.js | 0 .../core}/test/index.test.js | 2 +- .../with-base-path}/next.config.js | 0 .../with-base-path}/pages/_middleware.js | 0 .../with-base-path}/pages/about.js | 0 .../with-base-path}/pages/index.js | 0 .../with-base-path}/test/index.test.js | 0 .../middleware/with-eval/lib/utils.js | 8 + .../middleware/with-eval/pages/_middleware.js | 19 +++ .../middleware/with-eval/test/index.test.js | 79 ++++++++++ 32 files changed, 298 insertions(+), 27 deletions(-) create mode 100644 test/integration/middleware/core/lib/utils.js rename test/integration/{middleware-core => middleware/core}/next.config.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/interface/[id]/index.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/interface/_middleware.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/interface/static.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/redirects/_middleware.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/redirects/header.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/redirects/index.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/redirects/new-home.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/redirects/old-home.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/responses/_middleware.js (95%) rename test/integration/{middleware-core => middleware/core}/pages/responses/deep/_middleware.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/responses/deep/index.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/responses/index.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/rewrites/_middleware.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/rewrites/a.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/rewrites/about.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/rewrites/b.js (100%) rename test/integration/{middleware-core => middleware/core}/pages/rewrites/index.js (100%) rename test/integration/{middleware-core => middleware/core}/test/index.test.js (99%) rename test/integration/{middleware-base-path => middleware/with-base-path}/next.config.js (100%) rename test/integration/{middleware-base-path => middleware/with-base-path}/pages/_middleware.js (100%) rename test/integration/{middleware-base-path => middleware/with-base-path}/pages/about.js (100%) rename test/integration/{middleware-base-path => middleware/with-base-path}/pages/index.js (100%) rename test/integration/{middleware-base-path => middleware/with-base-path}/test/index.test.js (100%) create mode 100644 test/integration/middleware/with-eval/lib/utils.js create mode 100644 test/integration/middleware/with-eval/pages/_middleware.js create mode 100644 test/integration/middleware/with-eval/test/index.test.js diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 4db5e3179589a83..579cfb8a230da3f 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -1,4 +1,4 @@ -import { webpack, sources } from 'next/dist/compiled/webpack/webpack' +import { webpack, sources, webpack5 } from 'next/dist/compiled/webpack/webpack' import { getMiddlewareRegex } from '../../../shared/lib/router/utils' import { getSortedRoutes } from '../../../shared/lib/router/utils' import { @@ -39,7 +39,7 @@ export default class MiddlewarePlugin { } createAssets( - compilation: any, + compilation: webpack5.Compilation, assets: any, envPerRoute: Map ) { @@ -52,6 +52,7 @@ export default class MiddlewarePlugin { } for (const entrypoint of entrypoints.values()) { + if (!entrypoint.name) continue const result = MIDDLEWARE_FULL_ROUTE_REGEX.exec(entrypoint.name) const ssrEntryInfo = ssrEntries.get(entrypoint.name) @@ -111,19 +112,21 @@ export default class MiddlewarePlugin { ) } - apply(compiler: webpack.Compiler) { + apply(compiler: webpack5.Compiler) { + const { dev } = this + const wp = compiler.webpack compiler.hooks.compilation.tap( PLUGIN_NAME, (compilation, { normalModuleFactory }) => { const envPerRoute = new Map() - compilation.hooks.finishModules.tap(PLUGIN_NAME, () => { + compilation.hooks.afterOptimizeModules.tap(PLUGIN_NAME, () => { const { moduleGraph } = compilation as any envPerRoute.clear() for (const [name, info] of compilation.entries) { if (name.match(MIDDLEWARE_ROUTE)) { - const middlewareEntries = new Set() + const middlewareEntries = new Set() const env = new Set() const addEntriesFromDependency = (dep: any) => { @@ -133,19 +136,41 @@ export default class MiddlewarePlugin { } } + const runtime = wp.util.runtime.getEntryRuntime(compilation, name) + info.dependencies.forEach(addEntriesFromDependency) info.includeDependencies.forEach(addEntriesFromDependency) const queue = new Set(middlewareEntries) for (const module of queue) { - const { buildInfo } = module as any - if (buildInfo?.usingIndirectEval) { - // @ts-ignore TODO: Remove ignore when webpack 5 is stable - const error = new webpack.WebpackError( - `\`eval\` not allowed in Middleware ${name}` + const { buildInfo } = module + if ( + !dev && + buildInfo && + isUsedByExports({ + module, + moduleGraph, + runtime, + usedByExports: buildInfo.usingIndirectEval, + }) + ) { + if ( + /node_modules[\\/]regenerator-runtime[\\/]runtime\.js/.test( + module.identifier() + ) + ) + continue + const error = new wp.WebpackError( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware ${name}${ + typeof buildInfo.usingIndirectEval !== 'boolean' + ? `\nUsed by ${Array.from( + buildInfo.usingIndirectEval + ).join(', ')}` + : '' + }` ) error.module = module - compilation.warnings.push(error) + compilation.errors.push(error) } if (buildInfo?.nextUsedEnvVars !== undefined) { @@ -167,19 +192,82 @@ export default class MiddlewarePlugin { } }) - const handler = (parser: any) => { - const flagModule = () => { - parser.state.module.buildInfo.usingIndirectEval = true + const handler = (parser: webpack5.javascript.JavascriptParser) => { + const wrapExpression = (expr: any) => { + if (dev) { + const dep1 = new wp.dependencies.ConstDependency( + '__next_eval__(function() { return ', + expr.range[0] + ) + dep1.loc = expr.loc + parser.state.module.addPresentationalDependency(dep1) + const dep2 = new wp.dependencies.ConstDependency( + '})', + expr.range[1] + ) + dep2.loc = expr.loc + parser.state.module.addPresentationalDependency(dep2) + } + expressionHandler() + return true + } + + const flagModule = ( + usedByExports: boolean | Set | undefined + ) => { + if (usedByExports === undefined) usedByExports = true + const old = parser.state.module.buildInfo.usingIndirectEval + if (old === true || usedByExports === false) return + if (!old || usedByExports === true) { + parser.state.module.buildInfo.usingIndirectEval = usedByExports + return + } + const set = new Set(old) + for (const item of usedByExports) { + set.add(item) + } + parser.state.module.buildInfo.usingIndirectEval = set + } + + const expressionHandler = () => { + wp.optimize.InnerGraph.onUsage(parser.state, flagModule) + } + + const ignore = () => { + return true } - parser.hooks.expression.for('eval').tap(PLUGIN_NAME, flagModule) - parser.hooks.expression.for('Function').tap(PLUGIN_NAME, flagModule) + // wrapping + parser.hooks.call.for('eval').tap(PLUGIN_NAME, wrapExpression) + parser.hooks.call.for('global.eval').tap(PLUGIN_NAME, wrapExpression) + parser.hooks.call.for('Function').tap(PLUGIN_NAME, wrapExpression) + parser.hooks.call + .for('global.Function') + .tap(PLUGIN_NAME, wrapExpression) + parser.hooks.new.for('Function').tap(PLUGIN_NAME, wrapExpression) + parser.hooks.new + .for('global.Function') + .tap(PLUGIN_NAME, wrapExpression) + + // fallbacks + parser.hooks.expression + .for('eval') + .tap(PLUGIN_NAME, expressionHandler) + parser.hooks.expression + .for('Function') + .tap(PLUGIN_NAME, expressionHandler) + parser.hooks.expression + .for('Function.prototype') + .tap(PLUGIN_NAME, ignore) parser.hooks.expression .for('global.eval') - .tap(PLUGIN_NAME, flagModule) + .tap(PLUGIN_NAME, expressionHandler) parser.hooks.expression .for('global.Function') - .tap(PLUGIN_NAME, flagModule) + .tap(PLUGIN_NAME, expressionHandler) + parser.hooks.expression + .for('global.Function.prototype') + .tap(PLUGIN_NAME, ignore) const memberChainHandler = (_expr: any, members: string[]) => { if ( @@ -237,3 +325,21 @@ export default class MiddlewarePlugin { ) } } + +function isUsedByExports(args: { + module: webpack5.Module + moduleGraph: webpack5.ModuleGraph + runtime: any + usedByExports: boolean | Set | undefined +}): boolean { + const { moduleGraph, runtime, module, usedByExports } = args + if (usedByExports === undefined) return false + if (typeof usedByExports === 'boolean') return usedByExports + const exportsInfo = moduleGraph.getExportsInfo(module) + const wp = webpack as unknown as typeof webpack5 + for (const exportName of usedByExports) { + if (exportsInfo.getUsed(exportName, runtime) !== wp.UsageState.Unused) + return true + } + return false +} diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 731398f9f359361..03ac4dec26d562e 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -514,7 +514,13 @@ export default class DevServer extends Server { parsed: UrlWithParsedQuery }): Promise { try { - const result = await super.runMiddleware(params) + const result = await super.runMiddleware({ + ...params, + onWarning: (warn) => { + this.logErrorWithOriginalStack(warn, 'warning', 'client') + }, + }) + result?.waitUntil.catch((error) => this.logErrorWithOriginalStack(error, 'unhandledRejection', 'client') ) @@ -589,7 +595,7 @@ export default class DevServer extends Server { private async logErrorWithOriginalStack( err?: unknown, - type?: 'unhandledRejection' | 'uncaughtException', + type?: 'unhandledRejection' | 'uncaughtException' | 'warning', stats: 'server' | 'client' = 'server' ) { let usedOriginalStack = false @@ -630,11 +636,15 @@ export default class DevServer extends Server { const { file, lineNumber, column, methodName } = originalStackFrame console.error( - chalk.red('error') + + (type === 'warning' ? chalk.yellow('warn') : chalk.red('error')) + ' - ' + `${file} (${lineNumber}:${column}) @ ${methodName}` ) - console.error(`${chalk.red(err.name)}: ${err.message}`) + console.error( + `${(type === 'warning' ? chalk.yellow : chalk.red)(err.name)}: ${ + err.message + }` + ) console.error(originalCodeFrame) usedOriginalStack = true } @@ -647,7 +657,9 @@ export default class DevServer extends Server { } if (!usedOriginalStack) { - if (type) { + if (type === 'warning') { + Log.warn(err + '') + } else if (type) { Log.error(`${type}:`, err + '') } else { Log.error(err + '') diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index b4ce74c36472fcb..e6cd4f8f579f900 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -611,6 +611,7 @@ export default class Server { response: ServerResponse parsedUrl: ParsedNextUrl parsed: UrlWithParsedQuery + onWarning?: (warning: Error) => void }): Promise { this.middlewareBetaWarning() @@ -672,6 +673,12 @@ export default class Server { page: page, }, ssr: !!this.nextConfig.experimental.concurrentFeatures, + onWarning: (warning: Error) => { + if (params.onWarning) { + warning.message += ` "./${middlewareInfo.name}"` + params.onWarning(warning) + } + }, }) for (let [key, value] of result.response.headers) { diff --git a/packages/next/server/web/sandbox/sandbox.ts b/packages/next/server/web/sandbox/sandbox.ts index a6e3688f236c46c..94afaf0b501fd41 100644 --- a/packages/next/server/web/sandbox/sandbox.ts +++ b/packages/next/server/web/sandbox/sandbox.ts @@ -10,9 +10,11 @@ import vm from 'vm' let cache: | { context: { [key: string]: any } + onWarning: (warn: Error) => void paths: Map require: Map sandbox: vm.Context + warnedEvals: Set } | undefined @@ -33,12 +35,14 @@ export function clearSandboxCache(path: string, content: Buffer | string) { export async function run(params: { name: string + onWarning: (warn: Error) => void paths: string[] request: RequestData ssr: boolean }): Promise { if (cache === undefined) { const context: { [key: string]: any } = { + __next_eval__, _ENTRIES: {}, atob: polyfills.atob, Blob, @@ -89,11 +93,21 @@ export async function run(params: { cache = { context, + onWarning: params.onWarning, + paths: new Map(), require: new Map([ [require.resolve('next/dist/compiled/cookie'), { exports: cookie }], ]), - paths: new Map(), - sandbox: vm.createContext(context), + sandbox: vm.createContext(context, { + codeGeneration: + process.env.NODE_ENV === 'production' + ? { + strings: false, + wasm: false, + } + : undefined, + }), + warnedEvals: new Set(), } loadDependencies(cache.sandbox, [ @@ -110,6 +124,8 @@ export async function run(params: { map: { Request: 'Request' }, }, ]) + } else { + cache.onWarning = params.onWarning } for (const paramPath of params.paths) { @@ -218,3 +234,17 @@ function getFetchURL(input: RequestInfo, headers: NodeHeaders = {}): string { function isRequestLike(obj: unknown): obj is Request { return Boolean(obj && typeof obj === 'object' && 'url' in obj) } + +function __next_eval__(fn: Function) { + const key = fn.toString() + if (!cache?.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__) + cache?.warnedEvals.add(key) + cache?.onWarning(warning) + } + return fn() +} diff --git a/test/integration/middleware/core/lib/utils.js b/test/integration/middleware/core/lib/utils.js new file mode 100644 index 000000000000000..1e3f457afc11ac3 --- /dev/null +++ b/test/integration/middleware/core/lib/utils.js @@ -0,0 +1,8 @@ +export function getTextWithEval() { + // eslint-disable-next-line no-eval + return eval('with some text') +} + +export function getText() { + return 'with some text' +} diff --git a/test/integration/middleware-core/next.config.js b/test/integration/middleware/core/next.config.js similarity index 100% rename from test/integration/middleware-core/next.config.js rename to test/integration/middleware/core/next.config.js diff --git a/test/integration/middleware-core/pages/interface/[id]/index.js b/test/integration/middleware/core/pages/interface/[id]/index.js similarity index 100% rename from test/integration/middleware-core/pages/interface/[id]/index.js rename to test/integration/middleware/core/pages/interface/[id]/index.js diff --git a/test/integration/middleware-core/pages/interface/_middleware.js b/test/integration/middleware/core/pages/interface/_middleware.js similarity index 100% rename from test/integration/middleware-core/pages/interface/_middleware.js rename to test/integration/middleware/core/pages/interface/_middleware.js diff --git a/test/integration/middleware-core/pages/interface/static.js b/test/integration/middleware/core/pages/interface/static.js similarity index 100% rename from test/integration/middleware-core/pages/interface/static.js rename to test/integration/middleware/core/pages/interface/static.js diff --git a/test/integration/middleware-core/pages/redirects/_middleware.js b/test/integration/middleware/core/pages/redirects/_middleware.js similarity index 100% rename from test/integration/middleware-core/pages/redirects/_middleware.js rename to test/integration/middleware/core/pages/redirects/_middleware.js diff --git a/test/integration/middleware-core/pages/redirects/header.js b/test/integration/middleware/core/pages/redirects/header.js similarity index 100% rename from test/integration/middleware-core/pages/redirects/header.js rename to test/integration/middleware/core/pages/redirects/header.js diff --git a/test/integration/middleware-core/pages/redirects/index.js b/test/integration/middleware/core/pages/redirects/index.js similarity index 100% rename from test/integration/middleware-core/pages/redirects/index.js rename to test/integration/middleware/core/pages/redirects/index.js diff --git a/test/integration/middleware-core/pages/redirects/new-home.js b/test/integration/middleware/core/pages/redirects/new-home.js similarity index 100% rename from test/integration/middleware-core/pages/redirects/new-home.js rename to test/integration/middleware/core/pages/redirects/new-home.js diff --git a/test/integration/middleware-core/pages/redirects/old-home.js b/test/integration/middleware/core/pages/redirects/old-home.js similarity index 100% rename from test/integration/middleware-core/pages/redirects/old-home.js rename to test/integration/middleware/core/pages/redirects/old-home.js diff --git a/test/integration/middleware-core/pages/responses/_middleware.js b/test/integration/middleware/core/pages/responses/_middleware.js similarity index 95% rename from test/integration/middleware-core/pages/responses/_middleware.js rename to test/integration/middleware/core/pages/responses/_middleware.js index 7a6f66ee09bdb5f..255f1c3180a1b84 100644 --- a/test/integration/middleware-core/pages/responses/_middleware.js +++ b/test/integration/middleware/core/pages/responses/_middleware.js @@ -1,6 +1,7 @@ import { createElement } from 'react' import { renderToString } from 'react-dom/server.browser' import { NextResponse } from 'next/server' +import { getText } from '../../lib/utils' export async function middleware(request, ev) { // eslint-disable-next-line no-undef @@ -36,7 +37,8 @@ export async function middleware(request, ev) { ev.waitUntil( (async () => { writer.write(encoder.encode('this is a streamed ')) - writer.write(encoder.encode('response')) + writer.write(encoder.encode('response ')) + writer.write(encoder.encode(getText())) writer.close() })() ) diff --git a/test/integration/middleware-core/pages/responses/deep/_middleware.js b/test/integration/middleware/core/pages/responses/deep/_middleware.js similarity index 100% rename from test/integration/middleware-core/pages/responses/deep/_middleware.js rename to test/integration/middleware/core/pages/responses/deep/_middleware.js diff --git a/test/integration/middleware-core/pages/responses/deep/index.js b/test/integration/middleware/core/pages/responses/deep/index.js similarity index 100% rename from test/integration/middleware-core/pages/responses/deep/index.js rename to test/integration/middleware/core/pages/responses/deep/index.js diff --git a/test/integration/middleware-core/pages/responses/index.js b/test/integration/middleware/core/pages/responses/index.js similarity index 100% rename from test/integration/middleware-core/pages/responses/index.js rename to test/integration/middleware/core/pages/responses/index.js diff --git a/test/integration/middleware-core/pages/rewrites/_middleware.js b/test/integration/middleware/core/pages/rewrites/_middleware.js similarity index 100% rename from test/integration/middleware-core/pages/rewrites/_middleware.js rename to test/integration/middleware/core/pages/rewrites/_middleware.js diff --git a/test/integration/middleware-core/pages/rewrites/a.js b/test/integration/middleware/core/pages/rewrites/a.js similarity index 100% rename from test/integration/middleware-core/pages/rewrites/a.js rename to test/integration/middleware/core/pages/rewrites/a.js diff --git a/test/integration/middleware-core/pages/rewrites/about.js b/test/integration/middleware/core/pages/rewrites/about.js similarity index 100% rename from test/integration/middleware-core/pages/rewrites/about.js rename to test/integration/middleware/core/pages/rewrites/about.js diff --git a/test/integration/middleware-core/pages/rewrites/b.js b/test/integration/middleware/core/pages/rewrites/b.js similarity index 100% rename from test/integration/middleware-core/pages/rewrites/b.js rename to test/integration/middleware/core/pages/rewrites/b.js diff --git a/test/integration/middleware-core/pages/rewrites/index.js b/test/integration/middleware/core/pages/rewrites/index.js similarity index 100% rename from test/integration/middleware-core/pages/rewrites/index.js rename to test/integration/middleware/core/pages/rewrites/index.js diff --git a/test/integration/middleware-core/test/index.test.js b/test/integration/middleware/core/test/index.test.js similarity index 99% rename from test/integration/middleware-core/test/index.test.js rename to test/integration/middleware/core/test/index.test.js index 66d1cc11fd6fec9..2c386dfe9970996 100644 --- a/test/integration/middleware-core/test/index.test.js +++ b/test/integration/middleware/core/test/index.test.js @@ -254,7 +254,7 @@ function responseTests(locale = '') { `${locale}/responses/stream-a-response` ) const html = await res.text() - expect(html).toBe('this is a streamed response') + expect(html).toBe('this is a streamed response with some text') }) it(`${locale} should respond with a body`, async () => { diff --git a/test/integration/middleware-base-path/next.config.js b/test/integration/middleware/with-base-path/next.config.js similarity index 100% rename from test/integration/middleware-base-path/next.config.js rename to test/integration/middleware/with-base-path/next.config.js diff --git a/test/integration/middleware-base-path/pages/_middleware.js b/test/integration/middleware/with-base-path/pages/_middleware.js similarity index 100% rename from test/integration/middleware-base-path/pages/_middleware.js rename to test/integration/middleware/with-base-path/pages/_middleware.js diff --git a/test/integration/middleware-base-path/pages/about.js b/test/integration/middleware/with-base-path/pages/about.js similarity index 100% rename from test/integration/middleware-base-path/pages/about.js rename to test/integration/middleware/with-base-path/pages/about.js diff --git a/test/integration/middleware-base-path/pages/index.js b/test/integration/middleware/with-base-path/pages/index.js similarity index 100% rename from test/integration/middleware-base-path/pages/index.js rename to test/integration/middleware/with-base-path/pages/index.js diff --git a/test/integration/middleware-base-path/test/index.test.js b/test/integration/middleware/with-base-path/test/index.test.js similarity index 100% rename from test/integration/middleware-base-path/test/index.test.js rename to test/integration/middleware/with-base-path/test/index.test.js diff --git a/test/integration/middleware/with-eval/lib/utils.js b/test/integration/middleware/with-eval/lib/utils.js new file mode 100644 index 000000000000000..3c17949f4c9f05f --- /dev/null +++ b/test/integration/middleware/with-eval/lib/utils.js @@ -0,0 +1,8 @@ +export async function usingEval() { + // eslint-disable-next-line no-eval + return { value: eval('100') } +} + +export async function notUsingEval() { + return { value: 100 } +} diff --git a/test/integration/middleware/with-eval/pages/_middleware.js b/test/integration/middleware/with-eval/pages/_middleware.js new file mode 100644 index 000000000000000..0ea96d0bb80239a --- /dev/null +++ b/test/integration/middleware/with-eval/pages/_middleware.js @@ -0,0 +1,19 @@ +import { notUsingEval, usingEval } from '../lib/utils' + +export async function middleware(request) { + if (request.nextUrl.pathname === '/using-eval') { + return new Response(JSON.stringify(await usingEval()), { + headers: { + 'Content-Type': 'application/json', + }, + }) + } + + if (request.nextUrl.pathname === '/not-using-eval') { + return new Response(JSON.stringify(await notUsingEval()), { + headers: { + 'Content-Type': 'application/json', + }, + }) + } +} diff --git a/test/integration/middleware/with-eval/test/index.test.js b/test/integration/middleware/with-eval/test/index.test.js new file mode 100644 index 000000000000000..acd86cf95292078 --- /dev/null +++ b/test/integration/middleware/with-eval/test/index.test.js @@ -0,0 +1,79 @@ +/* eslint-env jest */ + +import stripAnsi from 'next/dist/compiled/strip-ansi' +import { join } from 'path' +import { + fetchViaHTTP, + findPort, + killApp, + launchApp, + nextBuild, + waitFor, +} from 'next-test-utils' + +const context = {} +const DYNAMIC_CODE_ERROR = `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware` + +jest.setTimeout(1000 * 60 * 2) +context.appDir = join(__dirname, '../') + +describe('Middleware usage of dynamic code evaluation', () => { + describe('dev mode', () => { + let output = '' + + beforeAll(async () => { + context.appPort = await findPort() + context.app = await launchApp(context.appDir, context.appPort, { + env: { __NEXT_TEST_WITH_DEVTOOL: 1 }, + onStdout(msg) { + output += msg + }, + onStderr(msg) { + output += msg + }, + }) + }) + + beforeEach(() => (output = '')) + afterAll(() => killApp(context.app)) + + it('shows a warning when running code with eval', async () => { + const res = await fetchViaHTTP(context.appPort, `/using-eval`) + const json = await res.json() + await waitFor(500) + expect(json.value).toEqual(100) + expect(output).toContain(DYNAMIC_CODE_ERROR) + expect(output).toContain('DynamicCodeEvaluationWarning') + expect(output).toContain('pages/_middleware') + expect(output).toContain('lib/utils.js') + expect(output).toContain('usingEval') + expect(stripAnsi(output)).toContain("value: eval('100')") + }) + + it('does not show warning when no code uses eval', async () => { + const res = await fetchViaHTTP(context.appPort, `/not-using-eval`) + const json = await res.json() + await waitFor(500) + expect(json.value).toEqual(100) + expect(output).not.toContain(DYNAMIC_CODE_ERROR) + }) + }) + + describe('production mode', () => { + let buildResult + + beforeAll(async () => { + buildResult = await nextBuild(context.appDir, undefined, { + stderr: true, + stdout: true, + }) + }) + + it('should have middleware warning during build', () => { + expect(buildResult.stderr).toContain(`Failed to compile`) + expect(buildResult.stderr).toContain(`Used by usingEval`) + expect(buildResult.stderr).toContain(`./pages/_middleware.js`) + expect(buildResult.stderr).toContain(DYNAMIC_CODE_ERROR) + }) + }) +}) From 0307ba0d1d6e4a39d64438bf2cf4f2def2c25e58 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 5 Nov 2021 22:51:10 +0100 Subject: [PATCH 3/4] Optimize the SSR middleware runtime size (#30906) By conditionally importing `react-dom/server` under the web runtime and reusing `renderToReadableStream` instead of `renderToStaticMarkup`, we can get rid of the legacy browser React DOM server from the runtime. ~Furthermore we can make the build target `es6` for the SSR middleware, and make some code paths tree-shakable (done in another PR).~ Together this makes the runtime ~32kb smaller. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint` --- packages/next/build/webpack-config.ts | 8 ++++++ packages/next/server/render.tsx | 36 ++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index eab85535f3556a3..71174550d9c0f25 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -676,6 +676,14 @@ export default async function getBaseWebpackConfig( false, } : {}), + + ...(webServerRuntime + ? { + 'react-dom/server': dev + ? 'react-dom/cjs/react-dom-server.browser.development' + : 'react-dom/cjs/react-dom-server.browser.production.min', + } + : {}), }, ...(targetWeb ? { diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 8059b75c172c0e2..53fa578287d73ab 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -2,7 +2,7 @@ import { IncomingMessage, ServerResponse } from 'http' import { ParsedUrlQuery } from 'querystring' import type { Writable as WritableType } from 'stream' import React from 'react' -import * as ReactDOMServer from 'react-dom/server' +import ReactDOMServer from 'react-dom/server' import { StyleRegistry, createStyleRegistry } from 'styled-jsx' import { UnwrapPromise } from '../lib/coalesced-function' import { @@ -950,7 +950,12 @@ export async function renderToHTML( */ const generateStaticHTML = supportsDynamicHTML !== true const renderDocument = async () => { - if (Document.getInitialProps) { + if (process.browser && Document.getInitialProps) { + throw new Error( + '`getInitialProps` in Document component is not supported with `concurrentFeatures` enabled.' + ) + } + if (!process.browser && Document.getInitialProps) { const renderPage: RenderPage = ( options: ComponentsEnhancer = {} ): RenderPageResult | Promise => { @@ -1120,7 +1125,8 @@ export async function renderToHTML( styles: documentResult.styles, useMaybeDeferContent, } - const documentHTML = ReactDOMServer.renderToStaticMarkup( + + const document = ( {documentResult.documentElement(htmlProps)} @@ -1128,6 +1134,30 @@ export async function renderToHTML( ) + let documentHTML: string + if (process.browser) { + // There is no `renderToStaticMarkup` exposed in the web environment, use + // blocking `renderToReadableStream` to get the similar result. + let result = '' + const readable = (ReactDOMServer as any).renderToReadableStream(document, { + onError: (e: any) => { + throw e + }, + }) + const reader = readable.getReader() + const decoder = new TextDecoder() + while (true) { + const { done, value } = await reader.read() + if (done) { + break + } + result += typeof value === 'string' ? value : decoder.decode(value) + } + documentHTML = result + } else { + documentHTML = ReactDOMServer.renderToStaticMarkup(document) + } + if (process.env.NODE_ENV !== 'production') { const nonRenderedComponents = [] const expectedDocComponents = ['Main', 'Head', 'NextScript', 'Html'] From b75b2f02c9dc33c649c8d90c952be9bfee70c60b Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 5 Nov 2021 23:59:46 +0100 Subject: [PATCH 4/4] Improve error handling in the SSR middleware (#31057) This PR improves error handling in the SSR middleware. Previously the response was sent out synchronously, and and errors were silently swallowed. There was no `.catch` for `renderToHTML`. This changes the middleware to be asynchronous, which waits until the initial Document to be rendered correctly and then starts the streaming. With this change we can also send correct status code when there're immediate errors before Fizz. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint` --- .../next-middleware-ssr-loader/index.ts | 36 +++++++++---------- .../test/index.test.js | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index 1f367ac3eadebd1..07f6dfd0861826f 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -57,10 +57,6 @@ export default async function middlewareRSCLoader(this: any) { throw new Error('Your page must export a \`default\` component') } - function renderError(err, status) { - return new Response(err.toString(), {status}) - } - function wrapReadable(readable) { const encoder = new TextEncoder() const transformStream = new TransformStream() @@ -104,15 +100,10 @@ export default async function middlewareRSCLoader(this: any) { const Component = Page` } - function render(request) { + async function render(request) { const url = request.nextUrl const query = Object.fromEntries(url.searchParams) - if (Document.getInitialProps) { - const err = new Error('Document.getInitialProps is not supported with server components, please remove it from pages/_document') - return renderError(err, 500) - } - // Preflight request if (request.method === 'HEAD') { return new Response('OK.', { @@ -172,18 +163,27 @@ export default async function middlewareRSCLoader(this: any) { const writer = transformStream.writable.getWriter() const encoder = new TextEncoder() - renderToHTML( - { url: url.pathname }, - {}, - url.pathname, - query, - renderOpts - ).then(result => { + try { + const result = await renderToHTML( + { url: url.pathname }, + {}, + url.pathname, + query, + renderOpts + ) result.pipe({ write: str => writer.write(encoder.encode(str)), end: () => writer.close() }) - }) + } catch (err) { + return new Response( + (err || 'An error occurred while rendering ' + url.pathname + '.').toString(), + { + status: 500, + headers: { 'x-middleware-ssr': '1' } + } + ) + } return new Response(transformStream.readable, { headers: { 'x-middleware-ssr': '1' } diff --git a/test/integration/react-streaming-and-server-components/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index 613c90d350d5e74..c8f5e4999bb21e1 100644 --- a/test/integration/react-streaming-and-server-components/test/index.test.js +++ b/test/integration/react-streaming-and-server-components/test/index.test.js @@ -190,7 +190,7 @@ const documentSuite = { expect(res.status).toBe(500) expect(html).toContain( - 'Document.getInitialProps is not supported with server components, please remove it from pages/_document' + 'Error: `getInitialProps` in Document component is not supported with `concurrentFeatures` enabled.' ) }) },