## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` Fixes #38743. Fixes: https://github.com/vercel/next.js/pull/38750 The PR adds basic `TemplateLiteral` support for static analysis. The corresponding re-production of #38743 has also been implemented in e2e tests.
This commit is contained in:
parent
366a04b7ac
commit
02c78a5c15
8 changed files with 213 additions and 41 deletions
|
@ -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
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Not Allowed</th>
|
||||
<th>Allowed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```js
|
||||
// `config` should be an object
|
||||
export const config = 'hello world'
|
||||
```
|
||||
|
||||
This is not allowed
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```js
|
||||
const config = {}
|
||||
config.amp = true
|
||||
export const config = {}
|
||||
```
|
||||
|
||||
This is not allowed
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```js
|
||||
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,
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```js
|
||||
export const config = {
|
||||
amp: true,
|
||||
}
|
||||
|
||||
export const config = {
|
||||
amp: false,
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```js
|
||||
// `config.runtime` contains a dynamic expression
|
||||
export const config = {
|
||||
runtime: `node${'js'}`,
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```js
|
||||
export const config = {
|
||||
runtime: 'nodejs',
|
||||
}
|
||||
export const config = {
|
||||
runtime: `nodejs`,
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```js
|
||||
// Re-exported `config` is not allowed
|
||||
export { config } from '../config'
|
||||
```
|
||||
|
||||
This is allowed
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```js
|
||||
export const config = { amp: true }
|
||||
export const config = {}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### 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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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<PageStaticInfo> {
|
||||
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<string, boolean>()
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
7
test/e2e/switchable-runtime/pages/api/edge.js
Normal file
7
test/e2e/switchable-runtime/pages/api/edge.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default (req) => {
|
||||
return new Response(`Returned by Edge API Route ${req.url}`)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
runtime: `experimental-edge`,
|
||||
}
|
|
@ -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`
|
||||
),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export default function Page() {
|
||||
return <p>hello world</p>
|
||||
}
|
||||
|
||||
export const config = {
|
||||
runtime: `something-${'real' + 1 + 'y odd'}`,
|
||||
}
|
Loading…
Reference in a new issue