Introduce __NEXT_INVARIANTS__ for process-invariant config values#90270
Introduce __NEXT_INVARIANTS__ for process-invariant config values#90270gnoff wants to merge 2 commits intojstory/refine-node-environment-on-entryfrom
__NEXT_INVARIANTS__ for process-invariant config values#90270Conversation
|
Notifying the following users due to files changed in this PR based on this repo's notify modifiers: @timneutkens, @ijjk, @shuding, @huozhi: |
__NEXT_INVARIANTS__ for process-level config values
__NEXT_INVARIANTS__ for process-level config values__NEXT_INVARIANTS__ for process-invariant config values
04998f3 to
da64022
Compare
91d2e8c to
e1d8291
Compare
da64022 to
b783593
Compare
Stats from current PR🟢 3 improvements
📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles: **399 kB** → **399 kB** ✅ -5 B80 files with content-based hashes (individual files not comparable between builds) Server Middleware
Build DetailsBuild Manifests
📦 WebpackClient Main Bundles
Polyfills
Pages
Server Edge SSR
Middleware
Build DetailsBuild Manifests
Build Cache
🔄 Shared (bundler-independent)Runtimes
📝 Changed Files (9 files)Files with changes:
View diffsapp-page-exp..ntime.dev.jsfailed to diffapp-page-exp..time.prod.jsDiff too large to display app-page-tur..ntime.dev.jsfailed to diffapp-page-tur..time.prod.jsDiff too large to display app-page-tur..ntime.dev.jsfailed to diffapp-page-tur..time.prod.jsfailed to diffapp-page.runtime.dev.jsfailed to diffapp-page.runtime.prod.jsDiff too large to display server.runtime.prod.jsDiff too large to display |
8cf7234 to
3bc208e
Compare
Currently node-environment is imported inconsistently — some entry points load it, others rely on downstream modules to eventually import it. This is fragile because it means the environment extensions (AsyncLocalStorage polyfill, error formatting, console patching, etc.) aren't guaranteed to be set up before other code runs. Similarly, installGlobalBehaviors is called separately from config loading even though it always needs to happen whenever config is produced. This PR makes two changes: 1. Add node-environment to all process entry points The three CLI commands (next-build.ts, next-dev.ts, next-start.ts) and start-server.ts (the forked child process entry point for next dev) now import node-environment at the top, before any other imports. This guarantees environment extensions are available before config loading, server initialization, or any other code runs. The import is removed from next-server.ts because NextNodeServer is never a process entry point — it's always constructed after a CLI entry point or start-server.ts has already loaded the environment. Other process entry points (export/worker.ts, static-paths-worker.ts, router-server.ts) keep their existing imports since they're separate processes. 2. Move installGlobalBehaviors into config loading installGlobalBehaviors (which configures console dimming for aborted requests) was called from two places — NextNodeServer constructor and export/worker.ts — separately from where config was loaded. This separated cause (config is available) from effect (global behaviors are installed) and meant every new config consumer had to remember to call it. loadConfig is renamed to loadConfigFromFile and now calls installGlobalBehaviors internally after resolving config. A new loadConfigForBuildWorker function handles the export worker case where config arrives pre-serialized via IPC rather than being loaded from disk — it also calls installGlobalBehaviors internally. Both use lazy require() for installGlobalBehaviors rather than a static import because console-dim.external.tsx depends on AsyncLocalStorage being on globalThis, which requires node-environment-baseline.ts to have run first. Static importing it from config.ts would trigger the dependency chain at module load time, before the environment is set up in contexts like the Jest test process that import config.ts transitively. The direct installGlobalBehaviors calls are removed from NextNodeServer and export/worker.ts — it's now an internal detail of config loading that consumers don't need to think about.
3bc208e to
d75ddba
Compare
b783593 to
d16ea80
Compare
Failing test suitesCommit: 84c944c | About building and testing Next.js
Expand output● next-invariants › should have a client component for every invariant key and no extras ● next-invariants › should SSR the replaced values in client components ● next-invariants › should SSR the replaced values in server components ● next-invariants › should hydrate client components in the browser ● next-invariants › should read invariants from the runtime global in external packages ● next-invariants › should not have any NEXT_INVARIANTS references in client bundles ● next-invariants › should not have any NEXT_INVARIANTS references in server bundles
Expand output● Telemetry CLI › production mode › cli session: babel tooling config ● Telemetry CLI › production mode › cli session: custom babel config (plugin) ● Telemetry CLI › production mode › cli session: package.json custom babel config (plugin) ● Telemetry CLI › production mode › cli session: custom babel config (preset) ● Telemetry CLI › production mode › cli session: next config with webpack ● Telemetry CLI › production mode › detect static 404 correctly for ● Telemetry CLI › production mode › detect page counts correctly for |
Builds on the loadConfigFromFile/loadConfigForBuildWorker refactor to add __NEXT_INVARIANTS__ — a flat, frozen global object of pre-processed config values that never change after startup. This eliminates the pattern of threading values like trailingSlash through renderOpts → closures → function parameters when they're actually process-level constants. __NEXT_INVARIANTS__ works identically in all contexts without the author needing to think about which one they're in. In client and server bundles, __NEXT_INVARIANTS__.trailingSlash is statically replaced with the literal value by defineEnv at compile time, enabling dead code elimination. In external (non-bundled) server code, the bare identifier resolves to the real frozen object on globalThis at runtime. A Proxy sentinel is installed in node-environment-baseline.ts that throws if any property is accessed before config is resolved, catching ordering bugs early. Initialization happens inside loadConfigFromFile and loadConfigForBuildWorker — no consumer needs to call it manually. It's idempotent (first call wins) so redundant calls are harmless. The defineEnv entries are generated automatically by iterating Object.keys(__NEXT_INVARIANTS__). Adding a new invariant property to NextInvariants and initializeNextInvariants is sufficient — no manual defineEnv wiring needed. All values must be JSON-serializable. The NextInvariants interface has an index signature constraint that produces a compile error if someone adds a non-serializable property like RegExp or Map. The type declaration is in an internal-only src/next-invariants.d.ts included in the framework's tsconfig but not published to users via the next package types. Initial properties: isDevServer, trailingSlash, experimentalOptimisticRouting. As a first consumer, trailingSlash is migrated out of MetadataContext. resolve-url.ts reads __NEXT_INVARIANTS__.trailingSlash directly instead of receiving it through the closure chain, removing trailingSlash from MetadataContext, createMetadataContext, and the renderOpts dependency. Tests: Unit tests verify: Proxy sentinel throws on read/write before initialization, initialization produces correct frozen values, idempotency. E2e tests with non-default config (trailingSlash: true, experimental.optimisticRouting: true) verify: - Self-enforcing fixture parity — client component files in app/invariants/ must exactly match the keys of NextInvariants, so adding a new invariant without a test component fails - SSR and browser hydration render correct values in client components (proves client bundle defineEnv replacement) - SSR renders correct values in server components (proves server bundle defineEnv replacement) - An external package using destructuring reads correct values from the runtime global (proves the globalThis path works for non-bundled code) - Production bundle scan confirms no __NEXT_INVARIANTS__ string survives in any client or server JS file
d16ea80 to
84c944c
Compare
74f6040 to
a81ecd3
Compare
Stacked on #90269
Builds on the loadConfigFromFile/loadConfigForBuildWorker refactor to add
__NEXT_INVARIANTS__— a flat, frozen global object of pre-processed config values that never change after startup. This eliminates the pattern of threading values like trailingSlash through renderOpts → closures → function parameters when they're actually process-level constants.__NEXT_INVARIANTS__works identically in all contexts without the author needing to think about which one they're in. In client and server bundles,__NEXT_INVARIANTS__.trailingSlashis statically replaced with the literal value by defineEnv at compile time, enabling dead code elimination. In external (non-bundled) server code, the bare identifier resolves to the real frozen object on globalThis at runtime.A Proxy sentinel is installed in node-environment-baseline.ts that throws if any property is accessed before config is resolved, catching ordering bugs early.
Initialization happens inside loadConfigFromFile and loadConfigForBuildWorker — no consumer needs to call it manually. It's idempotent (first call wins) so redundant calls are harmless.
The defineEnv entries are generated automatically by iterating Object.keys(
__NEXT_INVARIANTS__). Adding a new invariant property to NextInvariants and initializeNextInvariants is sufficient — no manual defineEnv wiring needed.All values must be JSON-serializable. The NextInvariants interface has an index signature constraint that produces a compile error if someone adds a non-serializable property like RegExp or Map.
The type declaration is in an internal-only src/next-invariants.d.ts included in the framework's tsconfig but not published to users via the next package types.
Initial properties: isDevServer, trailingSlash, experimentalOptimisticRouting.
As a first consumer, trailingSlash is migrated out of MetadataContext. resolve-url.ts reads
__NEXT_INVARIANTS__.trailingSlashdirectly instead of receiving it through the closure chain, removing trailingSlash from MetadataContext, createMetadataContext, and the renderOpts dependency.Tests:
Unit tests verify: Proxy sentinel throws on read/write before initialization, initialization produces correct frozen values, idempotency.
E2e tests with non-default config (trailingSlash: true, experimental.optimisticRouting: true) verify:
__NEXT_INVARIANTS__string survives in any client or server JS file