diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 42c54b6c6bb8..9fef923b17a4 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -1013,134 +1013,6 @@ jobs: stepName: 'test-cache-components-prod-${{ matrix.group }}' secrets: inherit - test-node-streams-cache-components-dev: - name: test node streams cache components dev - needs: - [ - 'optimize-ci', - 'changes', - 'build-native', - 'build-next', - 'fetch-test-timings', - ] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/6, 2/6, 3/6, 4/6, 5/6, 6/6] - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: | - export __NEXT_USE_NODE_STREAMS=true - export __NEXT_CACHE_COMPONENTS=true - export __NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS=true - export __NEXT_EXPERIMENTAL_APP_NEW_SCROLL_HANDLER=true - export NEXT_EXTERNAL_TESTS_FILTERS="test/cache-components-tests-manifest.json" - export NEXT_TEST_MODE=dev - export IS_TURBOPACK_TEST=1 - export TURBOPACK_DEV=1 - - node run-tests.js \ - --timings \ - --require-timings \ - -g ${{ matrix.group }} \ - --type development - testTimingsArtifact: 'test-timings' - stepName: 'test-node-streams-cache-components-dev-${{ matrix.group }}' - secrets: inherit - - test-node-streams-cache-components-prod: - name: test node streams cache components prod - needs: - [ - 'optimize-ci', - 'changes', - 'build-native', - 'build-next', - 'fetch-test-timings', - ] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/7, 2/7, 3/7, 4/7, 5/7, 6/7, 7/7] - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: | - export __NEXT_USE_NODE_STREAMS=true - export __NEXT_CACHE_COMPONENTS=true - export __NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS=true - export __NEXT_EXPERIMENTAL_APP_NEW_SCROLL_HANDLER=true - export NEXT_EXTERNAL_TESTS_FILTERS="test/cache-components-tests-manifest.json" - export NEXT_TEST_MODE=start - export IS_TURBOPACK_TEST=1 - export TURBOPACK_BUILD=1 - - node run-tests.js \ - --timings \ - --require-timings \ - -g ${{ matrix.group }} \ - --type production - testTimingsArtifact: 'test-timings' - stepName: 'test-node-streams-cache-components-prod-${{ matrix.group }}' - secrets: inherit - - test-node-streams-dev: - name: test node streams dev - needs: ['optimize-ci', 'changes', 'build-native', 'build-next'] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/6, 2/6, 3/6, 4/6, 5/6, 6/6] - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: | - export __NEXT_USE_NODE_STREAMS=true - export NEXT_EXTERNAL_TESTS_FILTERS="test/use-node-streams-tests-manifest.json" - export NEXT_TEST_MODE=dev - export IS_TURBOPACK_TEST=1 - export TURBOPACK_DEV=1 - export __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true - export RUST_BACKTRACE=1 - - node run-tests.js \ - --timings \ - -g ${{ matrix.group }} \ - --type development - stepName: 'test-node-streams-dev-${{ matrix.group }}' - secrets: inherit - - test-node-streams-prod: - name: test node streams prod - needs: ['optimize-ci', 'changes', 'build-native', 'build-next'] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/7, 2/7, 3/7, 4/7, 5/7, 6/7, 7/7] - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: | - export __NEXT_USE_NODE_STREAMS=true - export NEXT_EXTERNAL_TESTS_FILTERS="test/use-node-streams-tests-manifest.json" - export NEXT_TEST_MODE=start - export IS_TURBOPACK_TEST=1 - export TURBOPACK_BUILD=1 - export __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true - export RUST_BACKTRACE=1 - - node run-tests.js \ - --timings \ - -g ${{ matrix.group }} \ - --type production - stepName: 'test-node-streams-prod-${{ matrix.group }}' - secrets: inherit - tests-pass: needs: [ @@ -1157,10 +1029,6 @@ jobs: 'test-firefox-safari', 'test-cache-components-dev', 'test-cache-components-prod', - 'test-node-streams-cache-components-dev', - 'test-node-streams-cache-components-prod', - 'test-node-streams-dev', - 'test-node-streams-prod', 'test-cargo-unit', 'rust-check', 'rustdoc-check', diff --git a/packages/next-env/index.ts b/packages/next-env/index.ts index 392670c7817e..5b34e68be666 100644 --- a/packages/next-env/index.ts +++ b/packages/next-env/index.ts @@ -18,7 +18,17 @@ let cachedLoadedEnvFiles: LoadedEnvFiles = [] let previousLoadedEnvFiles: LoadedEnvFiles = [] export function updateInitialEnv(newEnv: Env) { - Object.assign(initialEnv || {}, newEnv) + if (!initialEnv) { + return + } + + for (const [key, value] of Object.entries(newEnv)) { + if (value === undefined) { + delete initialEnv[key] + } else { + initialEnv[key] = value + } + } } type Log = { diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 8da5f855611e..fe771f1b5ff3 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -2040,6 +2040,7 @@ export const defaultConfig = Object.freeze({ gestureTransition: false, inlineCss: false, useCache: undefined, + useNodeStreams: true, slowModuleDetection: undefined, globalNotFound: false, browserDebugInfoInTerminal: 'warn', diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index c61f17984c65..937c481487eb 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1620,9 +1620,28 @@ function finalizeConfig(config: NextConfigComplete): NextConfigComplete { validationLevel: config.experimental.instantInsights?.validationLevel ?? 'manual-warning', } + syncUseNodeStreamsEnv(config) return config } +function syncUseNodeStreamsEnv(config: NextConfig): void { + // This must use resolved config: user configs are inspected before defaults + // are merged, while runtime bundles must select the default implementation. + const useNodeStreams = config.experimental?.useNodeStreams + ? 'true' + : undefined + + if (useNodeStreams) { + process.env.__NEXT_USE_NODE_STREAMS = useNodeStreams + } else { + delete process.env.__NEXT_USE_NODE_STREAMS + } + + // Dev env reloads restore process.env from this snapshot. Preserve the + // resolved runtime selection so a reload cannot mix stream implementations. + updateInitialEnv({ __NEXT_USE_NODE_STREAMS: useNodeStreams }) +} + async function applyModifyConfig( config: NextConfigComplete, phase: PHASE_TYPE, @@ -1750,6 +1769,7 @@ export default async function loadConfig( return cachedResult.rawConfig } + syncUseNodeStreamsEnv(cachedResult.config) return cachedResult.config } else { // Reset next.config errors before loading config @@ -1777,6 +1797,8 @@ export default async function loadConfig( process.env.__NEXT_PRIVATE_STANDALONE_CONFIG ) + syncUseNodeStreamsEnv(standaloneConfig) + // Cache the standalone config configCache.set(cacheKey, { config: standaloneConfig, @@ -2229,16 +2251,6 @@ function enforceExperimentalFeatures( config.experimental.useNodeStreams = true } - // Keep runtime bundle selection env in sync with the resolved config. - // Explicit user config (e.g. useNodeStreams: false) should win over an - // inherited shell env var to avoid selecting nodestream runtime bundles - // while define-env compiled user bundles with node streams disabled. - if (config.experimental.useNodeStreams) { - process.env.__NEXT_USE_NODE_STREAMS = 'true' - } else { - delete process.env.__NEXT_USE_NODE_STREAMS - } - // TODO: Remove this once strictRouteTypes is the default. if ( process.env.__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES === 'true' && diff --git a/test/unit/preserve-process-env.test.ts b/test/unit/preserve-process-env.test.ts index cbddc233040e..174ea9f764d2 100644 --- a/test/unit/preserve-process-env.test.ts +++ b/test/unit/preserve-process-env.test.ts @@ -1,4 +1,8 @@ -import { loadEnvConfig } from '../../packages/next-env/' +import { + loadEnvConfig, + resetEnv, + updateInitialEnv, +} from '../../packages/next-env/' describe('preserve process env', () => { it('should not reassign `process.env`', () => { @@ -6,4 +10,20 @@ describe('preserve process env', () => { loadEnvConfig('.') expect(Object.is(originalProcessEnv, process.env)).toBeTrue() }) + + it('should remove values unset in the initial env snapshot', () => { + const key = '__NEXT_TEST_UNSET_INITIAL_ENV' + + try { + loadEnvConfig('.') + process.env[key] = 'changed' + updateInitialEnv({ [key]: undefined }) + + resetEnv() + + expect(process.env[key]).toBeUndefined() + } finally { + delete process.env[key] + } + }) }) diff --git a/test/use-node-streams-tests-manifest.json b/test/use-node-streams-tests-manifest.json deleted file mode 100644 index 46c1c103b37e..000000000000 --- a/test/use-node-streams-tests-manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 2, - "suites": {}, - "rules": { - "include": [ - "test/e2e/**/*.test.{t,j}s{,x}", - "test/production/app-*/**/*.test.{t,j}s{,x}", - "test/development/app-*/**/*.test.{t,j}s{,x}", - "test/development/acceptance-app/**/*.test.{t,j}s{,x}" - ], - "exclude": [] - } -}