mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
### What? Defaults `experimental.useNodeStreams` to `true` while keeping the experimental config flag and explicit `false` opt-out available. Removes redundant dedicated node-stream CI jobs because the ordinary path exercises Node streams after this change. Depends on the standalone test compatibility update in #94347, which intentionally remains outside the Node streams PR stack. ### Why? Node streams can be exercised through the standard Node.js App Router test path once they are the default, allowing a smaller rollout before the larger follow-up that removes the flag and obsolete plumbing. CI exposed one necessary compatibility fix for this intermediate state: projects with a `next.config.*` received the new default after raw experimental-feature processing, leaving the runtime stream selector on the web implementation while compiled app code selected Node stream helpers. ### How? - Set `defaultConfig.experimental.useNodeStreams` to `true`. - Synchronize `__NEXT_USE_NODE_STREAMS` from the fully resolved config, including cached and standalone resolved configs, so defaults and adapter modifications are reflected at runtime. - Preserve the current public config/schema and explicit `experimental: { useNodeStreams: false }` opt-out; edge bundles continue to define the stream flag as `false`. - Remove the dedicated node-stream test jobs and now-unused manifest; the affected streaming assertion compatibility changes are isolated in #94347. ### Verification Run before extracting the prerequisite assertion changes into #94347: - `pnpm --filter=next types` - `pnpm --filter=next build` - `IS_WEBPACK_TEST=1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm test-start-webpack test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts` - `IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm test-start-turbo test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts` - `IS_WEBPACK_TEST=1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm test-start-webpack test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts` - `pnpm test-dev-webpack test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts` - `pnpm test-dev-turbo test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts` - `pnpm test-start-webpack test/production/app-dir/use-node-streams-env-precedence/use-node-streams-env-precedence.test.ts` - `pnpm test-start-webpack test/e2e/app-dir/app/standalone.test.ts` <!-- NEXT_JS_LLM_PR -->
191 lines
5.0 KiB
TypeScript
191 lines
5.0 KiB
TypeScript
/* eslint-disable import/no-extraneous-dependencies */
|
|
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
import * as dotenv from 'dotenv'
|
|
import { expand as dotenvExpand } from 'dotenv-expand'
|
|
|
|
export type Env = { [key: string]: string | undefined }
|
|
export type LoadedEnvFiles = Array<{
|
|
path: string
|
|
contents: string
|
|
env: Env
|
|
}>
|
|
|
|
export let initialEnv: Env | undefined = undefined
|
|
let combinedEnv: Env | undefined = undefined
|
|
let parsedEnv: Env | undefined = undefined
|
|
let cachedLoadedEnvFiles: LoadedEnvFiles = []
|
|
let previousLoadedEnvFiles: LoadedEnvFiles = []
|
|
|
|
export function updateInitialEnv(newEnv: Env) {
|
|
if (!initialEnv) {
|
|
return
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(newEnv)) {
|
|
if (value === undefined) {
|
|
delete initialEnv[key]
|
|
} else {
|
|
initialEnv[key] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
type Log = {
|
|
info: (...args: any[]) => void
|
|
error: (...args: any[]) => void
|
|
}
|
|
|
|
function replaceProcessEnv(sourceEnv: Env) {
|
|
Object.keys(process.env).forEach((key) => {
|
|
// Allow mutating internal Next.js env variables after the server has initiated.
|
|
// This is necessary for dynamic things like the IPC server port.
|
|
if (!key.startsWith('__NEXT_PRIVATE')) {
|
|
if (sourceEnv[key] === undefined || sourceEnv[key] === '') {
|
|
delete process.env[key]
|
|
}
|
|
}
|
|
})
|
|
|
|
Object.entries(sourceEnv).forEach(([key, value]) => {
|
|
process.env[key] = value
|
|
})
|
|
}
|
|
|
|
export function processEnv(
|
|
loadedEnvFiles: LoadedEnvFiles,
|
|
dir?: string,
|
|
log: Log = console,
|
|
forceReload = false,
|
|
onReload?: (envFilePath: string) => void
|
|
) {
|
|
if (!initialEnv) {
|
|
initialEnv = Object.assign({}, process.env)
|
|
}
|
|
// only reload env when forceReload is specified
|
|
if (
|
|
!forceReload &&
|
|
(process.env.__NEXT_PROCESSED_ENV || loadedEnvFiles.length === 0)
|
|
) {
|
|
return [process.env as Env]
|
|
}
|
|
// flag that we processed the environment values already.
|
|
process.env.__NEXT_PROCESSED_ENV = 'true'
|
|
|
|
const origEnv = Object.assign({}, initialEnv)
|
|
const parsed: dotenv.DotenvParseOutput = {}
|
|
|
|
for (const envFile of loadedEnvFiles) {
|
|
try {
|
|
let result: dotenv.DotenvConfigOutput = {}
|
|
result.parsed = dotenv.parse(envFile.contents)
|
|
|
|
result = dotenvExpand(result)
|
|
|
|
if (
|
|
result.parsed &&
|
|
!previousLoadedEnvFiles.some(
|
|
(item) =>
|
|
item.contents === envFile.contents && item.path === envFile.path
|
|
)
|
|
) {
|
|
onReload?.(envFile.path)
|
|
}
|
|
|
|
for (const key of Object.keys(result.parsed || {})) {
|
|
if (
|
|
typeof parsed[key] === 'undefined' &&
|
|
typeof origEnv[key] === 'undefined'
|
|
) {
|
|
// We're being imprecise in the type system - assume parsed[key] can be undefined
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
|
parsed[key] = result.parsed?.[key]!
|
|
}
|
|
}
|
|
|
|
// Add the parsed env to the loadedEnvFiles
|
|
envFile.env = result.parsed || {}
|
|
} catch (err) {
|
|
log.error(
|
|
`Failed to load env from ${path.join(dir || '', envFile.path)}`,
|
|
err
|
|
)
|
|
}
|
|
}
|
|
return [Object.assign(process.env, parsed), parsed]
|
|
}
|
|
|
|
export function resetEnv() {
|
|
if (initialEnv) {
|
|
replaceProcessEnv(initialEnv)
|
|
}
|
|
}
|
|
|
|
export function loadEnvConfig(
|
|
dir: string,
|
|
dev?: boolean,
|
|
log: Log = console,
|
|
forceReload = false,
|
|
onReload?: (envFilePath: string) => void
|
|
): {
|
|
combinedEnv: Env
|
|
parsedEnv: Env | undefined
|
|
loadedEnvFiles: LoadedEnvFiles
|
|
} {
|
|
if (!initialEnv) {
|
|
initialEnv = Object.assign({}, process.env)
|
|
}
|
|
// only reload env when forceReload is specified
|
|
if (combinedEnv && !forceReload) {
|
|
return { combinedEnv, parsedEnv, loadedEnvFiles: cachedLoadedEnvFiles }
|
|
}
|
|
replaceProcessEnv(initialEnv)
|
|
previousLoadedEnvFiles = cachedLoadedEnvFiles
|
|
cachedLoadedEnvFiles = []
|
|
|
|
const isTest = process.env.NODE_ENV === 'test'
|
|
const mode = isTest ? 'test' : dev ? 'development' : 'production'
|
|
const dotenvFiles = [
|
|
`.env.${mode}.local`,
|
|
// Don't include `.env.local` for `test` environment
|
|
// since normally you expect tests to produce the same
|
|
// results for everyone
|
|
mode !== 'test' && `.env.local`,
|
|
`.env.${mode}`,
|
|
'.env',
|
|
].filter(Boolean) as string[]
|
|
|
|
for (const envFile of dotenvFiles) {
|
|
// only load .env if the user provided has an env config file
|
|
const dotEnvPath = path.join(dir, envFile)
|
|
|
|
try {
|
|
const stats = fs.statSync(dotEnvPath)
|
|
|
|
// make sure to only attempt to read files or named pipes
|
|
if (!stats.isFile() && !stats.isFIFO()) {
|
|
continue
|
|
}
|
|
|
|
const contents = fs.readFileSync(dotEnvPath, 'utf8')
|
|
cachedLoadedEnvFiles.push({
|
|
path: envFile,
|
|
contents,
|
|
env: {}, // This will be populated in processEnv
|
|
})
|
|
} catch (err: any) {
|
|
if (err.code !== 'ENOENT') {
|
|
log.error(`Failed to load env from ${envFile}`, err)
|
|
}
|
|
}
|
|
}
|
|
;[combinedEnv, parsedEnv] = processEnv(
|
|
cachedLoadedEnvFiles,
|
|
dir,
|
|
log,
|
|
forceReload,
|
|
onReload
|
|
)
|
|
return { combinedEnv, parsedEnv, loadedEnvFiles: cachedLoadedEnvFiles }
|
|
}
|