Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/proud-cases-shine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@next-community/adapter-vercel": patch
---

Pass through prerender classification metadata (`hasPostponed`, `hasFallback`, `htmlSize`, `isDynamicRoute`) from `AdapterOutput['PRERENDER']` to `prerender-config.json`, matching the `@vercel/next` builder so deployment summaries can classify PPR and Cache Components routes. The fields are tri-state: `false`/`0` are written as-is and only `undefined` is omitted.
2 changes: 2 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ jobs:
run: git diff --exit-code
- name: Run typecheck
run: pnpm run typecheck
- name: Run tests
run: pnpm run test

enforce-changeset:
name: Enforce Changeset
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"format": "biome format --write && biome check --fix",
"prepare": "husky",
"typecheck": "biome check",
"test": "turbo test",
"clean": "turbo clean",
"changeset": "changeset"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"scripts": {
"build": "rm -rf dist; tsc --noEmit && node --experimental-transform-types build.mts",
"test": "vitest"
"test": "vitest run"
},
"devDependencies": {
"@types/bytes": "3.1.1",
Expand All @@ -30,6 +30,7 @@
"next": "16.3.0-canary.24",
"picomatch": "4.0.1",
"source-map": "0.7.4",
"vitest": "3.2.7",
"webpack-sources": "3.2.3"
},
"engines": {
Expand Down
88 changes: 88 additions & 0 deletions packages/adapter/src/outputs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import type { AdapterOutput } from 'next';
import { AdapterOutputType } from 'next/dist/shared/lib/constants';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import type { FuncOutputs } from './outputs';
import { handlePrerenderOutputs } from './outputs';

describe('handlePrerenderOutputs', () => {
let vercelOutputDir: string;

beforeEach(async () => {
vercelOutputDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'adapter-vercel-test-')
);
});

afterEach(async () => {
await fs.rm(vercelOutputDir, { recursive: true, force: true });
});

function createPrerenderOutput(
extraFields: Record<string, unknown> = {}
): AdapterOutput['PRERENDER'] {
return {
id: 'prerender-1',
pathname: '/blog/hello',
type: AdapterOutputType.PRERENDER,
parentOutputId: 'parent-1',
groupId: 1,
config: {
allowQuery: ['slug'],
allowHeader: ['x-prerender-revalidate'],
},
...extraFields,
} as AdapterOutput['PRERENDER'];
}

async function runAndReadConfig(output: AdapterOutput['PRERENDER']) {
const nodeOutputsParentMap = new Map<string, FuncOutputs[0]>([
['parent-1', { pathname: '/blog/hello' } as FuncOutputs[0]],
]);

await handlePrerenderOutputs([output], {
config: {},
vercelOutputDir,
nodeOutputsParentMap,
rscContentType: 'text/x-component',
varyHeader: 'RSC',
});

return fs.readFile(
path.join(
vercelOutputDir,
'functions',
'blog/hello.prerender-config.json'
),
'utf8'
);
}

it('passes prerender classification metadata through without collapsing false/0', async () => {
const rawConfig = await runAndReadConfig(
createPrerenderOutput({
hasPostponed: true,
hasFallback: false,
htmlSize: 0,
isDynamicRoute: true,
})
);

const parsedConfig = JSON.parse(rawConfig);
expect(parsedConfig.hasPostponed).toBe(true);
expect(parsedConfig.hasFallback).toBe(false);
expect(parsedConfig.htmlSize).toBe(0);
expect(parsedConfig.isDynamicRoute).toBe(true);
});

it('omits classification keys when the fields are not provided', async () => {
const rawConfig = await runAndReadConfig(createPrerenderOutput());

expect(rawConfig).not.toContain('hasPostponed');
expect(rawConfig).not.toContain('hasFallback');
expect(rawConfig).not.toContain('htmlSize');
expect(rawConfig).not.toContain('isDynamicRoute');
});
});
47 changes: 47 additions & 0 deletions packages/adapter/src/outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,41 @@ export async function handleNodeOutputs(
);
}

/**
* Prerender classification metadata added to `AdapterOutput['PRERENDER']`
* by https://github.com/vercel/next.js/pull/95534. Remove this type (and
* the cast below) once the `next` devDependency includes those fields.
*/
type PrerenderClassificationFields = {
/**
* hasPostponed signals whether the build-time prerender of a PPR app
* page postponed (React suspended on dynamic data). `false` means it
* prerendered without postponing; `undefined` means the signal does
* not apply (pages router, route handlers, blocking templates).
*/
hasPostponed?: boolean;
/**
* hasFallback signals whether a dynamic route template has a static
* fallback shell generated during build. `false` means the template
* is blocking or omitted; `undefined` means the concept does not
* apply (concrete prerenders).
*/
hasFallback?: boolean;
/**
* htmlSize is the byte size of the prerendered HTML shell for app
* pages. `0` means an empty shell (everything postponed). It is only
* set on the HTML prerender output; RSC/data/segment outputs leave it
* `undefined`, as do pages router and route handler outputs.
*/
htmlSize?: number;
/**
* isDynamicRoute signals whether this prerender originates from a
* dynamic route template (`dynamicRoutes` in the prerender manifest)
* rather than a concrete prerendered path (`routes`).
*/
isDynamicRoute?: boolean;
};
Comment on lines +618 to +651

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to remove this once the Nextjs side is merged. then bumping the next canary version dependency here will give you the true types.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, wont merge this until the next changes are merged and I make this change!


export async function handlePrerenderOutputs(
prerenderOutputs: Array<AdapterOutput['PRERENDER']>,
{
Expand Down Expand Up @@ -672,6 +707,9 @@ export async function handlePrerenderOutputs(
)
: undefined;

const classification = output as AdapterOutput['PRERENDER'] &
PrerenderClassificationFields;

const { parentOutputId } = output;
prerenderParentIds.add(parentOutputId);

Expand Down Expand Up @@ -783,6 +821,15 @@ export async function handlePrerenderOutputs(
allowHeader: output.config.allowHeader,
partialFallback: output.config.partialFallback || undefined,

// Prerender classification metadata (vercel/next.js#95534),
// passed through raw: the values are tri-state, so `false`/`0`
// must stay distinct from `undefined` (which JSON.stringify
// drops).
hasPostponed: classification.hasPostponed,
hasFallback: classification.hasFallback,
htmlSize: classification.htmlSize,
isDynamicRoute: classification.isDynamicRoute,

bypassToken: output.config.bypassToken,
experimentalBypassFor: output.config.bypassFor,

Expand Down
Loading