diff --git a/errors/invalid-page-config.md b/errors/invalid-page-config.md index 6984a9bb1b9e..6d24391d2ea4 100644 --- a/errors/invalid-page-config.md +++ b/errors/invalid-page-config.md @@ -1,39 +1,119 @@ -# Invalid Page Config +# Invalid Page / API Route Config #### Why This Error Occurred -In one of your pages you did `export const config` with an invalid value. +In one of your pages or API Routes you did `export const config` with an invalid value. #### Possible Ways to Fix It The page's config must be an object initialized directly when being exported and not modified dynamically. +The config object must only contains static constant literals without expressions. -This is not allowed + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Not AllowedAllowed
```js +// `config` should be an object export const config = 'hello world' ``` -This is not allowed + + +```js +export const config = {} +``` + +
```js -const config = {} +export const config = {} +// `config.amp` is defined after `config` is exported config.amp = true + +// `config.amp` contains a dynamic expression +export const config = { + amp: 1 + 1 > 2, +} +``` + + + +```js +export const config = { + amp: true, +} + +export const config = { + amp: false, +} ``` -This is not allowed +
+ +```js +// `config.runtime` contains a dynamic expression +export const config = { + runtime: `node${'js'}`, +} +``` + + + +```js +export const config = { + runtime: 'nodejs', +} +export const config = { + runtime: `nodejs`, +} +``` + +
```js +// Re-exported `config` is not allowed export { config } from '../config' ``` -This is allowed + ```js -export const config = { amp: true } +export const config = {} ``` +
+ ### Useful Links - [Enabling AMP Support](https://nextjs.org/docs/advanced-features/amp-support/introduction) - [API Middlewares](https://nextjs.org/docs/api-routes/api-middlewares) +- [Switchable Runtime](https://nextjs.org/docs/advanced-features/react-18/switchable-runtime) diff --git a/packages/next/build/analysis/extract-const-value.ts b/packages/next/build/analysis/extract-const-value.ts index 0cfb372c5b8f..db9ce53187e7 100644 --- a/packages/next/build/analysis/extract-const-value.ts +++ b/packages/next/build/analysis/extract-const-value.ts @@ -11,6 +11,7 @@ import type { ObjectExpression, RegExpLiteral, StringLiteral, + TemplateLiteral, VariableDeclaration, } from '@swc/core' @@ -60,26 +61,6 @@ export function extractExportedConstValue( throw new NoSuchDeclarationError() } -/** - * A wrapper on top of `extractExportedConstValue` that returns undefined - * instead of throwing when the thrown error is known. - */ -export function tryToExtractExportedConstValue( - module: Module, - exportedName: string -) { - try { - return extractExportedConstValue(module, exportedName) - } catch (error) { - if ( - error instanceof UnsupportedValueError || - error instanceof NoSuchDeclarationError - ) { - return undefined - } - } -} - function isExportDeclaration(node: Node): node is ExportDeclaration { return node.type === 'ExportDeclaration' } @@ -124,8 +105,12 @@ function isRegExpLiteral(node: Node): node is RegExpLiteral { return node.type === 'RegExpLiteral' } -class UnsupportedValueError extends Error {} -class NoSuchDeclarationError extends Error {} +function isTemplateLiteral(node: Node): node is TemplateLiteral { + return node.type === 'TemplateLiteral' +} + +export class UnsupportedValueError extends Error {} +export class NoSuchDeclarationError extends Error {} function extractValue(node: Node): any { if (isNullLiteral(node)) { @@ -191,6 +176,25 @@ function extractValue(node: Node): any { } return obj + } else if (isTemplateLiteral(node)) { + // e.g. `abc` + if (node.expressions.length !== 0) { + // TODO: should we add support for `${'e'}d${'g'}'e'`? + throw new UnsupportedValueError() + } + + // When TemplateLiteral has 0 expressions, the length of quasis is always 1. + // Because when parsing TemplateLiteral, the parser yields the first quasi, + // then the first expression, then the next quasi, then the next expression, etc., + // until the last quasi. + // Thus if there is no expression, the parser ends at the frst and also last quasis + // + // A "cooked" interpretation where backslashes have special meaning, while a + // "raw" interpretation where backslashes do not have special meaning + // https://exploringjs.com/impatient-js/ch_template-literals.html#template-strings-cooked-vs-raw + const [{ cooked, raw }] = node.quasis + + return cooked ?? raw } else { throw new UnsupportedValueError() } diff --git a/packages/next/build/analysis/get-page-static-info.ts b/packages/next/build/analysis/get-page-static-info.ts index 60d94aa800e3..77dda52ce689 100644 --- a/packages/next/build/analysis/get-page-static-info.ts +++ b/packages/next/build/analysis/get-page-static-info.ts @@ -1,6 +1,9 @@ import { isServerRuntime, ServerRuntime } from '../../server/config-shared' import type { NextConfig } from '../../server/config-shared' -import { tryToExtractExportedConstValue } from './extract-const-value' +import { + extractExportedConstValue, + UnsupportedValueError, +} from './extract-const-value' import { escapeStringRegexp } from '../../shared/lib/escape-regexp' import { parseModule } from './parse-module' import { promises as fs } from 'fs' @@ -32,13 +35,23 @@ export async function getPageStaticInfo(params: { isDev?: boolean page?: string }): Promise { - const { isDev, pageFilePath, nextConfig } = params + const { isDev, pageFilePath, nextConfig, page } = params const fileContent = (await tryToReadFile(pageFilePath, !isDev)) || '' if (/runtime|getStaticProps|getServerSideProps|matcher/.test(fileContent)) { const swcAST = await parseModule(pageFilePath, fileContent) const { ssg, ssr } = checkExports(swcAST) - const config = tryToExtractExportedConstValue(swcAST, 'config') || {} + + // default / failsafe value for config + let config: any = {} + try { + config = extractExportedConstValue(swcAST, 'config') + } catch (e) { + if (e instanceof UnsupportedValueError) { + warnAboutUnsupportedValue(pageFilePath, page) + } + // `export config` doesn't exist, or other unknown error throw by swc, silence them + } if ( typeof config.runtime !== 'string' && @@ -218,3 +231,19 @@ function warnAboutExperimentalEdgeApiFunctions() { } let warnedAboutExperimentalEdgeApiFunctions = false + +const warnedUnsupportedValueMap = new Map() +function warnAboutUnsupportedValue( + pageFilePath: string, + page: string | undefined +) { + if (warnedUnsupportedValueMap.has(pageFilePath)) { + return + } + Log.warn( + `You have exported a \`config\` field in ${ + page ? `route "${page}"` : `"${pageFilePath}"` + } that Next.js can't recognize, so it will be ignored. See: https://nextjs.org/docs/messages/invalid-page-config` + ) + warnedUnsupportedValueMap.set(pageFilePath, true) +} diff --git a/test/e2e/switchable-runtime/index.test.ts b/test/e2e/switchable-runtime/index.test.ts index 6c05d4ad30cf..8d704a6c6e1c 100644 --- a/test/e2e/switchable-runtime/index.test.ts +++ b/test/e2e/switchable-runtime/index.test.ts @@ -115,11 +115,15 @@ describe('Switchable runtime', () => { ) }) - it('should build /api/hello as an api route with edge runtime', async () => { - const response = await fetchViaHTTP(context.appPort, '/api/hello') - const text = await response.text() + it('should build /api/hello and /api/edge as an api route with edge runtime', async () => { + let response = await fetchViaHTTP(context.appPort, '/api/hello') + let text = await response.text() expect(text).toMatch(/Hello from .+\/api\/hello/) + response = await fetchViaHTTP(context.appPort, '/api/edge') + text = await response.text() + expect(text).toMatch(/Returned by Edge API Route .+\/api\/edge/) + if (!(global as any).isNextDeploy) { const manifest = await readJson( join(context.appDir, '.next/server/middleware-manifest.json') @@ -137,6 +141,17 @@ describe('Switchable runtime', () => { regexp: '^/api/hello$', wasm: [], }, + '/api/edge': { + env: [], + files: [ + 'server/edge-runtime-webpack.js', + 'server/pages/api/edge.js', + ], + name: 'pages/api/edge', + page: '/api/edge', + regexp: '^/api/edge$', + wasm: [], + }, }, }) } @@ -235,11 +250,15 @@ describe('Switchable runtime', () => { }) }) - it('should build /api/hello as an api route with edge runtime', async () => { - const response = await fetchViaHTTP(context.appPort, '/api/hello') - const text = await response.text() + it('should build /api/hello and /api/edge as an api route with edge runtime', async () => { + let response = await fetchViaHTTP(context.appPort, '/api/hello') + let text = await response.text() expect(text).toMatch(/Hello from .+\/api\/hello/) + response = await fetchViaHTTP(context.appPort, '/api/edge') + text = await response.text() + expect(text).toMatch(/Returned by Edge API Route .+\/api\/edge/) + if (!(global as any).isNextDeploy) { const manifest = await readJson( join(context.appDir, '.next/server/middleware-manifest.json') @@ -257,6 +276,17 @@ describe('Switchable runtime', () => { regexp: '^/api/hello$', wasm: [], }, + '/api/edge': { + env: [], + files: [ + 'server/edge-runtime-webpack.js', + 'server/pages/api/edge.js', + ], + name: 'pages/api/edge', + page: '/api/edge', + regexp: '^/api/edge$', + wasm: [], + }, }, }) } diff --git a/test/e2e/switchable-runtime/pages/api/edge.js b/test/e2e/switchable-runtime/pages/api/edge.js new file mode 100644 index 000000000000..d375a24aaab7 --- /dev/null +++ b/test/e2e/switchable-runtime/pages/api/edge.js @@ -0,0 +1,7 @@ +export default (req) => { + return new Response(`Returned by Edge API Route ${req.url}`) +} + +export const config = { + runtime: `experimental-edge`, +} diff --git a/test/production/exported-runtimes-value-validation/index.test.ts b/test/production/exported-runtimes-value-validation/index.test.ts index 73d4009ff442..70157cab6498 100644 --- a/test/production/exported-runtimes-value-validation/index.test.ts +++ b/test/production/exported-runtimes-value-validation/index.test.ts @@ -4,7 +4,7 @@ import path from 'path' describe('Exported runtimes value validation', () => { test('fails to build on malformed input', async () => { const result = await nextBuild( - path.resolve(__dirname, './app'), + path.resolve(__dirname, './invalid-runtime/app'), undefined, { stdout: true, stderr: true } ) @@ -15,4 +15,19 @@ describe('Exported runtimes value validation', () => { ), }) }) + + test('warns on unrecognized runtimes value', async () => { + const result = await nextBuild( + path.resolve(__dirname, './unsupported-syntax/app'), + undefined, + { stdout: true, stderr: true } + ) + + expect(result).toMatchObject({ + code: 0, + stderr: expect.stringContaining( + `You have exported a \`config\` field in route "/" that Next.js can't recognize, so it will be ignored` + ), + }) + }) }) diff --git a/test/production/exported-runtimes-value-validation/app/pages/index.js b/test/production/exported-runtimes-value-validation/invalid-runtime/app/pages/index.js similarity index 100% rename from test/production/exported-runtimes-value-validation/app/pages/index.js rename to test/production/exported-runtimes-value-validation/invalid-runtime/app/pages/index.js diff --git a/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/index.js b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/index.js new file mode 100644 index 000000000000..a340aa42d830 --- /dev/null +++ b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/index.js @@ -0,0 +1,7 @@ +export default function Page() { + return

hello world

+} + +export const config = { + runtime: `something-${'real' + 1 + 'y odd'}`, +}