Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7d81b2f
ci(native): produce the Pages handler corpus per Next.js release
soleil-colza Nov 2, 2026
0426465
ci(native): take the producer's pnpm from its fixture
soleil-colza Nov 2, 2026
8258ee8
Merge remote-tracking branch 'origin/chore/next-16.4.4-20261102' into…
soleil-colza Nov 2, 2026
347f125
feat(adapter): qualify the native Next.js sources of 16.4.4 beside 16…
soleil-colza Nov 2, 2026
ae47979
feat(adapter): fingerprint the Turbopack runtime of Next.js 16.4.4
soleil-colza Nov 2, 2026
f6c0738
feat: qualify fast-jwt, the JWT library of Next.js 16.4.4, for native…
soleil-colza Nov 2, 2026
3371056
ci(native): build the native fixtures on a chosen reviewed release
soleil-colza Nov 2, 2026
05c7de0
docs: describe native support for Next.js 16.4.3 and 16.4.4
soleil-colza Nov 2, 2026
87ff601
style: format the files the native release pins touched
soleil-colza Nov 2, 2026
ddfcad9
fix(adapter): hold the native Next.js package to its release's bytes
soleil-colza Nov 2, 2026
993f0c8
fix(adapter): read strict factory tables only where a runtime install…
soleil-colza Nov 3, 2026
cae9167
Merge remote-tracking branch 'origin/main' into feat/native-next-16-4…
soleil-colza Nov 3, 2026
1c790fd
fix(adapter): check only the tables a 16.4.4 runtime installs
soleil-colza Nov 3, 2026
eb69d44
fix(adapter): keep native Next.js release pins immutable
soleil-colza Nov 3, 2026
30695b2
feat(adapter): add Next.js 16.4.4 Pages handler references
soleil-colza Nov 3, 2026
915fdd1
ci(native): build the official fixtures on Next.js 16.4.3 and 16.4.4
soleil-colza Nov 3, 2026
e89b05e
docs: lift the pending Pages-only limitation on Next.js 16.4.4
soleil-colza Nov 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions .github/workflows/native-addon-functions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ jobs:
fail-fast: false
matrix:
node: [24.22.1, 26.13.0]
next: ['16.4.3', '16.4.4']
include:
- next: '16.4.3'
next-commit: 29bbeab09981148ade186e8599ecdede15abc281
- next: '16.4.4'
next-commit: b175f52b05a5f6c6c39ca72565ea178542be323b
env:
# The official fixtures build with this release; the SDK's own tests pin theirs.
UPWIND_NATIVE_NEXT: ${{ matrix.next }}
# Release-independent checks run once per Node.js version, in the newest release's leg.
RELEASE_INDEPENDENT: ${{ matrix.next == '16.4.4' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
Expand All @@ -34,47 +45,60 @@ jobs:
- name: Create an isolated artifact mount
run: sudo install -d -m 0777 /bundle
- name: Compile, package and invoke a real native addon
if: env.RELEASE_INDEPENDENT == 'true'
run: pnpm exec vitest run --project adapter packages/adapter/test/native-container-integration.test.ts
env:
UPWIND_NATIVE_FUNCTION_INTEGRATION: '1'
- name: Verify failure diagnostic filesystem boundaries
if: env.RELEASE_INDEPENDENT == 'true'
run: pnpm exec vitest run --project adapter packages/adapter/test/native-next-diagnostics.test.ts
- name: Verify the fixed Node dependency evaluation guard
if: env.RELEASE_INDEPENDENT == 'true'
run: pnpm exec vitest run --project adapter packages/adapter/test/node-jwt-guard-integration.test.ts
env:
UPWIND_NATIVE_FUNCTION_INTEGRATION: '1'
- name: Verify the fixed Node fast-jwt evaluation guard (Next.js 16.4.4)
if: env.RELEASE_INDEPENDENT == 'true'
run: pnpm exec vitest run --project adapter packages/adapter/test/node-fast-jwt-guard-integration.test.ts
env:
UPWIND_NATIVE_FUNCTION_INTEGRATION: '1'
- name: Verify the fixed WebSocket optional dependency guard
if: env.RELEASE_INDEPENDENT == 'true'
run: pnpm exec vitest run --project adapter packages/adapter/test/node-ws-guard-integration.test.ts
env:
UPWIND_NATIVE_FUNCTION_INTEGRATION: '1'
- name: Verify the fixed Turbopack selector specialization
if: env.RELEASE_INDEPENDENT == 'true'
run: pnpm exec vitest run --project adapter packages/adapter/test/node-turbopack-selector-integration.test.ts
env:
UPWIND_NATIVE_FUNCTION_INTEGRATION: '1'
- name: Verify complete original Node export closures
if: env.RELEASE_INDEPENDENT == 'true'
run: pnpm exec vitest run --project adapter packages/adapter/test/node-raw-closure-integration.test.ts
env:
UPWIND_NATIVE_FUNCTION_INTEGRATION: '1'
- name: Check out the fixed Next.js 16.4.3 native fixtures
- name: Check out the fixed Next.js ${{ matrix.next }} native fixtures
shell: bash
env:
NEXT_COMMIT: ${{ matrix.next-commit }}
run: |
set -euo pipefail
fixture_source="$RUNNER_TEMP/upwind-official-native-next"
git init --quiet "$fixture_source"
git -C "$fixture_source" remote add origin https://github.com/vercel/next.js.git
git -C "$fixture_source" sparse-checkout init --no-cone
git -C "$fixture_source" sparse-checkout set --no-cone '/test/e2e/prerender-native-module/' '/test/e2e/app-dir/turbopack-reports/'
git -C "$fixture_source" fetch --no-tags --depth=1 origin 29bbeab09981148ade186e8599ecdede15abc281
git -C "$fixture_source" fetch --no-tags --depth=1 origin "$NEXT_COMMIT"
git -C "$fixture_source" checkout --detach FETCH_HEAD
test "$(git -C "$fixture_source" rev-parse HEAD)" = 29bbeab09981148ade186e8599ecdede15abc281
test "$(git -C "$fixture_source" rev-parse HEAD)" = "$NEXT_COMMIT"
- name: Build and invoke the official native fixture and fallback path
id: official_native
run: node tools/native-addon-next-ci.mts "$RUNNER_TEMP/upwind-official-native-next/test/e2e/prerender-native-module"
- name: Retain bounded public fixture JavaScript after failure
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: native-next-compiled-js-${{ matrix.node }}-${{ github.run_id }}-${{ github.run_attempt }}
name: native-next-compiled-js-${{ matrix.node }}-next-${{ matrix.next }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/upwind-native-next-diagnostics
if-no-files-found: warn
retention-days: 1
Expand All @@ -92,7 +116,7 @@ jobs:
if: failure() && steps.official_native_diagnostic.outcome == 'failure'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: native-next-server-minification-off-js-${{ matrix.node }}-${{ github.run_id }}-${{ github.run_attempt }}
name: native-next-server-minification-off-js-${{ matrix.node }}-next-${{ matrix.next }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/upwind-native-next-server-minification-off/upwind-native-next-diagnostics
if-no-files-found: warn
retention-days: 1
Expand All @@ -110,12 +134,12 @@ jobs:
if: failure() && steps.official_native_app.outcome == 'failure'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: native-next-app-router-js-${{ matrix.node }}-${{ github.run_id }}-${{ github.run_attempt }}
name: native-next-app-router-js-${{ matrix.node }}-next-${{ matrix.next }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/upwind-native-next-app-router/upwind-native-next-diagnostics
if-no-files-found: warn
retention-days: 1
- name: Verify the fixed Next require hook bootstrap
if: always() && !cancelled() && steps.build.outcome == 'success'
if: always() && !cancelled() && steps.build.outcome == 'success' && env.RELEASE_INDEPENDENT == 'true'
run: pnpm exec vitest run --project runtime packages/runtime/test/node-next-hook-bootstrap-integration.test.ts
env:
UPWIND_NATIVE_FUNCTION_INTEGRATION: '1'
11 changes: 10 additions & 1 deletion .github/workflows/native-pages-handler-profile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,22 @@ jobs:
option: '--server-minification=off'
output: independent-pages-handler-profile-server-minification-off
artifact: UNQUALIFIED-public-pages-handler-server-minification-off
- server-minification: default
option: '--next=16.4.4'
output: independent-pages-handler-profile-next-16.4.4
artifact: UNQUALIFIED-public-pages-handler-next-16.4.4
- server-minification: off
option: '--next=16.4.4 --server-minification=off'
output: independent-pages-handler-profile-next-16.4.4-server-minification-off
artifact: UNQUALIFIED-public-pages-handler-next-16.4.4-server-minification-off
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The producer's own pnpm, from its fixture: the repository's `packageManager` names another.
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 12.15.0
package_json_file: tools/native-pages-handler-profile/fixture/package.json
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.22.1
Expand Down
41 changes: 26 additions & 15 deletions docs/native-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ a proved closure of them. A project with an `app/` directory gives every Functio
is in a Pages Router route. Only the native loads are proved, by the same loader classification,
binary selection, ABI and digests as for any Node Function, and a Cloudflare platform import in the
Function's own graph is refused. The shared-cache rewrites still apply; the instrumentation loader
and the custom-cache guard are rewritten only over their reviewed Next.js 16.4.3 bytes, and any
other bytes at those paths are refused. The Turbopack selectors are left to the bootstrap's
and the custom-cache guard are rewritten only over the reviewed bytes of the Next.js release the
package is, 16.4.3 or 16.4.4, and any other bytes at those paths are refused. The Turbopack selectors are left to the bootstrap's
`TURBOPACK`. Such an artifact does not have the manifest-consumer and closure proofs, the JWT,
WebSocket and environment initialization guards, or an alias proof for packages that only server
chunks load: a missing package link shows when the route runs, not at build. A distDir that would be
Expand All @@ -67,8 +67,14 @@ Node ABI and Linux x64 glibc target before publishing it. Worker loader hooks, p
and native shims are not applied to Node files. The existing shared cache and request runtime is
retained through a Node transport, rather than replaced with an independent `next start` cache.

The original Next.js manifest consumers and Turbopack runtime target Next.js 16.4.3. Changed
compiler or template shapes are refused until their source contracts are reviewed. A minified
The original Next.js manifest consumers and Turbopack runtime target Next.js 16.4.3 and 16.4.4. The
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
two publish the same reviewed sources but for the page template factory and the server runtimes,
which are pinned for each release (`node-next-release.ts`), and a package holding another release's
copy of one is refused. 16.4.4's Turbopack runtime installs no leading strict factory table, which
16.4.3's chunks may carry, so a chunk with one beside that runtime is refused. Each release has its
own reviewed getHandler references (`node-manifest-handler-catalog.ts`), with and without server
minification. Changed compiler or template shapes are refused until their source contracts are
reviewed. A minified
CommonJS Pages factory can retain its original sloppy arrow function when its physical module
mode and closed userland partition are proved alongside the protected template. This inspection
does not insert a strict directive or change the shipped JavaScript. The adapter leaves the
Expand Down Expand Up @@ -113,17 +119,21 @@ in the Node archive, so a runtime override cannot invoke a dependency omitted by
The default shared cache and rendering paths remain available. This policy does not replace a
missing dependency with a stub or allow unresolved application imports.

The fixed Next.js JWT dependency has a separate, source-reviewed initialization profile. It
permits only its exact global-recovery compiler site after proving the original package, closed
The fixed Next.js JWT dependency has a separate, source-reviewed initialization profile for each
release: Next.js 16.4.3 compiles in jsonwebtoken, and 16.4.4 compiles in fast-jwt. The jsonwebtoken
profile permits only its exact global-recovery compiler site. The fast-jwt profile permits no
compiler site and only one `process.binding("buffer")` read, in its copy of safer-buffer, behind a
`!o.kStringMaxLength` check. Node.js 24 and 26 define `buffer.kStringMaxLength`, so that read
never runs on them. Either is permitted only after proving the original package, closed
dependencies, CommonJS callers and completed route closure. The Node bootstrap checks the
original bytes and required builtin state immediately before CommonJS evaluation, including
after cache eviction. Changed sources, loader customization and unsupported compiler operations
remain publication errors. This profile requires the default Node.js 24.22.1 loader and does not
make arbitrary dynamic compilation portable. These restrictions apply to native Node artifacts;
ordinary Worker output keeps its existing version support.

The same guard observes the original `semver-noop` dependency evaluation and checks its genuine
cached namespace before JWT initialization. Scoped load and require boundaries preserve Node's
For jsonwebtoken, the same guard observes the original `semver-noop` dependency evaluation and
checks its genuine cached namespace before JWT initialization; fast-jwt has no such dependency. Scoped load and require boundaries preserve Node's
monitoring hooks while checking that fixed dependencies return the qualified module values.

The fixed Next.js WebSocket dependency has a separate absent-only profile for its two literal
Expand Down Expand Up @@ -158,10 +168,11 @@ refuse artifacts they cannot execute. These errors preserve the application's ru

The dedicated Linux CI control compiles and executes a real N-API binary, creates the actual adapter
artifact, starts its Node HTTP entry, and checks that independent Worker and Edge routes remain on
Workers. A separate CI step verifies a pinned, unchanged Next.js 16.4.3 native fixture, builds it
with the actual adapter and invokes its emitted Node entry on loopback, including `/blog/second`
with `fallback: true`. The unchanged App Router fixture `test/e2e/app-dir/turbopack-reports` is built
and invoked the same way. Its pages are prerendered, so the control adds one page rendered for each
request, and checks that a route left on Workers is refused by the Node entry. This checks the
artifact and Node transport; hosted cache and deployment
integration still require operator rollout verification. No CI result is implied by this document.
Workers. A separate CI step verifies the pinned, unchanged native fixture of each supported Next.js
release, 16.4.3 and 16.4.4. It builds the fixture with the actual adapter and invokes its emitted
Node entry on loopback, including `/blog/second` with `fallback: true`. The unchanged App Router
fixture `test/e2e/app-dir/turbopack-reports` is built and invoked the same way. Its pages are
prerendered, so the control adds one page rendered for each request, and checks that a route left
on Workers is refused by the Node entry. This checks the artifact and Node transport; hosted cache
and deployment integration still require operator rollout verification. No CI result is implied by
this document.
3 changes: 2 additions & 1 deletion docs/node26.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Runtime initialization rejects a different version; unknown versions cannot sele
The existing source hashes, loader ownership, optional dependency absence and realm checks remain
mandatory. The version is selected before application evaluation, with no request-time I/O.

Node 26 removed `buffer.SlowBuffer`, which the original fixed JWT dependency still reads.
Node 26 removed `buffer.SlowBuffer`, which the fixed JWT dependency of Next.js 16.4.3, jsonwebtoken,
still reads; 16.4.4's fast-jwt does not.
Trusted initialization installs a compatibility constructor before capturing the builtin realm.
It delegates to `Buffer.allocUnsafeSlow`, with the constructor/prototype chains from Node 24's
[official buffer implementation](https://github.com/nodejs/node/blob/v24.22.1/lib/buffer.js).
Expand Down
2 changes: 1 addition & 1 deletion fixtures/next-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": { "build": "upwind build --target=node" },
"dependencies": {
"next": "16.4.3",
"next": "16.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sharp": "0.35.5"
Expand Down
6 changes: 4 additions & 2 deletions packages/adapter/src/node-closure-proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
nodeClosureReference as isReference,
nodeCompilerMetadataSites,
} from './node-compiler-metadata.ts';
import { withNodeJwtLoader } from './node-jwt-compiler-profiles.ts';
import { nodeJwtCompilerSite, type NodeJwtCompilerProof } from './node-jwt-compiler-proof.ts';
import { isGlobalCompiler } from './node-jwt-compiler-sites.ts';
import { nodeNextCacheWriteSites } from './node-next-cache-write-proof.ts';
Expand Down Expand Up @@ -521,10 +522,11 @@ export function dynamicNodeClosureRefusal(
return 'an unproved Turbopack factory context';
const jwtSite = nodeJwtCompilerSite(jwt, file, code, program);
const compilerSites = new Set(nodeCompilerMetadataSites(all));
if (jwtSite !== undefined) compilerSites.add(jwtSite);
if (jwtSite?.kind === 'compiler') compilerSites.add(jwtSite.node);
const wsSites = nodeWsLoaderSites(loaders?.ws, file, code, program);
for (const site of wsSites) compilerSites.add(site);
const hookSites = nodeNextRequireHookSites(loaders?.nextRequireHook, file, code, program);
const hooks = nodeNextRequireHookSites(loaders?.nextRequireHook, file, code, program);
const hookSites = withNodeJwtLoader(hooks, jwtSite);
for (const visit of all) {
if (serverChunk && (isContextLoader(visit, contexts) || isContextEscape(visit, contexts)))
return 'an unproved Turbopack external import';
Expand Down
60 changes: 60 additions & 0 deletions packages/adapter/src/node-jwt-compiler-profiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Node } from './lexical-scope.ts';
import { nodeJwtGlobalRecoverySite } from './node-jwt-compiler-sites.ts';
import { nodeFastJwtBindingSite } from './node-jwt-fast-sites.ts';
import type { NodeNextRequireHookSites } from './node-next-require-hook-sites.ts';

/** The one site of a qualified source the ordinary closure walker refuses, and how it reads it. */
export interface NodeJwtCompilerSite {
/** A code compiler call, or a Node loader read. */
readonly kind: 'compiler' | 'loader';
readonly node: Node;
}

/** The JWT library a release of Next.js compiles in, by its exact published bytes. */
export interface NodeJwtCompilerProfile {
readonly name: 'next-16.4.3-jsonwebtoken-ncc-v1' | 'next-16.4.4-fast-jwt-ncc-v1';
readonly release: '16.4.3' | '16.4.4';
readonly suffix: string;
readonly sha256: string;
readonly bytes: number;
/** The builtins it loads, each by the exact specifier it writes. */
readonly builtins: readonly string[];
/** The no-op module Next.js substitutes for its `semver`, where it has one. */
readonly semverSha256?: string;
site(program: unknown): NodeJwtCompilerSite | undefined;
}

/** The require hook's exempt loader operations, with a qualified source's own loader read. */
export function withNodeJwtLoader(
hooks: NodeNextRequireHookSites,
site: NodeJwtCompilerSite | undefined,
): NodeNextRequireHookSites {
if (site?.kind !== 'loader') return hooks;
return { ...hooks, operations: new Set([...hooks.operations, site.node]) };
}

function siteOf(kind: NodeJwtCompilerSite['kind'], node: Node | undefined) {
return node === undefined ? undefined : Object.freeze({ kind, node });
}

export const NODE_JWT_COMPILER_PROFILES: readonly NodeJwtCompilerProfile[] = Object.freeze([
Object.freeze({
name: 'next-16.4.3-jsonwebtoken-ncc-v1',
release: '16.4.3',
suffix: '/dist/compiled/jsonwebtoken/index.js',
sha256: '056c2dddba96faa35f4dc0740228a241990561290ff3bd5456c7c7dbc5a7a6b9',
bytes: 121_051,
builtins: Object.freeze(['buffer', 'crypto', 'stream', 'util']),
semverSha256: '34c5853dd094229baa28a8b15b86e2143eeb0de0360530798ef3e9a7890942b6',
site: (program: unknown) => siteOf('compiler', nodeJwtGlobalRecoverySite(program)),
}),
Object.freeze({
name: 'next-16.4.4-fast-jwt-ncc-v1',
release: '16.4.4',
suffix: '/dist/compiled/fast-jwt/index.js',
sha256: '8cc4cea3ed6b85a6771a2546bbb88dcd1ca21e6061d89057728d3da4ecccfabf',
bytes: 131_737,
builtins: Object.freeze(['buffer', 'node:crypto', 'node:util', 'util']),
site: (program: unknown) => siteOf('loader', nodeFastJwtBindingSite(program)),
}),
]);
Loading
Loading