Skip to content

Introduce __NEXT_INVARIANTS__ for process-invariant config values#90270

Open
gnoff wants to merge 2 commits intojstory/refine-node-environment-on-entryfrom
jstory/next-invariants
Open

Introduce __NEXT_INVARIANTS__ for process-invariant config values#90270
gnoff wants to merge 2 commits intojstory/refine-node-environment-on-entryfrom
jstory/next-invariants

Conversation

@gnoff
Copy link
Contributor

@gnoff gnoff commented Feb 20, 2026

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__.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

@vercel
Copy link
Contributor

vercel bot commented Feb 20, 2026

Notifying the following users due to files changed in this PR based on this repo's notify modifiers:

@timneutkens, @ijjk, @shuding, @huozhi:

packages/next/src/server/config.ts

@gnoff gnoff changed the title Introduce __NEXT_INVARIANTS__ for process-level config values Introduce __NEXT_INVARIANTS__ for process-level config values Feb 20, 2026
@gnoff gnoff changed the title Introduce __NEXT_INVARIANTS__ for process-level config values Introduce __NEXT_INVARIANTS__ for process-invariant config values Feb 20, 2026
@gnoff gnoff force-pushed the jstory/next-invariants branch from 04998f3 to da64022 Compare February 20, 2026 21:48
@gnoff gnoff force-pushed the jstory/refine-node-environment-on-entry branch from 91d2e8c to e1d8291 Compare February 20, 2026 21:50
@gnoff gnoff force-pushed the jstory/next-invariants branch from da64022 to b783593 Compare February 20, 2026 21:50
@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 20, 2026

Stats from current PR

🟢 3 improvements

Metric Canary PR Change Trend
node_modules Size 474 MB 473 MB 🟢 314 kB (0%) ▁▁▁▁▁
Turbo Build Time 4.026s 3.687s 🟢 339ms (-8%) █▁▇▅▅
Turbo Build Time (cached) 4.154s 3.764s 🟢 390ms (-9%) █▁█▅▅
📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 455ms 456ms █▁█▂▃
Cold (Ready in log) 440ms 457ms █▁█▃▃
Cold (First Request) 1.245s 1.267s █▄█▄▄
Warm (Listen) 457ms 506ms █▁█▂▃
Warm (Ready in log) 448ms 463ms █▁█▃▃
Warm (First Request) 348ms 335ms █▁█▃▃
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 455ms 455ms ▁▁▁▁█
Cold (Ready in log) 439ms 456ms ▂▁▁▂█
Cold (First Request) 1.948s 1.986s ▁▁▁▃█
Warm (Listen) 455ms 455ms ▁▁▁▁█
Warm (Ready in log) 439ms 457ms ▂▁▁▂█
Warm (First Request) 1.962s 2.001s ▁▁▁▃█

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 4.026s 3.687s 🟢 339ms (-8%) █▁▇▅▅
Cached Build 4.154s 3.764s 🟢 390ms (-9%) █▁█▅▅
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.016s 14.049s ▁▁▁▄█
Cached Build 14.168s 14.129s ▁▁▁▄█
node_modules Size 474 MB 473 MB 🟢 314 kB (0%) ▁▁▁▁▁
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles: **399 kB** → **399 kB** ✅ -5 B

