rsnext/packages/next/build/analysis/get-page-static-info.ts

358 lines
10 KiB
TypeScript
Raw Normal View History

import { isServerRuntime } from '../../server/config-shared'
import type { NextConfig } from '../../server/config-shared'
import type { Middleware, RouteHas } from '../../lib/load-custom-routes'
import {
extractExportedConstValue,
UnsupportedValueError,
} from './extract-const-value'
import { parseModule } from './parse-module'
import { promises as fs } from 'fs'
import { tryToParsePath } from '../../lib/try-to-parse-path'
import * as Log from '../output/log'
import { SERVER_RUNTIME } from '../../lib/constants'
import { ServerRuntime } from 'next/types'
import { checkCustomRoutes } from '../../lib/load-custom-routes'
feat(edge): allows configuring Dynamic code execution guard (#39539) ### 📖 What's in there? Dynamic code evaluation (`eval()`, `new Function()`, ...) is not supported on the edge runtime, hence why we fail the build when detecting such statement in the middleware or `experimental-edge` routes at build time. However, there could be false positives, which static analysis and tree-shaking can not exclude: - `qs` through these dependencies (get-intrinsic: [source](https://github.com/ljharb/get-intrinsic/blob/main/index.js#L12)) - `function-bind` ([source](https://github.com/Raynos/function-bind/blob/master/implementation.js#L42)) - `has` ([source](https://github.com/tarruda/has/blob/master/src/index.js#L5)) This PR leverages the existing `config` export to let user allow some of their files. it’s meant for allowing users to import 3rd party modules who embed dynamic code evaluation, but do not use it (because or code paths), and can't be tree-shaked. By default, it’s keeping the existing behavior: warn in dev, fails to build. If users allow dynamic code, and that code is reached at runtime, their app stills breaks. ### 🧪 How to test? - (existing) integration tests for disallowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-dynamic` - (new) integration tests for allowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-configurable` - (amended) production tests for validating the new configuration keys: `pnpm testheadless --testPathPattern=config-validations` To try it live, you could have an application such as: ```js // lib/index.js /* eslint-disable no-eval */ export function hasUnusedDynamic() { if ((() => false)()) { eval('100') } } export function hasDynamic() { eval('100') } // pages/index.jsx export default function Page({ edgeRoute }) { return <p>{edgeRoute}</p> } export const getServerSideProps = async (req) => { const res = await fetch(`http://localhost:3000/api/route`) const data = await res.json() return { props: { edgeRoute: data.ok ? `Hi from the edge route` : '' } } } // pages/api/route.js import { hasDynamic } from '../../lib' export default async function handle() { hasDynamic() return Response.json({ ok: true }) } export const config = { runtime: 'experimental-edge' , allowDynamic: '/lib/**' } ``` Playing with `config.allowDynamic`, you should be able to: - build the app even if it uses `eval()` (it will obviously fail at runtime) - build the app that _imports but does not use_ `eval()` - run the app in dev, even if it uses `eval()` with no warning ### 🆙 Notes to reviewers Before adding documentation and telemetry, I'd like to collect comments on a couple of points: - the overall design for this feature: is a list of globs useful and easy enough? - should the globs be relative to the application root (current implementation) to to the edge route/middleware file? - (especially to @sokra) is the implementation idiomatic enough? I've leverage loaders to read the _entry point_ configuration once, then the ModuleGraph to get it back during the parsing phase. I couldn't re-use the existing `getExtractMetadata()` facility since it's happening late after the parsing. - there's a glitch with `import { ServerRuntime } from '../../types'` in `get-page-static-info.ts` ([here](https://github.com/vercel/next.js/pull/39539/files#diff-cb7ac6392c3dd707c5edab159c3144ec114eafea92dad5d98f4eedfc612174d2L12)). I had to use `next/types` because it was failing during lint. Any clue why? ### ☑️ Checklist - [ ] 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` - [x] Integration tests added - [x] Documentation added - [x] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md`
2022-09-13 00:01:00 +02:00
import { matcher } from 'next/dist/compiled/micromatch'
import { RSC_MODULE_TYPES } from '../../shared/lib/constants'
export interface MiddlewareConfig {
matchers: MiddlewareMatcher[]
unstable_allowDynamicGlobs: string[]
regions: string[] | string
}
export interface MiddlewareMatcher {
regexp: string
locale?: false
has?: RouteHas[]
}
export interface PageStaticInfo {
runtime?: ServerRuntime
ssg?: boolean
ssr?: boolean
rsc?: RSCModuleType
middleware?: Partial<MiddlewareConfig>
}
const CLIENT_MODULE_LABEL = `/* __next_internal_client_entry_do_not_use__ */`
export type RSCModuleType = 'server' | 'client'
export function getRSCModuleType(source: string): RSCModuleType {
return source.includes(CLIENT_MODULE_LABEL)
? RSC_MODULE_TYPES.client
: RSC_MODULE_TYPES.server
}
/**
* Receives a parsed AST from SWC and checks if it belongs to a module that
* requires a runtime to be specified. Those are:
* - Modules with `export function getStaticProps | getServerSideProps`
* - Modules with `export { getStaticProps | getServerSideProps } <from ...>`
* - Modules with `export const runtime = ...`
*/
function checkExports(swcAST: any): {
ssr: boolean
ssg: boolean
runtime?: string
} {
if (Array.isArray(swcAST?.body)) {
try {
let runtime: string | undefined
let ssr: boolean = false
let ssg: boolean = false
for (const node of swcAST.body) {
if (
node.type === 'ExportDeclaration' &&
node.declaration?.type === 'VariableDeclaration'
) {
const id = node.declaration?.declarations[0]?.id.value
if (id === 'runtime') {
runtime = node.declaration?.declarations[0]?.init.value
}
}
if (
node.type === 'ExportDeclaration' &&
node.declaration?.type === 'FunctionDeclaration' &&
['getStaticProps', 'getServerSideProps'].includes(
node.declaration.identifier?.value
)
) {
ssg = node.declaration.identifier.value === 'getStaticProps'
ssr = node.declaration.identifier.value === 'getServerSideProps'
}
if (
node.type === 'ExportDeclaration' &&
node.declaration?.type === 'VariableDeclaration'
) {
const id = node.declaration?.declarations[0]?.id.value
if (['getStaticProps', 'getServerSideProps'].includes(id)) {
ssg = id === 'getStaticProps'
ssr = id === 'getServerSideProps'
}
}
if (node.type === 'ExportNamedDeclaration') {
const values = node.specifiers.map(
(specifier: any) =>
specifier.type === 'ExportSpecifier' &&
specifier.orig?.type === 'Identifier' &&
specifier.orig?.value
)
ssg = values.some((value: any) => ['getStaticProps'].includes(value))
ssr = values.some((value: any) =>
['getServerSideProps'].includes(value)
)
}
}
return { ssr, ssg, runtime }
} catch (err) {}
}
return { ssg: false, ssr: false }
}
async function tryToReadFile(filePath: string, shouldThrow: boolean) {
try {
return await fs.readFile(filePath, {
encoding: 'utf8',
})
} catch (error) {
if (shouldThrow) {
throw error
}
}
}
function getMiddlewareMatchers(
matcherOrMatchers: unknown,
nextConfig: NextConfig
): MiddlewareMatcher[] {
let matchers: unknown[] = []
if (Array.isArray(matcherOrMatchers)) {
matchers = matcherOrMatchers
} else {
matchers.push(matcherOrMatchers)
}
const { i18n } = nextConfig
let routes = matchers.map(
(m) => (typeof m === 'string' ? { source: m } : m) as Middleware
)
// check before we process the routes and after to ensure
// they are still valid
checkCustomRoutes(routes, 'middleware')
routes = routes.map((r) => {
let { source } = r
const isRoot = source === '/'
if (i18n?.locales && r.locale !== false) {
source = `/:nextInternalLocale([^/.]{1,})${isRoot ? '' : source}`
}
source = `/:nextData(_next/data/[^/]{1,})?${source}${
isRoot
? `(${nextConfig.i18n ? '|\\.json|' : ''}/?index|/?index\\.json)?`
: '(.json)?'
}`
if (nextConfig.basePath) {
source = `${nextConfig.basePath}${source}`
}
return { ...r, source }
})
checkCustomRoutes(routes, 'middleware')
return routes.map((r) => {
const { source, ...rest } = r
const parsedPage = tryToParsePath(source)
if (parsedPage.error || !parsedPage.regexStr) {
throw new Error(`Invalid source: ${source}`)
}
return {
...rest,
regexp: parsedPage.regexStr,
}
})
}
function getMiddlewareConfig(
feat(edge): allows configuring Dynamic code execution guard (#39539) ### 📖 What's in there? Dynamic code evaluation (`eval()`, `new Function()`, ...) is not supported on the edge runtime, hence why we fail the build when detecting such statement in the middleware or `experimental-edge` routes at build time. However, there could be false positives, which static analysis and tree-shaking can not exclude: - `qs` through these dependencies (get-intrinsic: [source](https://github.com/ljharb/get-intrinsic/blob/main/index.js#L12)) - `function-bind` ([source](https://github.com/Raynos/function-bind/blob/master/implementation.js#L42)) - `has` ([source](https://github.com/tarruda/has/blob/master/src/index.js#L5)) This PR leverages the existing `config` export to let user allow some of their files. it’s meant for allowing users to import 3rd party modules who embed dynamic code evaluation, but do not use it (because or code paths), and can't be tree-shaked. By default, it’s keeping the existing behavior: warn in dev, fails to build. If users allow dynamic code, and that code is reached at runtime, their app stills breaks. ### 🧪 How to test? - (existing) integration tests for disallowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-dynamic` - (new) integration tests for allowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-configurable` - (amended) production tests for validating the new configuration keys: `pnpm testheadless --testPathPattern=config-validations` To try it live, you could have an application such as: ```js // lib/index.js /* eslint-disable no-eval */ export function hasUnusedDynamic() { if ((() => false)()) { eval('100') } } export function hasDynamic() { eval('100') } // pages/index.jsx export default function Page({ edgeRoute }) { return <p>{edgeRoute}</p> } export const getServerSideProps = async (req) => { const res = await fetch(`http://localhost:3000/api/route`) const data = await res.json() return { props: { edgeRoute: data.ok ? `Hi from the edge route` : '' } } } // pages/api/route.js import { hasDynamic } from '../../lib' export default async function handle() { hasDynamic() return Response.json({ ok: true }) } export const config = { runtime: 'experimental-edge' , allowDynamic: '/lib/**' } ``` Playing with `config.allowDynamic`, you should be able to: - build the app even if it uses `eval()` (it will obviously fail at runtime) - build the app that _imports but does not use_ `eval()` - run the app in dev, even if it uses `eval()` with no warning ### 🆙 Notes to reviewers Before adding documentation and telemetry, I'd like to collect comments on a couple of points: - the overall design for this feature: is a list of globs useful and easy enough? - should the globs be relative to the application root (current implementation) to to the edge route/middleware file? - (especially to @sokra) is the implementation idiomatic enough? I've leverage loaders to read the _entry point_ configuration once, then the ModuleGraph to get it back during the parsing phase. I couldn't re-use the existing `getExtractMetadata()` facility since it's happening late after the parsing. - there's a glitch with `import { ServerRuntime } from '../../types'` in `get-page-static-info.ts` ([here](https://github.com/vercel/next.js/pull/39539/files#diff-cb7ac6392c3dd707c5edab159c3144ec114eafea92dad5d98f4eedfc612174d2L12)). I had to use `next/types` because it was failing during lint. Any clue why? ### ☑️ Checklist - [ ] 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` - [x] Integration tests added - [x] Documentation added - [x] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md`
2022-09-13 00:01:00 +02:00
pageFilePath: string,
config: any,
nextConfig: NextConfig
): Partial<MiddlewareConfig> {
const result: Partial<MiddlewareConfig> = {}
if (config.matcher) {
result.matchers = getMiddlewareMatchers(config.matcher, nextConfig)
}
if (typeof config.regions === 'string' || Array.isArray(config.regions)) {
result.regions = config.regions
} else if (typeof config.regions !== 'undefined') {
Log.warn(
`The \`regions\` config was ignored: config must be empty, a string or an array of strings. (${pageFilePath})`
)
}
if (config.unstable_allowDynamic) {
result.unstable_allowDynamicGlobs = Array.isArray(
config.unstable_allowDynamic
)
? config.unstable_allowDynamic
: [config.unstable_allowDynamic]
for (const glob of result.unstable_allowDynamicGlobs ?? []) {
feat(edge): allows configuring Dynamic code execution guard (#39539) ### 📖 What's in there? Dynamic code evaluation (`eval()`, `new Function()`, ...) is not supported on the edge runtime, hence why we fail the build when detecting such statement in the middleware or `experimental-edge` routes at build time. However, there could be false positives, which static analysis and tree-shaking can not exclude: - `qs` through these dependencies (get-intrinsic: [source](https://github.com/ljharb/get-intrinsic/blob/main/index.js#L12)) - `function-bind` ([source](https://github.com/Raynos/function-bind/blob/master/implementation.js#L42)) - `has` ([source](https://github.com/tarruda/has/blob/master/src/index.js#L5)) This PR leverages the existing `config` export to let user allow some of their files. it’s meant for allowing users to import 3rd party modules who embed dynamic code evaluation, but do not use it (because or code paths), and can't be tree-shaked. By default, it’s keeping the existing behavior: warn in dev, fails to build. If users allow dynamic code, and that code is reached at runtime, their app stills breaks. ### 🧪 How to test? - (existing) integration tests for disallowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-dynamic` - (new) integration tests for allowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-configurable` - (amended) production tests for validating the new configuration keys: `pnpm testheadless --testPathPattern=config-validations` To try it live, you could have an application such as: ```js // lib/index.js /* eslint-disable no-eval */ export function hasUnusedDynamic() { if ((() => false)()) { eval('100') } } export function hasDynamic() { eval('100') } // pages/index.jsx export default function Page({ edgeRoute }) { return <p>{edgeRoute}</p> } export const getServerSideProps = async (req) => { const res = await fetch(`http://localhost:3000/api/route`) const data = await res.json() return { props: { edgeRoute: data.ok ? `Hi from the edge route` : '' } } } // pages/api/route.js import { hasDynamic } from '../../lib' export default async function handle() { hasDynamic() return Response.json({ ok: true }) } export const config = { runtime: 'experimental-edge' , allowDynamic: '/lib/**' } ``` Playing with `config.allowDynamic`, you should be able to: - build the app even if it uses `eval()` (it will obviously fail at runtime) - build the app that _imports but does not use_ `eval()` - run the app in dev, even if it uses `eval()` with no warning ### 🆙 Notes to reviewers Before adding documentation and telemetry, I'd like to collect comments on a couple of points: - the overall design for this feature: is a list of globs useful and easy enough? - should the globs be relative to the application root (current implementation) to to the edge route/middleware file? - (especially to @sokra) is the implementation idiomatic enough? I've leverage loaders to read the _entry point_ configuration once, then the ModuleGraph to get it back during the parsing phase. I couldn't re-use the existing `getExtractMetadata()` facility since it's happening late after the parsing. - there's a glitch with `import { ServerRuntime } from '../../types'` in `get-page-static-info.ts` ([here](https://github.com/vercel/next.js/pull/39539/files#diff-cb7ac6392c3dd707c5edab159c3144ec114eafea92dad5d98f4eedfc612174d2L12)). I had to use `next/types` because it was failing during lint. Any clue why? ### ☑️ Checklist - [ ] 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` - [x] Integration tests added - [x] Documentation added - [x] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md`
2022-09-13 00:01:00 +02:00
try {
matcher(glob)
} catch (err) {
throw new Error(
`${pageFilePath} exported 'config.unstable_allowDynamic' contains invalid pattern '${glob}': ${
feat(edge): allows configuring Dynamic code execution guard (#39539) ### 📖 What's in there? Dynamic code evaluation (`eval()`, `new Function()`, ...) is not supported on the edge runtime, hence why we fail the build when detecting such statement in the middleware or `experimental-edge` routes at build time. However, there could be false positives, which static analysis and tree-shaking can not exclude: - `qs` through these dependencies (get-intrinsic: [source](https://github.com/ljharb/get-intrinsic/blob/main/index.js#L12)) - `function-bind` ([source](https://github.com/Raynos/function-bind/blob/master/implementation.js#L42)) - `has` ([source](https://github.com/tarruda/has/blob/master/src/index.js#L5)) This PR leverages the existing `config` export to let user allow some of their files. it’s meant for allowing users to import 3rd party modules who embed dynamic code evaluation, but do not use it (because or code paths), and can't be tree-shaked. By default, it’s keeping the existing behavior: warn in dev, fails to build. If users allow dynamic code, and that code is reached at runtime, their app stills breaks. ### 🧪 How to test? - (existing) integration tests for disallowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-dynamic` - (new) integration tests for allowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-configurable` - (amended) production tests for validating the new configuration keys: `pnpm testheadless --testPathPattern=config-validations` To try it live, you could have an application such as: ```js // lib/index.js /* eslint-disable no-eval */ export function hasUnusedDynamic() { if ((() => false)()) { eval('100') } } export function hasDynamic() { eval('100') } // pages/index.jsx export default function Page({ edgeRoute }) { return <p>{edgeRoute}</p> } export const getServerSideProps = async (req) => { const res = await fetch(`http://localhost:3000/api/route`) const data = await res.json() return { props: { edgeRoute: data.ok ? `Hi from the edge route` : '' } } } // pages/api/route.js import { hasDynamic } from '../../lib' export default async function handle() { hasDynamic() return Response.json({ ok: true }) } export const config = { runtime: 'experimental-edge' , allowDynamic: '/lib/**' } ``` Playing with `config.allowDynamic`, you should be able to: - build the app even if it uses `eval()` (it will obviously fail at runtime) - build the app that _imports but does not use_ `eval()` - run the app in dev, even if it uses `eval()` with no warning ### 🆙 Notes to reviewers Before adding documentation and telemetry, I'd like to collect comments on a couple of points: - the overall design for this feature: is a list of globs useful and easy enough? - should the globs be relative to the application root (current implementation) to to the edge route/middleware file? - (especially to @sokra) is the implementation idiomatic enough? I've leverage loaders to read the _entry point_ configuration once, then the ModuleGraph to get it back during the parsing phase. I couldn't re-use the existing `getExtractMetadata()` facility since it's happening late after the parsing. - there's a glitch with `import { ServerRuntime } from '../../types'` in `get-page-static-info.ts` ([here](https://github.com/vercel/next.js/pull/39539/files#diff-cb7ac6392c3dd707c5edab159c3144ec114eafea92dad5d98f4eedfc612174d2L12)). I had to use `next/types` because it was failing during lint. Any clue why? ### ☑️ Checklist - [ ] 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` - [x] Integration tests added - [x] Documentation added - [x] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md`
2022-09-13 00:01:00 +02:00
(err as Error).message
}`
)
}
}
}
return result
}
let warnedAboutExperimentalEdgeApiFunctions = false
function warnAboutExperimentalEdgeApiFunctions() {
if (warnedAboutExperimentalEdgeApiFunctions) {
return
}
Log.warn(`You are using an experimental edge runtime, the API might change.`)
warnedAboutExperimentalEdgeApiFunctions = true
}
const warnedUnsupportedValueMap = new Map<string, boolean>()
function warnAboutUnsupportedValue(
pageFilePath: string,
page: string | undefined,
error: UnsupportedValueError
) {
if (warnedUnsupportedValueMap.has(pageFilePath)) {
return
}
Log.warn(
`Next.js can't recognize the exported \`config\` field in ` +
(page ? `route "${page}"` : `"${pageFilePath}"`) +
':\n' +
error.message +
(error.path ? ` at "${error.path}"` : '') +
'.\n' +
'The default config will be used instead.\n' +
'Read More - https://nextjs.org/docs/messages/invalid-page-config'
)
warnedUnsupportedValueMap.set(pageFilePath, true)
}
/**
* For a given pageFilePath and nextConfig, if the config supports it, this
* function will read the file and return the runtime that should be used.
* It will look into the file content only if the page *requires* a runtime
* to be specified, that is, when gSSP or gSP is used.
* Related discussion: https://github.com/vercel/next.js/discussions/34179
*/
export async function getPageStaticInfo(params: {
nextConfig: Partial<NextConfig>
pageFilePath: string
isDev?: boolean
page?: string
}): Promise<PageStaticInfo> {
const { isDev, pageFilePath, nextConfig, page } = params
const fileContent = (await tryToReadFile(pageFilePath, !isDev)) || ''
feat(edge): allows configuring Dynamic code execution guard (#39539) ### 📖 What's in there? Dynamic code evaluation (`eval()`, `new Function()`, ...) is not supported on the edge runtime, hence why we fail the build when detecting such statement in the middleware or `experimental-edge` routes at build time. However, there could be false positives, which static analysis and tree-shaking can not exclude: - `qs` through these dependencies (get-intrinsic: [source](https://github.com/ljharb/get-intrinsic/blob/main/index.js#L12)) - `function-bind` ([source](https://github.com/Raynos/function-bind/blob/master/implementation.js#L42)) - `has` ([source](https://github.com/tarruda/has/blob/master/src/index.js#L5)) This PR leverages the existing `config` export to let user allow some of their files. it’s meant for allowing users to import 3rd party modules who embed dynamic code evaluation, but do not use it (because or code paths), and can't be tree-shaked. By default, it’s keeping the existing behavior: warn in dev, fails to build. If users allow dynamic code, and that code is reached at runtime, their app stills breaks. ### 🧪 How to test? - (existing) integration tests for disallowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-dynamic` - (new) integration tests for allowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-configurable` - (amended) production tests for validating the new configuration keys: `pnpm testheadless --testPathPattern=config-validations` To try it live, you could have an application such as: ```js // lib/index.js /* eslint-disable no-eval */ export function hasUnusedDynamic() { if ((() => false)()) { eval('100') } } export function hasDynamic() { eval('100') } // pages/index.jsx export default function Page({ edgeRoute }) { return <p>{edgeRoute}</p> } export const getServerSideProps = async (req) => { const res = await fetch(`http://localhost:3000/api/route`) const data = await res.json() return { props: { edgeRoute: data.ok ? `Hi from the edge route` : '' } } } // pages/api/route.js import { hasDynamic } from '../../lib' export default async function handle() { hasDynamic() return Response.json({ ok: true }) } export const config = { runtime: 'experimental-edge' , allowDynamic: '/lib/**' } ``` Playing with `config.allowDynamic`, you should be able to: - build the app even if it uses `eval()` (it will obviously fail at runtime) - build the app that _imports but does not use_ `eval()` - run the app in dev, even if it uses `eval()` with no warning ### 🆙 Notes to reviewers Before adding documentation and telemetry, I'd like to collect comments on a couple of points: - the overall design for this feature: is a list of globs useful and easy enough? - should the globs be relative to the application root (current implementation) to to the edge route/middleware file? - (especially to @sokra) is the implementation idiomatic enough? I've leverage loaders to read the _entry point_ configuration once, then the ModuleGraph to get it back during the parsing phase. I couldn't re-use the existing `getExtractMetadata()` facility since it's happening late after the parsing. - there's a glitch with `import { ServerRuntime } from '../../types'` in `get-page-static-info.ts` ([here](https://github.com/vercel/next.js/pull/39539/files#diff-cb7ac6392c3dd707c5edab159c3144ec114eafea92dad5d98f4eedfc612174d2L12)). I had to use `next/types` because it was failing during lint. Any clue why? ### ☑️ Checklist - [ ] 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` - [x] Integration tests added - [x] Documentation added - [x] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md`
2022-09-13 00:01:00 +02:00
if (
/runtime|getStaticProps|getServerSideProps|export const config/.test(
feat(edge): allows configuring Dynamic code execution guard (#39539) ### 📖 What's in there? Dynamic code evaluation (`eval()`, `new Function()`, ...) is not supported on the edge runtime, hence why we fail the build when detecting such statement in the middleware or `experimental-edge` routes at build time. However, there could be false positives, which static analysis and tree-shaking can not exclude: - `qs` through these dependencies (get-intrinsic: [source](https://github.com/ljharb/get-intrinsic/blob/main/index.js#L12)) - `function-bind` ([source](https://github.com/Raynos/function-bind/blob/master/implementation.js#L42)) - `has` ([source](https://github.com/tarruda/has/blob/master/src/index.js#L5)) This PR leverages the existing `config` export to let user allow some of their files. it’s meant for allowing users to import 3rd party modules who embed dynamic code evaluation, but do not use it (because or code paths), and can't be tree-shaked. By default, it’s keeping the existing behavior: warn in dev, fails to build. If users allow dynamic code, and that code is reached at runtime, their app stills breaks. ### 🧪 How to test? - (existing) integration tests for disallowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-dynamic` - (new) integration tests for allowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-configurable` - (amended) production tests for validating the new configuration keys: `pnpm testheadless --testPathPattern=config-validations` To try it live, you could have an application such as: ```js // lib/index.js /* eslint-disable no-eval */ export function hasUnusedDynamic() { if ((() => false)()) { eval('100') } } export function hasDynamic() { eval('100') } // pages/index.jsx export default function Page({ edgeRoute }) { return <p>{edgeRoute}</p> } export const getServerSideProps = async (req) => { const res = await fetch(`http://localhost:3000/api/route`) const data = await res.json() return { props: { edgeRoute: data.ok ? `Hi from the edge route` : '' } } } // pages/api/route.js import { hasDynamic } from '../../lib' export default async function handle() { hasDynamic() return Response.json({ ok: true }) } export const config = { runtime: 'experimental-edge' , allowDynamic: '/lib/**' } ``` Playing with `config.allowDynamic`, you should be able to: - build the app even if it uses `eval()` (it will obviously fail at runtime) - build the app that _imports but does not use_ `eval()` - run the app in dev, even if it uses `eval()` with no warning ### 🆙 Notes to reviewers Before adding documentation and telemetry, I'd like to collect comments on a couple of points: - the overall design for this feature: is a list of globs useful and easy enough? - should the globs be relative to the application root (current implementation) to to the edge route/middleware file? - (especially to @sokra) is the implementation idiomatic enough? I've leverage loaders to read the _entry point_ configuration once, then the ModuleGraph to get it back during the parsing phase. I couldn't re-use the existing `getExtractMetadata()` facility since it's happening late after the parsing. - there's a glitch with `import { ServerRuntime } from '../../types'` in `get-page-static-info.ts` ([here](https://github.com/vercel/next.js/pull/39539/files#diff-cb7ac6392c3dd707c5edab159c3144ec114eafea92dad5d98f4eedfc612174d2L12)). I had to use `next/types` because it was failing during lint. Any clue why? ### ☑️ Checklist - [ ] 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` - [x] Integration tests added - [x] Documentation added - [x] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md`
2022-09-13 00:01:00 +02:00
fileContent
)
) {
const swcAST = await parseModule(pageFilePath, fileContent)
const { ssg, ssr, runtime } = checkExports(swcAST)
const rsc = getRSCModuleType(fileContent)
// default / failsafe value for config
let config: any = {}
try {
config = extractExportedConstValue(swcAST, 'config')
} catch (e) {
if (e instanceof UnsupportedValueError) {
warnAboutUnsupportedValue(pageFilePath, page, e)
}
// `export config` doesn't exist, or other unknown error throw by swc, silence them
}
// Currently, we use `export const config = { runtime: '...' }` to specify the page runtime.
// But in the new app directory, we prefer to use `export const runtime = '...'`
// and deprecate the old way. To prevent breaking changes for `pages`, we use the exported config
// as the fallback value.
let resolvedRuntime = runtime || config.runtime
if (
typeof resolvedRuntime !== 'undefined' &&
!isServerRuntime(resolvedRuntime)
) {
const options = Object.values(SERVER_RUNTIME).join(', ')
if (typeof resolvedRuntime !== 'string') {
Log.error(
`The \`runtime\` config must be a string. Please leave it empty or choose one of: ${options}`
)
} else {
Log.error(
`Provided runtime "${config.runtime}" is not supported. Please leave it empty or choose one of: ${options}`
)
}
if (!isDev) {
process.exit(1)
}
}
resolvedRuntime =
SERVER_RUNTIME.edge === resolvedRuntime
? SERVER_RUNTIME.edge
: ssr || ssg
? resolvedRuntime || nextConfig.experimental?.runtime
: undefined
if (resolvedRuntime === SERVER_RUNTIME.edge) {
warnAboutExperimentalEdgeApiFunctions()
}
feat(edge): allows configuring Dynamic code execution guard (#39539) ### 📖 What's in there? Dynamic code evaluation (`eval()`, `new Function()`, ...) is not supported on the edge runtime, hence why we fail the build when detecting such statement in the middleware or `experimental-edge` routes at build time. However, there could be false positives, which static analysis and tree-shaking can not exclude: - `qs` through these dependencies (get-intrinsic: [source](https://github.com/ljharb/get-intrinsic/blob/main/index.js#L12)) - `function-bind` ([source](https://github.com/Raynos/function-bind/blob/master/implementation.js#L42)) - `has` ([source](https://github.com/tarruda/has/blob/master/src/index.js#L5)) This PR leverages the existing `config` export to let user allow some of their files. it’s meant for allowing users to import 3rd party modules who embed dynamic code evaluation, but do not use it (because or code paths), and can't be tree-shaked. By default, it’s keeping the existing behavior: warn in dev, fails to build. If users allow dynamic code, and that code is reached at runtime, their app stills breaks. ### 🧪 How to test? - (existing) integration tests for disallowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-dynamic` - (new) integration tests for allowing dynamic code evaluation: `pnpm testheadless --testPathPattern=runtime-configurable` - (amended) production tests for validating the new configuration keys: `pnpm testheadless --testPathPattern=config-validations` To try it live, you could have an application such as: ```js // lib/index.js /* eslint-disable no-eval */ export function hasUnusedDynamic() { if ((() => false)()) { eval('100') } } export function hasDynamic() { eval('100') } // pages/index.jsx export default function Page({ edgeRoute }) { return <p>{edgeRoute}</p> } export const getServerSideProps = async (req) => { const res = await fetch(`http://localhost:3000/api/route`) const data = await res.json() return { props: { edgeRoute: data.ok ? `Hi from the edge route` : '' } } } // pages/api/route.js import { hasDynamic } from '../../lib' export default async function handle() { hasDynamic() return Response.json({ ok: true }) } export const config = { runtime: 'experimental-edge' , allowDynamic: '/lib/**' } ``` Playing with `config.allowDynamic`, you should be able to: - build the app even if it uses `eval()` (it will obviously fail at runtime) - build the app that _imports but does not use_ `eval()` - run the app in dev, even if it uses `eval()` with no warning ### 🆙 Notes to reviewers Before adding documentation and telemetry, I'd like to collect comments on a couple of points: - the overall design for this feature: is a list of globs useful and easy enough? - should the globs be relative to the application root (current implementation) to to the edge route/middleware file? - (especially to @sokra) is the implementation idiomatic enough? I've leverage loaders to read the _entry point_ configuration once, then the ModuleGraph to get it back during the parsing phase. I couldn't re-use the existing `getExtractMetadata()` facility since it's happening late after the parsing. - there's a glitch with `import { ServerRuntime } from '../../types'` in `get-page-static-info.ts` ([here](https://github.com/vercel/next.js/pull/39539/files#diff-cb7ac6392c3dd707c5edab159c3144ec114eafea92dad5d98f4eedfc612174d2L12)). I had to use `next/types` because it was failing during lint. Any clue why? ### ☑️ Checklist - [ ] 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` - [x] Integration tests added - [x] Documentation added - [x] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md`
2022-09-13 00:01:00 +02:00
const middlewareConfig = getMiddlewareConfig(
page ?? 'middleware/edge API route',
config,
nextConfig
)
return {
ssr,
ssg,
rsc,
...(middlewareConfig && { middleware: middlewareConfig }),
...(resolvedRuntime && { runtime: resolvedRuntime }),
}
}
return {
ssr: false,
ssg: false,
rsc: RSC_MODULE_TYPES.server,
runtime: nextConfig.experimental?.runtime,
}
}