Commit Graph

3 Commits

Author SHA1 Message Date
James Anderson 735d9dad77 fix(build): define process.env.NEXT_RUNTIME for server bundles (#1880)
* fix(build): define process.env.NEXT_RUNTIME for server bundles (#1365)

Next.js stamps each route bundle with a compile-time
`process.env.NEXT_RUNTIME` constant ('nodejs' or 'edge' depending on
the route's runtime setting). Vinext was not defining this constant,
so `process.env.NEXT_RUNTIME` resolved to `undefined` at runtime.

User-land code that reads `process.env.NEXT_RUNTIME` (including the
Next.js deploy test fixture `app/path-prefix.js`) would then produce
`'/undefined'` instead of `'/nodejs'`, causing `after()` callbacks
that call `revalidatePath()` to invalidate the wrong cache key. The
ISR page was never revalidated, and the timestamp-change assertion in
`next-after-app-deploy/index.test.ts` always failed.

Fix: inject `process.env.NEXT_RUNTIME = 'nodejs'` into all server
environments (rsc, ssr, custom worker envs) via the existing
`configEnvironment` hook. Client bundles are excluded by the existing
`if (name === "client") return null` guard. Vinext compiles a single
RSC bundle covering all routes, so 'nodejs' is the correct and only
applicable value — it matches both the Workers Node.js compatibility
layer and the `vinext start` Node.js HTTP server.

Fixes #1365

* Fix correct but breaks two tests

Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>

* fix(build): reconcile NEXT_RUNTIME tests and add client '' parity (#1365)

Addresses ask-bonk blocking review findings on #1880:

1. Restore `process.env.NEXT_RUNTIME = 'nodejs'` for server environments
   (removed by bot commit d2fbfb13 to dodge failing tests) and add
   `process.env.NEXT_RUNTIME = ''` to the top-level defines for the client
   bundle, matching Next.js define-env.ts parity.

2. Update the two broken tests to reflect the new correct behavior:
   - 'no-ops when defineServer not configured' → renamed and updated: the
     hook now always returns a define object (with at least NEXT_RUNTIME)
     even when no user defineServer entries are configured.
   - exact-match toEqual test: add 'process.env.NEXT_RUNTIME' to expected.

3. Add new test: 'injects process.env.NEXT_RUNTIME = nodejs for server envs
   and "" for client', covering both the top-level client '' value and the
   per-environment server 'nodejs' override.

4. Fix inaccurate comment: NEXT_RUNTIME is inlined at compile time; nothing
   sets it at runtime. The old comment claimed nodejs_compat/vinext start
   provide it at runtime, which is wrong.

5. Remove dead `if (Object.keys(serverDefines).length === 0) return null`
   early-return since serverDefines always contains at least NEXT_RUNTIME.

---------

Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
2026-06-09 15:19:20 +01:00
James Anderson 9596b72495 fix(pages-router): pass revalidateReason "on-demand" to gsp/gssp (#1462) (#1856)
* fix(pages-router): pass revalidateReason "on-demand" to getStaticProps (#1462)

Implements res.revalidate() for Pages Router API routes and wires the
on-demand revalidation trigger through to getStaticProps/getServerSideProps
context as revalidateReason: "on-demand".

res.revalidate(urlPath) issues an internal HEAD request to the target path
carrying the x-prerender-revalidate header (mirroring Next.js's api-resolver).
The Pages render path (dev-server and prod pages-page-data) detects that
header, bypasses the fresh/stale cache-hit short-circuits, and regenerates
the entry synchronously with reason "on-demand".

The build and stale reasons already worked; this finishes the on-demand case.

Closes #1462

* fix(pages-router): authenticate on-demand revalidation with a secret (#1462)

The on-demand `revalidateReason` detection treated `x-prerender-revalidate`
as an unauthenticated presence check and sent the literal value "1". Any
external client could send `x-prerender-revalidate: 1` to force synchronous
regeneration of any ISR page, bypassing the fresh/stale cache short-circuits
— a cache-stampede/DoS vector.

Mirror Next.js's `checkIsOnDemandRevalidate`: a request is only treated as
on-demand revalidation when the `x-prerender-revalidate` header value EQUALS a
secret (the vinext analog of `previewModeId`). Introduce a per-process
revalidate secret (`getRevalidateSecret`, 256-bit random hex via Web Crypto)
that `res.revalidate()` attaches to its internal loopback request, and a
constant-time `isOnDemandRevalidateRequest` equality check on the receiving
dev-server and Pages page handler. Header presence alone is never honored.

Also fix the test's HTML-reason extraction to avoid CodeQL's
incomplete-sanitization / bad-HTML-regex findings, and add a negative test
asserting forged `x-prerender-revalidate` values ("1", "", a guess) are
rejected while the real secret is accepted.

* refactor(pages-router): share res.revalidate() impl; support unstable_onlyGenerated (#1462)

Address non-blocking bonk review findings:
- Extract the two near-identical `res.revalidate()` implementations
  (`api-handler.ts`, `pages-node-compat.ts`) into a shared
  `performOnDemandRevalidate` helper so the secret wiring and success detection
  cannot drift between dev and prod.
- Align success detection with Next.js's api-resolver: accept
  `x-nextjs-cache: REVALIDATED`, status 200, or a 404 when the caller passed
  `unstable_onlyGenerated`. Add `unstable_onlyGenerated` support
  (`x-prerender-revalidate-if-generated` header).

* docs(pages-router): note x-nextjs-cache REVALIDATED branch is parity-only (#1462)

* fix(pages-router): make on-demand revalidate secret build-time (shared across isolates)

The on-demand ISR revalidation secret was a per-process random value. On
Cloudflare Workers, res.revalidate()'s loopback fetch() can land on a different
isolate than the sender, so a per-process secret mismatches across isolates and
false-rejects legitimate revalidations.

Mirror Next.js's previewModeId: generate one 256-bit secret at build time (in
the vinext build CLI, once per build, shared across plugin instances like
__VINEXT_SHARED_BUILD_ID) and bake it SERVER-ONLY into every server bundle via a
Vite define (__VINEXT_REVALIDATE_SECRET). It is byte-for-byte identical in every
isolate and never reaches the client bundle. getRevalidateSecret() reads the
baked constant at runtime and falls back to a process-shared random secret in
dev (single-process, no regression). Also persist revalidateSecret in
vinext-server.json for diagnostics/tooling parity with prerenderSecret.

Constant-time safeEqual validation and reject-arrays/empty/absent behavior are
unchanged.

Refs #1462

* test(pages-router): cover server-only revalidate secret; drop dead manifest reader

Address ask-bonk review of the build-time revalidate secret:

- Add tests pinning the security-critical invariant that the
  __VINEXT_REVALIDATE_SECRET define is baked into server environments (rsc/ssr)
  but NEVER into the client bundle (the client env returns null outright). A
  leak would ship the secret to every browser and re-open the cache-stampede
  vector the equality check prevents. Also harden the existing "no-ops when
  defineServer unset" test to explicitly clear the env var instead of relying on
  it happening to be unset.
- Drop the unused readRevalidateSecret export and the orphaned revalidateSecret
  manifest field — the runtime authenticates against the baked define (both
  Workers isolates and the Node prod-server run the bundle), so the on-disk copy
  had no consumer (knip-flagged dead code).
- Hoist the client early-return in vinext:compiler-define-server so the
  server-only guarantee is structural (removes the redundant name check), and
  reword the dev-fallback comments ("absent unless set during vinext build").

Refs #1462
2026-06-09 10:07:50 +01:00
James Anderson 34c8669182 fix(config): implement compiler.define / compiler.defineServer (#1638)
* feat(config): implement compiler.define and compiler.defineServer (#1496)

Forward `compiler.define` to Vite's top-level `define` (applies to both
client and server bundles) and `compiler.defineServer` to non-client
environments only via a `configEnvironment` hook, matching Next.js
Webpack `DefinePlugin` semantics. Values are JSON-stringified at config
resolution time so they slot directly into Vite's substitution format.

Refs Next.js: packages/next/src/build/define-env.ts.

Fixes #1496

* fix(config): reject compiler.define keys that collide with built-ins

Mirrors Next.js (`packages/next/src/build/define-env.ts`) — throw when a
user-provided `compiler.define` or `compiler.defineServer` key would
silently overwrite an internal define (e.g. `process.env.NODE_ENV`,
`__NEXT_*`, `__VINEXT_*`) or when `define` and `defineServer` define the
same key. Without this, a misconfigured project would produce subtly
wrong output instead of failing fast.

Addresses /bigbonk review on #1638.
2026-05-28 15:45:55 +01:00