80 files with content-based hashes (individual files not comparable between builds)

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 768 B 763 B
Total 768 B 763 B ✅ -5 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 451 B 450 B
Total 451 B 450 B ✅ -1 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.48 kB N/A -
6280-HASH.js gzip 57.5 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.53 kB N/A -
e8aec2e4-HASH.js gzip 62.6 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 256 B 255 B
main-HASH.js gzip 39.1 kB 39.1 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.53 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.49 kB -
6948ada0-HASH.js gzip N/A 62.6 kB -
9544-HASH.js gzip N/A 58.3 kB -
Total 231 kB 232 kB ⚠️ +749 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.5 kB 2.5 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.97 kB 7.97 kB ✅ -2 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 125 kB
page.js gzip 253 kB 253 kB
Total 377 kB 377 kB ⚠️ +90 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 612 B 615 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 43.8 kB 43.6 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 45.4 kB 45.3 kB ✅ -159 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 715 B 718 B
Total 715 B 718 B ⚠️ +3 B
Build Cache
Canary PR Change
0.pack gzip 3.96 MB 3.96 MB 🔴 +8.21 kB (+0%)
index.pack gzip 103 kB 102 kB
index.pack.old gzip 102 kB 104 kB 🔴 +1.62 kB (+2%)
Total 4.16 MB 4.17 MB ⚠️ +9.27 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 318 kB 318 kB
app-page-exp..prod.js gzip 168 kB 168 kB
app-page-tur...dev.js gzip 317 kB 317 kB
app-page-tur..prod.js gzip 168 kB 168 kB
app-page-tur...dev.js gzip 314 kB 314 kB
app-page-tur..prod.js gzip 166 kB 166 kB
app-page.run...dev.js gzip 314 kB 314 kB
app-page.run..prod.js gzip 167 kB 166 kB
app-route-ex...dev.js gzip 70.7 kB 70.7 kB
app-route-ex..prod.js gzip 49.1 kB 49.1 kB
app-route-tu...dev.js gzip 70.7 kB 70.7 kB
app-route-tu..prod.js gzip 49.2 kB 49.2 kB
app-route-tu...dev.js gzip 70.3 kB 70.3 kB
app-route-tu..prod.js gzip 48.9 kB 48.9 kB
app-route.ru...dev.js gzip 70.2 kB 70.2 kB
app-route.ru..prod.js gzip 48.9 kB 48.9 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.2 kB 43.2 kB
pages-api-tu..prod.js gzip 32.9 kB 32.9 kB
pages-api.ru...dev.js gzip 43.2 kB 43.2 kB
pages-api.ru..prod.js gzip 32.8 kB 32.8 kB
pages-turbo....dev.js gzip 52.5 kB 52.5 kB
pages-turbo...prod.js gzip 38.5 kB 38.5 kB
pages.runtim...dev.js gzip 52.5 kB 52.5 kB
pages.runtim..prod.js gzip 38.4 kB 38.4 kB
server.runti..prod.js gzip 62 kB 57.1 kB 🟢 4.91 kB (-8%)
Total 2.81 MB 2.8 MB ✅ -5.04 kB
📝 Changed Files (9 files)

Files with changes:

  • app-page-exp..ntime.dev.js
  • app-page-exp..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page.runtime.dev.js
  • app-page.runtime.prod.js
  • server.runtime.prod.js
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-exp..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js
failed to diff
app-page.runtime.dev.js
failed to diff
app-page.runtime.prod.js

Diff too large to display

server.runtime.prod.js

Diff too large to display

@gnoff gnoff force-pushed the jstory/refine-node-environment-on-entry branch 2 times, most recently from 8cf7234 to 3bc208e Compare February 21, 2026 17:05
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.
@gnoff gnoff force-pushed the jstory/refine-node-environment-on-entry branch from 3bc208e to d75ddba Compare February 21, 2026 17:35
@gnoff gnoff force-pushed the jstory/next-invariants branch from b783593 to d16ea80 Compare February 21, 2026 17:42
@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 21, 2026

Failing test suites

Commit: 84c944c | About building and testing Next.js

pnpm test-start test/e2e/app-dir/next-invariants/next-invariants.test.ts (job)

  • next-invariants > should have a client component for every invariant key and no extras (DD)
  • next-invariants > should SSR the replaced values in client components (DD)
  • next-invariants > should SSR the replaced values in server components (DD)
  • next-invariants > should hydrate client components in the browser (DD)
  • next-invariants > should read invariants from the runtime global in external packages (DD)
  • next-invariants > should not have any NEXT_INVARIANTS references in client bundles (DD)
  • next-invariants > should not have any NEXT_INVARIANTS references in server bundles (DD)
Expand output

● next-invariants › should have a client component for every invariant key and no extras

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● next-invariants › should SSR the replaced values in client components

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● next-invariants › should SSR the replaced values in server components

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● next-invariants › should hydrate client components in the browser

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● next-invariants › should read invariants from the runtime global in external packages

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● next-invariants › should not have any NEXT_INVARIANTS references in client bundles

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

● next-invariants › should not have any NEXT_INVARIANTS references in server bundles

next build failed with code/signal 1

  82 |             if (code || signal)
  83 |               reject(
> 84 |                 new Error(
     |                 ^
  85 |                   `next build failed with code/signal ${code || signal}`
  86 |                 )
  87 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:84:17)

