-
Notifications
You must be signed in to change notification settings - Fork 26.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor define-env-plugin to have stricter types #63128
Changes from all commits
cd5e9b2
a4eb7d0
bcc2b73
4eac0c9
c1684af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,4 +1,7 @@ | ||||||
import type { NextConfigComplete } from '../../../server/config-shared' | ||||||
import type { | ||||||
I18NDomains, | ||||||
NextConfigComplete, | ||||||
} from '../../../server/config-shared' | ||||||
import type { MiddlewareMatcher } from '../../analysis/get-page-static-info' | ||||||
import { webpack } from 'next/dist/compiled/webpack/webpack' | ||||||
import { needsExperimentalReact } from '../../../lib/needs-experimental-react' | ||||||
|
@@ -14,16 +17,16 @@ function errorIfEnvConflicted(config: NextConfigComplete, key: string) { | |||||
} | ||||||
} | ||||||
|
||||||
type BloomFilter = ReturnType< | ||||||
import('../../../shared/lib/bloom-filter').BloomFilter['export'] | ||||||
> | ||||||
|
||||||
export interface DefineEnvPluginOptions { | ||||||
isTurbopack: boolean | ||||||
allowedRevalidateHeaderKeys: string[] | undefined | ||||||
clientRouterFilters?: { | ||||||
staticFilter: ReturnType< | ||||||
import('../../../shared/lib/bloom-filter').BloomFilter['export'] | ||||||
> | ||||||
dynamicFilter: ReturnType< | ||||||
import('../../../shared/lib/bloom-filter').BloomFilter['export'] | ||||||
> | ||||||
staticFilter: BloomFilter | ||||||
dynamicFilter: BloomFilter | ||||||
} | ||||||
config: NextConfigComplete | ||||||
dev: boolean | ||||||
|
@@ -38,6 +41,92 @@ export interface DefineEnvPluginOptions { | |||||
previewModeId: string | undefined | ||||||
} | ||||||
|
||||||
interface DefineEnv { | ||||||
[key: string]: | ||||||
| string | ||||||
| string[] | ||||||
| boolean | ||||||
| undefined | ||||||
timneutkens marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| MiddlewareMatcher[] | ||||||
| BloomFilter | ||||||
| Partial<NextConfigComplete['images']> | ||||||
| I18NDomains | ||||||
} | ||||||
|
||||||
interface SerializedDefineEnv { | ||||||
[key: string]: string | ||||||
} | ||||||
|
||||||
/** | ||||||
* Collects all environment variables that are using the `NEXT_PUBLIC_` prefix. | ||||||
*/ | ||||||
function getNextPublicEnvironmentVariables(): DefineEnv { | ||||||
const defineEnv: DefineEnv = {} | ||||||
for (const key in process.env) { | ||||||
if (key.startsWith('NEXT_PUBLIC_')) { | ||||||
const value = process.env[key] | ||||||
if (value) { | ||||||
defineEnv[`process.env.${key}`] = value | ||||||
} | ||||||
} | ||||||
} | ||||||
return defineEnv | ||||||
} | ||||||
|
||||||
/** | ||||||
* Collects the `env` config value from the Next.js config. | ||||||
*/ | ||||||
function getNextConfigEnv(config: NextConfigComplete): DefineEnv { | ||||||
// Refactored code below to use for-of | ||||||
const defineEnv: DefineEnv = {} | ||||||
const env = config.env | ||||||
for (const key in env) { | ||||||
const value = env[key] | ||||||
if (value) { | ||||||
errorIfEnvConflicted(config, key) | ||||||
defineEnv[`process.env.${key}`] = value | ||||||
} | ||||||
} | ||||||
return defineEnv | ||||||
} | ||||||
|
||||||
/** | ||||||
* Serializes the DefineEnv config so that it can be inserted into the code by Webpack/Turbopack, JSON stringifies each value. | ||||||
*/ | ||||||
function serializeDefineEnv(defineEnv: DefineEnv): SerializedDefineEnv { | ||||||
const defineEnvStringified: SerializedDefineEnv = {} | ||||||
for (const key in defineEnv) { | ||||||
const value = defineEnv[key] | ||||||
defineEnvStringified[key] = JSON.stringify(value) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
return defineEnvStringified | ||||||
} | ||||||
|
||||||
function getImageConfig( | ||||||
config: NextConfigComplete, | ||||||
dev: boolean | ||||||
): { 'process.env.__NEXT_IMAGE_OPTS': Partial<NextConfigComplete['images']> } { | ||||||
return { | ||||||
'process.env.__NEXT_IMAGE_OPTS': { | ||||||
deviceSizes: config.images.deviceSizes, | ||||||
imageSizes: config.images.imageSizes, | ||||||
path: config.images.path, | ||||||
loader: config.images.loader, | ||||||
dangerouslyAllowSVG: config.images.dangerouslyAllowSVG, | ||||||
unoptimized: config?.images?.unoptimized, | ||||||
...(dev | ||||||
? { | ||||||
// pass domains in development to allow validating on the client | ||||||
domains: config.images.domains, | ||||||
remotePatterns: config.images?.remotePatterns, | ||||||
output: config.output, | ||||||
} | ||||||
: {}), | ||||||
}, | ||||||
} | ||||||
} | ||||||
|
||||||
export function getDefineEnv({ | ||||||
isTurbopack, | ||||||
allowedRevalidateHeaderKeys, | ||||||
|
@@ -53,180 +142,117 @@ export function getDefineEnv({ | |||||
isNodeServer, | ||||||
middlewareMatchers, | ||||||
previewModeId, | ||||||
}: DefineEnvPluginOptions) { | ||||||
return { | ||||||
}: DefineEnvPluginOptions): SerializedDefineEnv { | ||||||
const defineEnv: DefineEnv = { | ||||||
// internal field to identify the plugin config | ||||||
__NEXT_DEFINE_ENV: 'true', | ||||||
__NEXT_DEFINE_ENV: true, | ||||||
|
||||||
...Object.keys(process.env).reduce( | ||||||
(prev: { [key: string]: string }, key: string) => { | ||||||
if (key.startsWith('NEXT_PUBLIC_')) { | ||||||
prev[`process.env.${key}`] = JSON.stringify(process.env[key]!) | ||||||
} | ||||||
return prev | ||||||
}, | ||||||
{} | ||||||
), | ||||||
...Object.keys(config.env).reduce((acc, key) => { | ||||||
errorIfEnvConflicted(config, key) | ||||||
|
||||||
return { | ||||||
...acc, | ||||||
[`process.env.${key}`]: JSON.stringify(config.env[key]), | ||||||
} | ||||||
}, {}), | ||||||
...getNextPublicEnvironmentVariables(), | ||||||
...getNextConfigEnv(config), | ||||||
...(!isEdgeServer | ||||||
? {} | ||||||
: { | ||||||
EdgeRuntime: JSON.stringify( | ||||||
EdgeRuntime: | ||||||
/** | ||||||
* Cloud providers can set this environment variable to allow users | ||||||
* and library authors to have different implementations based on | ||||||
* the runtime they are running with, if it's not using `edge-runtime` | ||||||
*/ | ||||||
process.env.NEXT_EDGE_RUNTIME_PROVIDER || 'edge-runtime' | ||||||
), | ||||||
process.env.NEXT_EDGE_RUNTIME_PROVIDER || 'edge-runtime', | ||||||
}), | ||||||
'process.turbopack': JSON.stringify(isTurbopack), | ||||||
'process.env.TURBOPACK': JSON.stringify(isTurbopack), | ||||||
'process.turbopack': isTurbopack, | ||||||
'process.env.TURBOPACK': isTurbopack, | ||||||
// TODO: enforce `NODE_ENV` on `process.env`, and add a test: | ||||||
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), | ||||||
'process.env.NEXT_RUNTIME': JSON.stringify( | ||||||
isEdgeServer ? 'edge' : isNodeServer ? 'nodejs' : '' | ||||||
), | ||||||
'process.env.NEXT_MINIMAL': JSON.stringify(''), | ||||||
'process.env.__NEXT_PPR': JSON.stringify(config.experimental.ppr === true), | ||||||
'process.env.__NEXT_ACTIONS_DEPLOYMENT_ID': JSON.stringify( | ||||||
config.experimental.useDeploymentIdServerActions | ||||||
), | ||||||
'process.env.NEXT_DEPLOYMENT_ID': JSON.stringify( | ||||||
config.experimental.deploymentId || false | ||||||
), | ||||||
'process.env.__NEXT_FETCH_CACHE_KEY_PREFIX': | ||||||
JSON.stringify(fetchCacheKeyPrefix), | ||||||
'process.env.__NEXT_PREVIEW_MODE_ID': JSON.stringify(previewModeId), | ||||||
'process.env.__NEXT_ALLOWED_REVALIDATE_HEADERS': JSON.stringify( | ||||||
allowedRevalidateHeaderKeys | ||||||
), | ||||||
'process.env.__NEXT_MIDDLEWARE_MATCHERS': JSON.stringify( | ||||||
middlewareMatchers || [] | ||||||
), | ||||||
'process.env.__NEXT_MANUAL_CLIENT_BASE_PATH': JSON.stringify( | ||||||
config.experimental.manualClientBasePath | ||||||
), | ||||||
'process.env.__NEXT_CLIENT_ROUTER_FILTER_ENABLED': JSON.stringify( | ||||||
config.experimental.clientRouterFilter | ||||||
), | ||||||
'process.env.__NEXT_CLIENT_ROUTER_S_FILTER': JSON.stringify( | ||||||
clientRouterFilters?.staticFilter | ||||||
), | ||||||
'process.env.__NEXT_CLIENT_ROUTER_D_FILTER': JSON.stringify( | ||||||
clientRouterFilters?.dynamicFilter | ||||||
), | ||||||
'process.env.__NEXT_OPTIMISTIC_CLIENT_CACHE': JSON.stringify( | ||||||
config.experimental.optimisticClientCache | ||||||
), | ||||||
'process.env.__NEXT_MIDDLEWARE_PREFETCH': JSON.stringify( | ||||||
config.experimental.middlewarePrefetch | ||||||
), | ||||||
'process.env.__NEXT_CROSS_ORIGIN': JSON.stringify(config.crossOrigin), | ||||||
'process.browser': JSON.stringify(isClient), | ||||||
'process.env.__NEXT_TEST_MODE': JSON.stringify( | ||||||
process.env.__NEXT_TEST_MODE | ||||||
), | ||||||
'process.env.NODE_ENV': dev ? 'development' : 'production', | ||||||
'process.env.NEXT_RUNTIME': isEdgeServer | ||||||
? 'edge' | ||||||
: isNodeServer | ||||||
? 'nodejs' | ||||||
: '', | ||||||
'process.env.NEXT_MINIMAL': '', | ||||||
'process.env.__NEXT_PPR': config.experimental.ppr === true, | ||||||
'process.env.__NEXT_ACTIONS_DEPLOYMENT_ID': | ||||||
config.experimental.useDeploymentIdServerActions, | ||||||
'process.env.NEXT_DEPLOYMENT_ID': config.experimental.deploymentId || false, | ||||||
'process.env.__NEXT_FETCH_CACHE_KEY_PREFIX': fetchCacheKeyPrefix, | ||||||
'process.env.__NEXT_PREVIEW_MODE_ID': previewModeId, | ||||||
'process.env.__NEXT_ALLOWED_REVALIDATE_HEADERS': | ||||||
allowedRevalidateHeaderKeys, | ||||||
'process.env.__NEXT_MIDDLEWARE_MATCHERS': middlewareMatchers || [], | ||||||
'process.env.__NEXT_MANUAL_CLIENT_BASE_PATH': | ||||||
config.experimental.manualClientBasePath, | ||||||
'process.env.__NEXT_CLIENT_ROUTER_FILTER_ENABLED': | ||||||
config.experimental.clientRouterFilter, | ||||||
'process.env.__NEXT_CLIENT_ROUTER_S_FILTER': | ||||||
clientRouterFilters?.staticFilter, | ||||||
'process.env.__NEXT_CLIENT_ROUTER_D_FILTER': | ||||||
clientRouterFilters?.dynamicFilter, | ||||||
'process.env.__NEXT_OPTIMISTIC_CLIENT_CACHE': | ||||||
config.experimental.optimisticClientCache, | ||||||
'process.env.__NEXT_MIDDLEWARE_PREFETCH': | ||||||
config.experimental.middlewarePrefetch, | ||||||
'process.env.__NEXT_CROSS_ORIGIN': config.crossOrigin, | ||||||
'process.browser': isClient, | ||||||
'process.env.__NEXT_TEST_MODE': process.env.__NEXT_TEST_MODE, | ||||||
// This is used in client/dev-error-overlay/hot-dev-client.js to replace the dist directory | ||||||
...(dev && (isClient || isEdgeServer) | ||||||
? { | ||||||
'process.env.__NEXT_DIST_DIR': JSON.stringify(distDir), | ||||||
'process.env.__NEXT_DIST_DIR': distDir, | ||||||
} | ||||||
: {}), | ||||||
'process.env.__NEXT_TRAILING_SLASH': JSON.stringify(config.trailingSlash), | ||||||
'process.env.__NEXT_BUILD_INDICATOR': JSON.stringify( | ||||||
config.devIndicators.buildActivity | ||||||
), | ||||||
'process.env.__NEXT_BUILD_INDICATOR_POSITION': JSON.stringify( | ||||||
config.devIndicators.buildActivityPosition | ||||||
), | ||||||
'process.env.__NEXT_STRICT_MODE': JSON.stringify( | ||||||
config.reactStrictMode === null ? false : config.reactStrictMode | ||||||
), | ||||||
'process.env.__NEXT_STRICT_MODE_APP': JSON.stringify( | ||||||
'process.env.__NEXT_TRAILING_SLASH': config.trailingSlash, | ||||||
'process.env.__NEXT_BUILD_INDICATOR': config.devIndicators.buildActivity, | ||||||
'process.env.__NEXT_BUILD_INDICATOR_POSITION': | ||||||
config.devIndicators.buildActivityPosition, | ||||||
'process.env.__NEXT_STRICT_MODE': | ||||||
config.reactStrictMode === null ? false : config.reactStrictMode, | ||||||
'process.env.__NEXT_STRICT_MODE_APP': | ||||||
// When next.config.js does not have reactStrictMode it's enabled by default. | ||||||
config.reactStrictMode === null ? true : config.reactStrictMode | ||||||
), | ||||||
'process.env.__NEXT_OPTIMIZE_FONTS': JSON.stringify( | ||||||
!dev && config.optimizeFonts | ||||||
), | ||||||
'process.env.__NEXT_OPTIMIZE_CSS': JSON.stringify( | ||||||
config.experimental.optimizeCss && !dev | ||||||
), | ||||||
'process.env.__NEXT_SCRIPT_WORKERS': JSON.stringify( | ||||||
config.experimental.nextScriptWorkers && !dev | ||||||
), | ||||||
'process.env.__NEXT_SCROLL_RESTORATION': JSON.stringify( | ||||||
config.experimental.scrollRestoration | ||||||
), | ||||||
'process.env.__NEXT_IMAGE_OPTS': JSON.stringify({ | ||||||
deviceSizes: config.images.deviceSizes, | ||||||
imageSizes: config.images.imageSizes, | ||||||
path: config.images.path, | ||||||
loader: config.images.loader, | ||||||
dangerouslyAllowSVG: config.images.dangerouslyAllowSVG, | ||||||
unoptimized: config?.images?.unoptimized, | ||||||
...(dev | ||||||
? { | ||||||
// pass domains in development to allow validating on the client | ||||||
domains: config.images.domains, | ||||||
remotePatterns: config.images?.remotePatterns, | ||||||
output: config.output, | ||||||
} | ||||||
: {}), | ||||||
}), | ||||||
'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify(config.basePath), | ||||||
'process.env.__NEXT_STRICT_NEXT_HEAD': JSON.stringify( | ||||||
config.experimental.strictNextHead | ||||||
), | ||||||
'process.env.__NEXT_HAS_REWRITES': JSON.stringify(hasRewrites), | ||||||
'process.env.__NEXT_CONFIG_OUTPUT': JSON.stringify(config.output), | ||||||
'process.env.__NEXT_I18N_SUPPORT': JSON.stringify(!!config.i18n), | ||||||
'process.env.__NEXT_I18N_DOMAINS': JSON.stringify(config.i18n?.domains), | ||||||
'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(config.analyticsId), // TODO: remove in the next major version | ||||||
'process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE': JSON.stringify( | ||||||
config.skipMiddlewareUrlNormalize | ||||||
), | ||||||
'process.env.__NEXT_EXTERNAL_MIDDLEWARE_REWRITE_RESOLVE': JSON.stringify( | ||||||
config.experimental.externalMiddlewareRewritesResolve | ||||||
), | ||||||
'process.env.__NEXT_MANUAL_TRAILING_SLASH': JSON.stringify( | ||||||
config.skipTrailingSlashRedirect | ||||||
), | ||||||
'process.env.__NEXT_HAS_WEB_VITALS_ATTRIBUTION': JSON.stringify( | ||||||
config.reactStrictMode === null ? true : config.reactStrictMode, | ||||||
'process.env.__NEXT_OPTIMIZE_FONTS': !dev && config.optimizeFonts, | ||||||
'process.env.__NEXT_OPTIMIZE_CSS': config.experimental.optimizeCss && !dev, | ||||||
'process.env.__NEXT_SCRIPT_WORKERS': | ||||||
config.experimental.nextScriptWorkers && !dev, | ||||||
'process.env.__NEXT_SCROLL_RESTORATION': | ||||||
config.experimental.scrollRestoration, | ||||||
...getImageConfig(config, dev), | ||||||
'process.env.__NEXT_ROUTER_BASEPATH': config.basePath, | ||||||
'process.env.__NEXT_STRICT_NEXT_HEAD': config.experimental.strictNextHead, | ||||||
'process.env.__NEXT_HAS_REWRITES': hasRewrites, | ||||||
'process.env.__NEXT_CONFIG_OUTPUT': config.output, | ||||||
'process.env.__NEXT_I18N_SUPPORT': !!config.i18n, | ||||||
'process.env.__NEXT_I18N_DOMAINS': config.i18n?.domains, | ||||||
'process.env.__NEXT_ANALYTICS_ID': config.analyticsId, // TODO: remove in the next major version | ||||||
'process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE': | ||||||
config.skipMiddlewareUrlNormalize, | ||||||
'process.env.__NEXT_EXTERNAL_MIDDLEWARE_REWRITE_RESOLVE': | ||||||
config.experimental.externalMiddlewareRewritesResolve, | ||||||
'process.env.__NEXT_MANUAL_TRAILING_SLASH': | ||||||
config.skipTrailingSlashRedirect, | ||||||
'process.env.__NEXT_HAS_WEB_VITALS_ATTRIBUTION': | ||||||
config.experimental.webVitalsAttribution && | ||||||
config.experimental.webVitalsAttribution.length > 0 | ||||||
), | ||||||
'process.env.__NEXT_WEB_VITALS_ATTRIBUTION': JSON.stringify( | ||||||
config.experimental.webVitalsAttribution | ||||||
), | ||||||
'process.env.__NEXT_LINK_NO_TOUCH_START': JSON.stringify( | ||||||
config.experimental.linkNoTouchStart | ||||||
), | ||||||
'process.env.__NEXT_ASSET_PREFIX': JSON.stringify(config.assetPrefix), | ||||||
config.experimental.webVitalsAttribution.length > 0, | ||||||
'process.env.__NEXT_WEB_VITALS_ATTRIBUTION': | ||||||
config.experimental.webVitalsAttribution, | ||||||
'process.env.__NEXT_LINK_NO_TOUCH_START': | ||||||
config.experimental.linkNoTouchStart, | ||||||
'process.env.__NEXT_ASSET_PREFIX': config.assetPrefix, | ||||||
...(isNodeOrEdgeCompilation | ||||||
? { | ||||||
// Fix bad-actors in the npm ecosystem (e.g. `node-formidable`) | ||||||
// This is typically found in unmaintained modules from the | ||||||
// pre-webpack era (common in server-side code) | ||||||
'global.GENTLY': JSON.stringify(false), | ||||||
'global.GENTLY': false, | ||||||
} | ||||||
: undefined), | ||||||
...(isNodeOrEdgeCompilation | ||||||
? { | ||||||
'process.env.__NEXT_EXPERIMENTAL_REACT': JSON.stringify( | ||||||
needsExperimentalReact(config) | ||||||
), | ||||||
'process.env.__NEXT_EXPERIMENTAL_REACT': | ||||||
needsExperimentalReact(config), | ||||||
} | ||||||
: undefined), | ||||||
} | ||||||
return serializeDefineEnv(defineEnv) | ||||||
} | ||||||
|
||||||
export function getDefineEnvPlugin(options: DefineEnvPluginOptions) { | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,9 +20,11 @@ export type NextConfigComplete = Required<NextConfig> & { | |
configFileName: string | ||
} | ||
|
||
export type I18NDomains = DomainLocale[] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably no need for this type. You can just re-export There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer having this explicit type instead of re-exporting DomainLocale as then you end up with scattered There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, but this makes the type a bit less reusable. The name probably could have been kept as |
||
|
||
export interface I18NConfig { | ||
defaultLocale: string | ||
domains?: DomainLocale[] | ||
domains?: I18NDomains | ||
localeDetection?: false | ||
locales: string[] | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not import at the top?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export
is a method ofBloomFilter
, theoretically we could importBloomFilter
at the top but didn't want to make too many changes, kept it as-is from this line: https://github.com/vercel/next.js/pull/63128/files#diff-28c0329619b90836564a7769c43ff473dbd28e8e31b75e650624da4d06911763L22 just sharing the type more.