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

implement custom cache handler logic #606

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const configs: Config[] = [
{ name: 'headers', support: '✅' },
{ name: 'httpAgentOptions', support: 'N/A' },
{ name: 'images', support: '✅' },
{ name: 'incrementalCacheHandlerPath', support: '🔄' },
{ name: 'cacheHandler', support: '' },
{ name: 'logging', support: 'N/A' },
{ name: 'experimental/mdxRs', support: '✅' },
{ name: 'onDemandEntries', support: 'N/A' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ describe('no-unsupported-configs', () => {
/** @type {import('next').NextConfig} */
const nextConfig = {
assetPrefix: 'test',
incrementalCacheHandlerPath: true,
generateEtags: true,
}

module.exports = nextConfig
Expand All @@ -225,7 +225,7 @@ describe('no-unsupported-configs', () => {
},
{
message:
'The "incrementalCacheHandlerPath" configuration is not currently supported by next-on-pages.',
'The "generateEtags" configuration is not currently supported by next-on-pages.',
},
],
},
Expand Down
8 changes: 8 additions & 0 deletions packages/next-on-pages/build-metadata.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@
type NextOnPagesBuildMetadata = {
/** Locales used by the application (collected from the Vercel output) */
collectedLocales: string[];
/** (subset of) values obtained from the user's next.config.js (if any was found) */
config?: {
experimental?: Pick<
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- the import needs to be dynamic since the nextConfig file itself uses this type
import('./src/buildApplication/nextConfig').NextConfigExperimental,
'allowedRevalidateHeaderKeys' | 'fetchCacheKeyPrefix'
>;
};
};
18 changes: 11 additions & 7 deletions packages/next-on-pages/docs/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@

`@cloudflare/next-on-pages` comes with support for data revalidation and caching for fetch requests. This is done in our router and acts as an extension to Next.js' built-in functionality.

## Storage Options
## Zero Configuration Options
petebacondarwin marked this conversation as resolved.
Show resolved Hide resolved

There are various different bindings and storage options that one could use for caching. At the moment, `@cloudflare/next-on-pages` supports the Cache API and Workers KV out-of-the-box.
The following are two different caching implementation options that don't require any code nor configuration change.

In the future, support will be available for creating custom cache interfaces and using different bindings.

### Cache API
### Workers Cache API

The [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) is a per data-center cache that is ideal for storing data that is not required to be accessible globally. It is worth noting that Vercel's Data Cache is regional, like with the Cache API, so there is no difference in terms of data availability.

Due to how the Cache API works, you need to be using a domain for your deployment for it to take effect.

Besides the above requirement, no other action is needed to use this option.

### Workers KV

[Workers KV](https://developers.cloudflare.com/kv/) is a low-latency key-value store that is ideal for storing data that should be globally distributed. KV is eventually consistent, which means that it will take up to 60 seconds for updates to be reflected globally.
[Workers KV](https://developers.cloudflare.com/kv/) is a low-latency key-value store that is ideal for storing data that should be globally distributed. KV is eventually consistent, which means that it can take up to 60 seconds for updates to be reflected globally.

To use this option all you need to to is to add a binding to your Pages project with the name `__NEXT_ON_PAGES__KV_SUSPENSE_CACHE`, and map it to a KV namespace.

## Custom Cache Handler

To use Workers KV for caching, you need to add a binding to your Pages project with the name `__NEXT_ON_PAGES__KV_SUSPENSE_CACHE`, and map it to a KV namespace.
In case a custom solution is needed (for example in order to integrate with a third party storage solution or integrate with different [Cloudflare Bindings](https://developers.cloudflare.com/pages/functions/bindings/)) you will need to implement your own logic and register it via the Next.js [cacheHandler](https://nextjs.org/docs/app/api-reference/next-config-js/incrementalCacheHandlerPath) option.
2 changes: 1 addition & 1 deletion packages/next-on-pages/docs/supported.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ To check the latest state of the routers and possible missing features you can c
| headers | [pages](https://nextjs.org/docs/pages/api-reference/next-config-js/headers), [app](https://nextjs.org/docs/app/api-reference/next-config-js/headers) | ✅ |
| httpAgentOptions | [pages](https://nextjs.org/docs/pages/api-reference/next-config-js/httpAgentOptions), [app](https://nextjs.org/docs/app/api-reference/next-config-js/httpAgentOptions) | `N/A` |
| images | [pages](https://nextjs.org/docs/pages/api-reference/next-config-js/images), [app](https://nextjs.org/docs/app/api-reference/next-config-js/images) | ✅ |
| incrementalCacheHandlerPath | [app](https://nextjs.org/docs/app/api-reference/next-config-js/incrementalCacheHandlerPath) | 🔄 |
| cacheHandler | [app](https://nextjs.org/docs/app/api-reference/next-config-js/incrementalCacheHandlerPath) | |
| logging | [app](https://nextjs.org/docs/app/api-reference/next-config-js/logging) | `N/A`<sup>5</sup> |
| mdxRs | [app](https://nextjs.org/docs/app/api-reference/next-config-js/mdxRs) | ✅ |
| onDemandEntries | [pages](https://nextjs.org/docs/pages/api-reference/next-config-js/onDemandEntries), [app](https://nextjs.org/docs/app/api-reference/next-config-js/onDemandEntries) | `N/A`<sup>6</sup> |
Expand Down
1 change: 1 addition & 0 deletions packages/next-on-pages/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ declare global {
CF_PAGES?: string;
SHELL?: string;
__NEXT_ON_PAGES__KV_SUSPENSE_CACHE?: KVNamespace;
__BUILD_METADATA__: NextOnPagesBuildMetadata;
[key: string]: string | Fetcher;
}
}
Expand Down
87 changes: 87 additions & 0 deletions packages/next-on-pages/src/buildApplication/buildCacheFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { build } from 'esbuild';
import { join } from 'path';
import { getNextConfig } from './nextConfig';

/**
* Builds files needed by the application in order to implement the Next.js suspense caching, these can
* be either for the custom cache handler provided by the user or the builtin cache handlers.
*
* @param nopDistDir path to the dist directory in which the build process saves the output files
* @param minify flag indicating wether minification should be applied to the cache files
* @param templatesDir path to the templates directory
*/
export async function buildCacheFiles(
nopDistDir: string,
minify: boolean,
templatesDir: string,
): Promise<void> {
const outputCacheDir = join(nopDistDir, 'cache');

const nextConfig = await getNextConfig();

const cacheHandlerPath = nextConfig?.cacheHandler;

if (cacheHandlerPath) {
await buildCustomIncrementalCacheHandler(
cacheHandlerPath,
outputCacheDir,
minify,
);
} else {
await buildBuiltInCacheHandlers(templatesDir, outputCacheDir, minify);
}
}

/**
* Builds the file implementing the custom cache handler provided by the user.
*
* @param cacheHandlerPath path to the user defined incremental cache handler
* @param outputCacheDir path to the directory in which to write the file
* @param minify flag indicating wether minification should be applied to the output file
*/
async function buildCustomIncrementalCacheHandler(
cacheHandlerPath: string,
outputCacheDir: string,
minify: boolean,
): Promise<void> {
try {
await build({
entryPoints: [cacheHandlerPath],
bundle: true,
target: 'es2022',
platform: 'neutral',
outfile: join(outputCacheDir, 'custom.js'),
minify,
});
} catch {
throw new Error(
`Failed to build custom incremental cache handler from the following provided path: ${cacheHandlerPath}`,
);
}
}

/**
* Builds the files implementing the builtin cache handlers.
*
* @param templatesDir path to the templates directory (from which the builtin cache files are taken)
* @param outputCacheDir path to the directory in which to write the files
* @param minify flag indicating wether minification should be applied to the cache files
*/
async function buildBuiltInCacheHandlers(
templatesDir: string,
outputCacheDir: string,
minify: boolean,
): Promise<void> {
await build({
entryPoints: [
'builtInCacheHandler.ts',
'workersCacheApiCacheHandler.ts',
'KVCacheHandler.ts',
].map(fileName => join(templatesDir, 'cache', fileName)),
bundle: false,
target: 'es2022',
platform: 'neutral',
outdir: outputCacheDir,
minify,
});
}
22 changes: 12 additions & 10 deletions packages/next-on-pages/src/buildApplication/buildWorkerFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { generateGlobalJs } from './generateGlobalJs';
import type { ProcessedVercelOutput } from './processVercelOutput';
import { getNodeEnv } from '../utils/getNodeEnv';
import { normalizePath } from '../utils';
import { buildCacheFiles } from './buildCacheFiles';
import { extractBuildMetadataConfig, getNextConfig } from './nextConfig';

/**
* Construct a record for the build output map.
Expand Down Expand Up @@ -61,6 +63,14 @@ export async function buildWorkerFile(

const outputFile = join(workerJsDir, 'index.js');

const nextConfig = await getNextConfig();

const buildMetadataConfig = nextConfig
? {
config: extractBuildMetadataConfig(nextConfig),
}
: {};

await build({
entryPoints: [join(templatesDir, '_worker.js')],
banner: {
Expand All @@ -76,22 +86,14 @@ export async function buildWorkerFile(
__NODE_ENV__: JSON.stringify(getNodeEnv()),
__BUILD_METADATA__: JSON.stringify({
collectedLocales: collectLocales(vercelConfig.routes),
...buildMetadataConfig,
}),
},
outfile: outputFile,
minify,
});

await build({
entryPoints: ['adaptor.ts', 'cache-api.ts', 'kv.ts'].map(fileName =>
join(templatesDir, 'cache', fileName),
),
bundle: false,
target: 'es2022',
platform: 'neutral',
outdir: join(nopDistDir, 'cache'),
minify,
});
await buildCacheFiles(nopDistDir, minify, templatesDir);

return relative('.', outputFile);
}
Expand Down
Loading
Loading