Skip to content

Commit

Permalink
Add support for 'use cache' in route handlers using the Edge runtime (
Browse files Browse the repository at this point in the history
#71258)

Same as #70897, but for the Edge runtime. The changes are based on what
we're already doing for app pages that are using the Edge runtime.
  • Loading branch information
unstubbable authored Oct 15, 2024
1 parent e68ce6e commit 2f16722
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 37 deletions.
15 changes: 11 additions & 4 deletions crates/next-core/src/next_app/app_route_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ pub async fn get_app_route_entry(
Vc::upcast(module_asset_context),
project_root,
rsc_entry,
pathname.clone(),
page,
next_config,
);
}

Expand All @@ -130,17 +131,23 @@ async fn wrap_edge_route(
asset_context: Vc<Box<dyn AssetContext>>,
project_root: Vc<FileSystemPath>,
entry: Vc<Box<dyn Module>>,
pathname: RcStr,
page: AppPage,
next_config: Vc<NextConfig>,
) -> Result<Vc<Box<dyn Module>>> {
const INNER: &str = "INNER_ROUTE_ENTRY";

let next_config = &*next_config.await?;

let source = load_next_js_template(
"edge-app-route.js",
project_root,
fxindexmap! {
"VAR_USERLAND" => INNER.into(),
"VAR_PAGE" => page.to_string().into(),
},
fxindexmap! {
"nextConfig" => serde_json::to_string(next_config)?.into(),
},
fxindexmap! {},
fxindexmap! {},
)
.await?;
Expand All @@ -160,6 +167,6 @@ async fn wrap_edge_route(
asset_context,
project_root,
wrapped,
pathname,
AppPath::from(page).to_string().into(),
))
}
2 changes: 1 addition & 1 deletion packages/next/src/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ export function getEdgeServerEntry(opts: {
absolutePagePath: opts.absolutePagePath,
page: opts.page,
appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'),
nextConfigOutput: opts.config.output,
nextConfig: Buffer.from(JSON.stringify(opts.config)).toString('base64'),
preferredRegion: opts.preferredRegion,
middlewareConfig: Buffer.from(
JSON.stringify(opts.middlewareConfig || {})
Expand Down
25 changes: 24 additions & 1 deletion packages/next/src/build/templates/edge-app-route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
import { createServerModuleMap } from '../../server/app-render/action-utils'
import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
import type { NextConfigComplete } from '../../server/config-shared'
import { EdgeRouteModuleWrapper } from '../../server/web/edge-route-module-wrapper'

// Import the userland code.
import * as module from 'VAR_USERLAND'

// injected by the loader afterwards.
declare const nextConfig: NextConfigComplete
// INJECT:nextConfig

const maybeJSONParse = (str?: string) => (str ? JSON.parse(str) : undefined)

const rscManifest = self.__RSC_MANIFEST?.['VAR_PAGE']
const rscServerManifest = maybeJSONParse(self.__RSC_SERVER_MANIFEST)

if (rscManifest && rscServerManifest) {
setReferenceManifestsSingleton({
clientReferenceManifest: rscManifest,
serverActionsManifest: rscServerManifest,
serverModuleMap: createServerModuleMap({
serverActionsManifest: rscServerManifest,
pageName: 'VAR_PAGE',
}),
})
}

export const ComponentMod = module

export default EdgeRouteModuleWrapper.wrap(module.routeModule)
export default EdgeRouteModuleWrapper.wrap(module.routeModule, { nextConfig })
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { getModuleBuildInfo } from '../get-module-build-info'
import { stringifyRequest } from '../../stringify-request'
import type { NextConfig } from '../../../../server/config-shared'
import type { webpack } from 'next/dist/compiled/webpack/webpack'
import { WEBPACK_RESOURCE_QUERIES } from '../../../../lib/constants'
import type { MiddlewareConfig } from '../../../analysis/get-page-static-info'
import { loadEntrypoint } from '../../../load-entrypoint'
import { isMetadataRoute } from '../../../../lib/metadata/is-metadata-route'

export type EdgeAppRouteLoaderQuery = {
absolutePagePath: string
page: string
appDirLoader: string
preferredRegion: string | string[] | undefined
nextConfigOutput: NextConfig['output']
nextConfig: string
middlewareConfig: string
}

Expand All @@ -23,6 +23,7 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction<EdgeAppRouteLoaderQue
preferredRegion,
appDirLoader: appDirLoaderBase64 = '',
middlewareConfig: middlewareConfigBase64 = '',
nextConfig: nextConfigBase64,
} = this.getOptions()

const appDirLoader = Buffer.from(appDirLoaderBase64, 'base64').toString()
Expand All @@ -36,7 +37,7 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction<EdgeAppRouteLoaderQue
const buildInfo = getModuleBuildInfo(this._module)

buildInfo.nextEdgeSSR = {
isServerComponent: false,
isServerComponent: !isMetadataRoute(page), // Needed for 'use cache'.
page: page,
isAppDir: true,
}
Expand All @@ -53,9 +54,21 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction<EdgeAppRouteLoaderQue
stringifiedPagePath.length - 1
)}?${WEBPACK_RESOURCE_QUERIES.edgeSSREntry}`

return await loadEntrypoint('edge-app-route', {
VAR_USERLAND: modulePath,
})
const stringifiedConfig = Buffer.from(
nextConfigBase64 || '',
'base64'
).toString()

return await loadEntrypoint(
'edge-app-route',
{
VAR_USERLAND: modulePath,
VAR_PAGE: page,
},
{
nextConfig: stringifiedConfig,
}
)
}

export default EdgeAppRouteLoader
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { getProxiedPluginState } from '../../build-context'
import { PAGE_TYPES } from '../../../lib/page-types'
import { getModuleBuildInfo } from '../loaders/get-module-build-info'
import { getAssumedSourceType } from '../loaders/next-flight-loader'
import { isAppRouteRoute } from '../../../lib/is-app-route-route'

interface Options {
dev: boolean
Expand Down Expand Up @@ -394,16 +395,18 @@ export class FlightClientEntryPlugin {
addClientEntryAndSSRModulesList.push(injected)
}

// Create internal app
addClientEntryAndSSRModulesList.push(
this.injectClientEntryAndSSRModules({
compiler,
compilation,
entryName: name,
clientImports: { ...internalClientComponentEntryImports },
bundlePath: APP_CLIENT_INTERNALS,
})
)
if (!isAppRouteRoute(name)) {
// Create internal app
addClientEntryAndSSRModulesList.push(
this.injectClientEntryAndSSRModules({
compiler,
compilation,
entryName: name,
clientImports: { ...internalClientComponentEntryImports },
bundlePath: APP_CLIENT_INTERNALS,
})
)
}

if (actionEntryImports.size > 0) {
if (!actionMapsPerEntry[name]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function getEntryFiles(
.map(
(file) =>
'server/' +
file.replace('.js', '_' + CLIENT_REFERENCE_MANIFEST + '.js')
file.replace(/\.js$/, '_' + CLIENT_REFERENCE_MANIFEST + '.js')
)
)
}
Expand Down
6 changes: 5 additions & 1 deletion packages/next/src/build/webpack/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,18 @@ export function forEachEntryModule(

if (
!request.startsWith('next-edge-ssr-loader?') &&
!request.startsWith('next-edge-app-route-loader?') &&
!request.startsWith('next-app-loader?')
)
continue

let entryModule: NormalModule =
compilation.moduleGraph.getResolvedModule(entryDependency)

if (request.startsWith('next-edge-ssr-loader?')) {
if (
request.startsWith('next-edge-ssr-loader?') ||
request.startsWith('next-edge-app-route-loader?')
) {
entryModule.dependencies.forEach((dependency) => {
const modRequest: string | undefined = (dependency as any).request
if (modRequest?.includes('next-app-loader')) {
Expand Down
19 changes: 11 additions & 8 deletions packages/next/src/server/web/edge-route-module-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ import { searchParamsToUrlQuery } from '../../shared/lib/router/utils/querystrin
import type { RequestLifecycleOpts } from '../base-server'
import { CloseController, trackStreamConsumed } from './web-on-close'
import { getEdgePreviewProps } from './get-edge-preview-props'
import type { NextConfigComplete } from '../config-shared'

type WrapOptions = Partial<Pick<AdapterOptions, 'page'>>
export interface WrapOptions {
nextConfig: NextConfigComplete
}

/**
* EdgeRouteModuleWrapper is a wrapper around a route module.
Expand All @@ -33,7 +36,10 @@ export class EdgeRouteModuleWrapper {
*
* @param routeModule the route module to wrap
*/
private constructor(private readonly routeModule: AppRouteRouteModule) {
private constructor(
private readonly routeModule: AppRouteRouteModule,
private readonly nextConfig: NextConfigComplete
) {
// TODO: (wyattjoh) possibly allow the module to define it's own matcher
this.matcher = new RouteMatcher(routeModule.definition)
}
Expand All @@ -47,18 +53,14 @@ export class EdgeRouteModuleWrapper {
* override the ones passed from the runtime
* @returns a function that can be used as a handler for the edge runtime
*/
public static wrap(
routeModule: AppRouteRouteModule,
options: WrapOptions = {}
) {
public static wrap(routeModule: AppRouteRouteModule, options: WrapOptions) {
// Create the module wrapper.
const wrapper = new EdgeRouteModuleWrapper(routeModule)
const wrapper = new EdgeRouteModuleWrapper(routeModule, options.nextConfig)

// Return the wrapping function.
return (opts: AdapterOptions) => {
return adapter({
...opts,
...options,
IncrementalCache,
// Bind the handler method to the wrapper so it still has context.
handler: wrapper.handler.bind(wrapper),
Expand Down Expand Up @@ -118,6 +120,7 @@ export class EdgeRouteModuleWrapper {
dynamicIO: !!process.env.__NEXT_DYNAMIC_IO,
},
buildId: '', // TODO: Populate this properly.
cacheLifeProfiles: this.nextConfig.experimental.cacheLife,
},
}

Expand Down
4 changes: 4 additions & 0 deletions test/e2e/app-dir/app-static/app-static.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,9 +775,11 @@ describe('app-dir static/dynamic handling', () => {
"api/large-data/route.js",
"api/large-data/route_client-reference-manifest.js",
"api/revalidate-path-edge/route.js",
"api/revalidate-path-edge/route_client-reference-manifest.js",
"api/revalidate-path-node/route.js",
"api/revalidate-path-node/route_client-reference-manifest.js",
"api/revalidate-tag-edge/route.js",
"api/revalidate-tag-edge/route_client-reference-manifest.js",
"api/revalidate-tag-node/route.js",
"api/revalidate-tag-node/route_client-reference-manifest.js",
"articles/[slug]/page.js",
Expand Down Expand Up @@ -935,6 +937,7 @@ describe('app-dir static/dynamic handling', () => {
"response-url/page.js",
"response-url/page_client-reference-manifest.js",
"route-handler-edge/revalidate-360/route.js",
"route-handler-edge/revalidate-360/route_client-reference-manifest.js",
"route-handler/no-store-force-static/route.js",
"route-handler/no-store-force-static/route_client-reference-manifest.js",
"route-handler/no-store/route.js",
Expand Down Expand Up @@ -968,6 +971,7 @@ describe('app-dir static/dynamic handling', () => {
"stale-cache-serving-edge/app-page/page.js",
"stale-cache-serving-edge/app-page/page_client-reference-manifest.js",
"stale-cache-serving-edge/route-handler/route.js",
"stale-cache-serving-edge/route-handler/route_client-reference-manifest.js",
"stale-cache-serving/app-page/page.js",
"stale-cache-serving/app-page/page_client-reference-manifest.js",
"stale-cache-serving/route-handler/route.js",
Expand Down
3 changes: 0 additions & 3 deletions test/e2e/app-dir/dynamic-io/dynamic-io.routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,6 @@ describe('dynamic-io', () => {
expect(message2).toEqual(json.message2)
}

// TODO: Edge is missing Server Manifest for routes.
/*
str = await next.render('/routes/-edge/use_cache-cached', {})
json = JSON.parse(str)

Expand All @@ -259,7 +257,6 @@ describe('dynamic-io', () => {
expect(json.value).toEqual('at runtime')
expect(message1).toEqual(json.message1)
expect(message2).toEqual(json.message2)
*/
}
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const runtime = 'edge'

export { GET } from '../node/route'
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@ describe('use-cache-route-handler-only', () => {

const itSkipTurbopack = isTurbopack ? it.skip : it

itSkipTurbopack('should cache results in route handlers', async () => {
const response = await next.fetch('/')
itSkipTurbopack('should cache results in node route handlers', async () => {
const response = await next.fetch('/node')
const { rand1, rand2 } = await response.json()

expect(rand1).toEqual(rand2)
})

itSkipTurbopack('should cache results in edge route handlers', async () => {
const response = await next.fetch('/edge')
const { rand1, rand2 } = await response.json()

expect(rand1).toEqual(rand2)
Expand Down

0 comments on commit 2f16722

Please sign in to comment.