Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for 'use cache' in route handlers using the Edge runtime #71258

Merged
merged 5 commits into from
Oct 15, 2024
Merged
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
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
Loading