pnpm test test/integration/telemetry/test/index.test.ts (job)

  • Telemetry CLI > production mode > cli session: babel tooling config (DD)
  • Telemetry CLI > production mode > cli session: custom babel config (plugin) (DD)
  • Telemetry CLI > production mode > cli session: package.json custom babel config (plugin) (DD)
  • Telemetry CLI > production mode > cli session: custom babel config (preset) (DD)
  • Telemetry CLI > production mode > cli session: next config with webpack (DD)
  • Telemetry CLI > production mode > detect static 404 correctly for next build (DD)
  • Telemetry CLI > production mode > detect page counts correctly for next build (DD)
Expand output

● Telemetry CLI › production mode › cli session: babel tooling config

thrown: "Exceeded timeout of 60000 ms for a test.
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."

  188 |       })
  189 |
> 190 |       it('cli session: babel tooling config', async () => {
      |       ^
  191 |         await fs.rename(
  192 |           path.join(appDir, '.babelrc.default'),
  193 |           path.join(appDir, '.babelrc')

  at it (integration/telemetry/test/index.test.ts:190:7)
  at integration/telemetry/test/index.test.ts:98:56
  at Object.describe (integration/telemetry/test/index.test.ts:13:1)

● Telemetry CLI › production mode › cli session: custom babel config (plugin)

TypeError: Cannot read properties of null (reading 'pop')

  231 |
  232 |         const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/
> 233 |           .exec(stderr)
      |                 ^
  234 |           .pop()
  235 |
  236 |         expect(event).toMatch(/"hasNextConfig": false/)

  at Object.stderr (integration/telemetry/test/index.test.ts:233:17)

● Telemetry CLI › production mode › cli session: package.json custom babel config (plugin)

TypeError: Cannot read properties of null (reading 'pop')

  257 |
  258 |         const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/
> 259 |           .exec(stderr)
      |                 ^
  260 |           .pop()
  261 |
  262 |         expect(event).toMatch(/"hasNextConfig": false/)

  at Object.stderr (integration/telemetry/test/index.test.ts:259:17)

● Telemetry CLI › production mode › cli session: custom babel config (preset)

TypeError: Cannot read properties of null (reading 'pop')

  283 |
  284 |         const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/
> 285 |           .exec(stderr)
      |                 ^
  286 |           .pop()
  287 |
  288 |         expect(event).toMatch(/"hasNextConfig": false/)

  at Object.stderr (integration/telemetry/test/index.test.ts:285:17)

● Telemetry CLI › production mode › cli session: next config with webpack

TypeError: Cannot read properties of null (reading 'pop')

  309 |
  310 |         const event = /NEXT_CLI_SESSION_STARTED[\s\S]+?{([\s\S]+?)}/
> 311 |           .exec(stderr)
      |                 ^
  312 |           .pop()
  313 |
  314 |         expect(event).toMatch(/"hasNextConfig": true/)

  at Object.stderr (integration/telemetry/test/index.test.ts:311:17)

● Telemetry CLI › production mode › detect static 404 correctly for next build

TypeError: Cannot read properties of null (reading 'pop')

  342 |
  343 |         const event1 = /NEXT_BUILD_OPTIMIZED[\s\S]+?{([\s\S]+?)}/
> 344 |           .exec(stderr)
      |                 ^
  345 |           .pop()
  346 |         expect(event1).toMatch(/hasStatic404.*?true/)
  347 |       })

  at Object.stderr (integration/telemetry/test/index.test.ts:344:17)

● Telemetry CLI › production mode › detect page counts correctly for next build

TypeError: Cannot read properties of null (reading 'pop')

  354 |
  355 |         const event1 = /NEXT_BUILD_OPTIMIZED[\s\S]+?{([\s\S]+?)}/
> 356 |           .exec(stderr)
      |                 ^
  357 |           .pop()
  358 |         expect(event1).toMatch(/"staticPropsPageCount": 2/)
  359 |         expect(event1).toMatch(/"serverPropsPageCount": 2/)

  at Object.stderr (integration/telemetry/test/index.test.ts:356:17)

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
@gnoff gnoff force-pushed the jstory/next-invariants branch from d16ea80 to 84c944c Compare February 21, 2026 18:47
@gnoff gnoff force-pushed the jstory/refine-node-environment-on-entry branch 2 times, most recently from 74f6040 to a81ecd3 Compare February 22, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants