Skip to content
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

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 179 additions & 150 deletions packages/next/src/build/webpack/plugins/define-env-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
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'
import type { ImageConfigComplete } from '../../../shared/lib/image-config'

timneutkens marked this conversation as resolved.
Show resolved Hide resolved
function errorIfEnvConflicted(config: NextConfigComplete, key: string) {
const isPrivateKey = /^(?:NODE_.+)|^(?:__.+)$/i.test(key)
Expand All @@ -14,16 +18,16 @@ function errorIfEnvConflicted(config: NextConfigComplete, key: string) {
}
}

type BloomFilter = ReturnType<
import('../../../shared/lib/bloom-filter').BloomFilter['export']
>
Copy link
Member

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export is a method of BloomFilter, theoretically we could import BloomFilter 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.


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
Expand All @@ -38,6 +42,94 @@ export interface DefineEnvPluginOptions {
previewModeId: string | undefined
}

interface DefineEnv {
[key: string]:
| string
| string[]
| boolean
| undefined
| MiddlewareMatcher[]
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
| BloomFilter
| Partial<ImageConfigComplete>
| 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]
if (value) {
defineEnvStringified[key] = JSON.stringify(value)
}
}

return defineEnvStringified
}

function getImageConfig(
config: NextConfigComplete,
dev: boolean
): { 'process.env.__NEXT_IMAGE_OPTS': Partial<ImageConfigComplete> } {
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,
Expand All @@ -53,180 +145,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) {
Expand Down
Loading
Loading