Skip to content

Commit

Permalink
Reapply "[next] Reland add .action handling for dynamic routes" (#11509)
Browse files Browse the repository at this point in the history
This reverts commit 5b79603.
  • Loading branch information
ztanner committed Apr 29, 2024
1 parent b0e2b6c commit 45ce5f9
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 25 deletions.
2 changes: 2 additions & 0 deletions packages/next/src/index.ts
Expand Up @@ -1928,6 +1928,7 @@ export const build: BuildV2 = async ({
bypassToken: prerenderManifest.bypassToken || '',
isServerMode,
experimentalPPRRoutes,
hasActionOutputSupport: false,
}).then(arr =>
localizeDynamicRoutes(
arr,
Expand Down Expand Up @@ -1958,6 +1959,7 @@ export const build: BuildV2 = async ({
bypassToken: prerenderManifest.bypassToken || '',
isServerMode,
experimentalPPRRoutes,
hasActionOutputSupport: false,
}).then(arr =>
arr.map(route => {
route.src = route.src.replace('^', `^${dynamicPrefix}`);
Expand Down
98 changes: 98 additions & 0 deletions packages/next/src/server-build.ts
Expand Up @@ -51,6 +51,7 @@ import {
normalizePrefetches,
CreateLambdaFromPseudoLayersOptions,
getPostponeResumePathname,
LambdaGroup,
MAX_UNCOMPRESSED_LAMBDA_SIZE,
} from './utils';
import {
Expand All @@ -68,6 +69,7 @@ const CORRECT_NOT_FOUND_ROUTES_VERSION = 'v12.0.1';
const CORRECT_MIDDLEWARE_ORDER_VERSION = 'v12.1.7-canary.29';
const NEXT_DATA_MIDDLEWARE_RESOLVING_VERSION = 'v12.1.7-canary.33';
const EMPTY_ALLOW_QUERY_FOR_PRERENDERED_VERSION = 'v12.2.0';
const ACTION_OUTPUT_SUPPORT_VERSION = 'v14.2.2';
const CORRECTED_MANIFESTS_VERSION = 'v12.2.0';

// Ideally this should be in a Next.js manifest so we can change it in
Expand Down Expand Up @@ -199,6 +201,10 @@ export async function serverBuild({
nextVersion,
EMPTY_ALLOW_QUERY_FOR_PRERENDERED_VERSION
);
const hasActionOutputSupport = semver.gte(
nextVersion,
ACTION_OUTPUT_SUPPORT_VERSION
);
const projectDir = requiredServerFilesManifest.relativeAppDir
? path.join(baseDir, requiredServerFilesManifest.relativeAppDir)
: requiredServerFilesManifest.appDir || entryPath;
Expand Down Expand Up @@ -926,11 +932,23 @@ export async function serverBuild({
inversedAppPathManifest,
});

const appRouterStreamingActionLambdaGroups: LambdaGroup[] = [];

for (const group of appRouterLambdaGroups) {
if (!group.isPrerenders || group.isExperimentalPPR) {
group.isStreaming = true;
}
group.isAppRouter = true;

// We create a streaming variant of the Prerender lambda group
// to support actions that are part of a Prerender
if (hasActionOutputSupport) {
appRouterStreamingActionLambdaGroups.push({
...group,
isActionLambda: true,
isStreaming: true,
});
}
}

for (const group of appRouteHandlersLambdaGroups) {
Expand Down Expand Up @@ -982,6 +1000,13 @@ export async function serverBuild({
pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})),
appRouterStreamingPrerenderLambdaGroups:
appRouterStreamingActionLambdaGroups.map(group => ({
pages: group.pages,
isPrerender: group.isPrerenders,
pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})),
appRouteHandlersLambdaGroups: appRouteHandlersLambdaGroups.map(
group => ({
pages: group.pages,
Expand All @@ -999,6 +1024,7 @@ export async function serverBuild({
const combinedGroups = [
...pageLambdaGroups,
...appRouterLambdaGroups,
...appRouterStreamingActionLambdaGroups,
...apiLambdaGroups,
...appRouteHandlersLambdaGroups,
];
Expand Down Expand Up @@ -1208,6 +1234,11 @@ export async function serverBuild({

let outputName = path.posix.join(entryDirectory, pageName);

if (group.isActionLambda) {
// give the streaming prerenders a .action suffix
outputName = `${outputName}.action`;
}

// If this is a PPR page, then we should prefix the output name.
if (isPPR) {
if (!revalidate) {
Expand Down Expand Up @@ -1378,6 +1409,7 @@ export async function serverBuild({
isServerMode: true,
dynamicMiddlewareRouteMap: middleware.dynamicRouteMap,
experimentalPPRRoutes,
hasActionOutputSupport,
}).then(arr =>
localizeDynamicRoutes(
arr,
Expand Down Expand Up @@ -1905,6 +1937,72 @@ export async function serverBuild({
},
]
: []),
...(hasActionOutputSupport
? [
// Create rewrites for streaming prerenders (.action routes)
// This contains separate rewrites for each possible "has" (action header, or content-type)
// Also includes separate handling for index routes which should match to /index.action.
// This follows the same pattern as the rewrites for .rsc files.
{
src: `^${path.posix.join('/', entryDirectory, '/')}`,
dest: path.posix.join('/', entryDirectory, '/index.action'),
has: [
{
type: 'header',
key: 'next-action',
},
],
continue: true,
override: true,
},
{
src: `^${path.posix.join('/', entryDirectory, '/')}`,
dest: path.posix.join('/', entryDirectory, '/index.action'),
has: [
{
type: 'header',
key: 'content-type',
value: 'multipart/form-data;.*',
},
],
continue: true,
override: true,
},
{
src: `^${path.posix.join(
'/',
entryDirectory,
'/((?!.+\\.action).+?)(?:/)?$'
)}`,
dest: path.posix.join('/', entryDirectory, '/$1.action'),
has: [
{
type: 'header',
key: 'next-action',
},
],
continue: true,
override: true,
},
{
src: `^${path.posix.join(
'/',
entryDirectory,
'/((?!.+\\.action).+?)(?:/)?$'
)}`,
dest: path.posix.join('/', entryDirectory, '/$1.action'),
has: [
{
type: 'header',
key: 'content-type',
value: 'multipart/form-data;.*',
},
],
continue: true,
override: true,
},
]
: []),
{
src: `^${path.posix.join('/', entryDirectory, '/')}`,
has: [
Expand Down
30 changes: 22 additions & 8 deletions packages/next/src/utils.ts
Expand Up @@ -321,6 +321,7 @@ export async function getDynamicRoutes({
isServerMode,
dynamicMiddlewareRouteMap,
experimentalPPRRoutes,
hasActionOutputSupport,
}: {
entryPath: string;
entryDirectory: string;
Expand All @@ -333,6 +334,7 @@ export async function getDynamicRoutes({
isServerMode?: boolean;
dynamicMiddlewareRouteMap?: ReadonlyMap<string, RouteWithSrc>;
experimentalPPRRoutes: ReadonlySet<string>;
hasActionOutputSupport: boolean;
}): Promise<RouteWithSrc[]> {
if (routesManifest) {
switch (routesManifest.version) {
Expand Down Expand Up @@ -423,14 +425,25 @@ export async function getDynamicRoutes({
});
}

routes.push({
...route,
src: route.src.replace(
new RegExp(escapeStringRegexp('(?:/)?$')),
'(?:\\.rsc)(?:/)?$'
),
dest: route.dest?.replace(/($|\?)/, '.rsc$1'),
});
if (hasActionOutputSupport) {
routes.push({
...route,
src: route.src.replace(
new RegExp(escapeStringRegexp('(?:/)?$')),
'(?<nxtsuffix>(?:\\.action|\\.rsc))(?:/)?$'
),
dest: route.dest?.replace(/($|\?)/, '$nxtsuffix$1'),
});
} else {
routes.push({
...route,
src: route.src.replace(
new RegExp(escapeStringRegexp('(?:/)?$')),
'(?:\\.rsc)(?:/)?$'
),
dest: route.dest?.replace(/($|\?)/, '.rsc$1'),
});
}

routes.push(route);
}
Expand Down Expand Up @@ -1487,6 +1500,7 @@ export type LambdaGroup = {
isStreaming?: boolean;
isPrerenders?: boolean;
isExperimentalPPR?: boolean;
isActionLambda?: boolean;
isPages?: boolean;
isApiLambda: boolean;
pseudoLayer: PseudoLayer;
Expand Down
Expand Up @@ -7,4 +7,4 @@ export default function Root({ children }) {
<body>{children}</body>
</html>
);
}
}
31 changes: 17 additions & 14 deletions packages/next/test/fixtures/00-app-dir-actions/index.test.js
Expand Up @@ -37,6 +37,7 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
).then(res => res.json());

ctx.actionManifest = actionManifest;

Object.assign(ctx, info);
});

Expand All @@ -57,8 +58,8 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
expect(res.status).toEqual(200);
const body = await res.text();
expect(body).toContain('1338');
expect(res.headers.get('x-matched-path')).toBe(path);
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});

it('should bypass the static cache for a server action on a page with dynamic params', async () => {
Expand All @@ -77,8 +78,8 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
expect(res.status).toEqual(200);
const body = await res.text();
expect(body).toContain('1338');
expect(res.headers.get('x-matched-path')).toBe(path);
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});

it('should bypass the static cache for a multipart request (no action header)', async () => {
Expand Down Expand Up @@ -116,7 +117,7 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
expect(res.status).toEqual(200);
const body = await res.text();
expect(body).toContain('1338');
expect(res.headers.get('x-matched-path')).toBe(path);
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
});
Expand All @@ -132,9 +133,9 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
);

expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(path);
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});

it('should bypass the static cache for a server action on a page with dynamic params', async () => {
Expand All @@ -147,9 +148,9 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
);

expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(path);
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});

it('should properly invoke the action on a dynamic page', async () => {
Expand All @@ -162,7 +163,7 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
);

expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(path);
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
Expand All @@ -180,9 +181,11 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
);

expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(path);
expect(res.headers.get('x-matched-path')).toBe(
'/rsc/static/generate-static-params/[slug].action'
);
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});

it('should bypass the static cache for a server action when not pre-generated', async () => {
Expand All @@ -195,9 +198,9 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
);

expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(page);
expect(res.headers.get('x-matched-path')).toBe(page + '.action');
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
});
});
Expand Down
@@ -0,0 +1 @@
module.exports = {};
4 changes: 2 additions & 2 deletions packages/next/test/integration/integration-2.test.js
Expand Up @@ -428,7 +428,7 @@ it('should handle edge functions in app with basePath', async () => {
edgeFunctions.add(item);
}
}
expect(lambdas.size).toBe(1);
expect(lambdas.size).toBe(2);
expect(edgeFunctions.size).toBe(4);
});

Expand All @@ -452,5 +452,5 @@ it('should not generate lambdas that conflict with static index route in app wit
lambdas.add(item);
}
}
expect(lambdas.size).toBe(1);
expect(lambdas.size).toBe(3);
});

0 comments on commit 45ce5f9

Please sign in to comment.