-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ssg support through seeding incremental cache
- Loading branch information
1 parent
bcaaec6
commit f60dbc3
Showing
12 changed files
with
275 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { NEXT_META_SUFFIX, SEED_DATA_DIR } from "../../cache-handler"; | ||
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; | ||
import { dirname, join } from "node:path"; | ||
import { Config } from "../../config"; | ||
import type { PrerenderManifest } from "next/dist/build"; | ||
import { readPathsRecursively } from "./read-paths-recursively"; | ||
|
||
/** | ||
* Copies all prerendered routes from the standalone output directory to the OpenNext static assets | ||
* output directory. | ||
* | ||
* Updates metadata configs with the current time as a modified date, so that it can be re-used in | ||
* the incremental cache to determine whether an entry is _fresh_ or not. | ||
* | ||
* @param config Build config. | ||
*/ | ||
export function copyPrerenderedRoutes(config: Config) { | ||
console.log("# copyPrerenderedRoutes"); | ||
|
||
const serverAppDirPath = join(config.paths.standaloneAppServer, "app"); | ||
const prerenderManifestPath = join(config.paths.standaloneAppDotNext, "prerender-manifest.json"); | ||
const outputPath = join(config.paths.builderOutput, "assets", SEED_DATA_DIR); | ||
|
||
const prerenderManifest: PrerenderManifest = existsSync(prerenderManifestPath) | ||
? JSON.parse(readFileSync(prerenderManifestPath, "utf8")) | ||
: {}; | ||
const prerenderedRoutes = Object.keys(prerenderManifest.routes); | ||
|
||
const prerenderedAssets = readPathsRecursively(serverAppDirPath) | ||
.map((fullPath) => ({ fullPath, relativePath: fullPath.replace(serverAppDirPath, "") })) | ||
.filter(({ relativePath }) => | ||
prerenderedRoutes.includes(relativePath.replace(/\.\w+$/, "").replace(/^\/index$/, "/")) | ||
); | ||
|
||
prerenderedAssets.forEach(({ fullPath, relativePath }) => { | ||
const destPath = join(outputPath, relativePath); | ||
mkdirSync(dirname(destPath), { recursive: true }); | ||
|
||
if (fullPath.endsWith(NEXT_META_SUFFIX)) { | ||
const data = JSON.parse(readFileSync(fullPath, "utf8")); | ||
writeFileSync(destPath, JSON.stringify({ ...data, lastModified: Date.now() })); | ||
} else { | ||
copyFileSync(fullPath, destPath); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from "./ts-parse-file"; | ||
export * from "./copy-prerendered-routes"; |
23 changes: 23 additions & 0 deletions
23
packages/cloudflare/src/cli/build/utils/read-paths-recursively.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { join } from "node:path"; | ||
import { readdirSync } from "node:fs"; | ||
|
||
/** | ||
* Recursively reads all file paths in a directory. | ||
* | ||
* @param dir Directory to recursively read from. | ||
* @returns Array of all paths for all files in a directory. | ||
*/ | ||
export function readPathsRecursively(dir: string): string[] { | ||
try { | ||
const files = readdirSync(dir, { withFileTypes: true }); | ||
|
||
const paths = files.map((file) => { | ||
const filePath = join(dir, file.name); | ||
return file.isDirectory() ? readPathsRecursively(filePath) : [filePath]; | ||
}); | ||
|
||
return paths.flat(); | ||
} catch { | ||
return []; | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export const RSC_PREFETCH_SUFFIX = ".prefetch.rsc"; | ||
export const RSC_SUFFIX = ".rsc"; | ||
export const NEXT_DATA_SUFFIX = ".json"; | ||
export const NEXT_META_SUFFIX = ".meta"; | ||
export const NEXT_BODY_SUFFIX = ".body"; | ||
export const NEXT_HTML_SUFFIX = ".html"; | ||
|
||
export const SEED_DATA_DIR = "_cf_seed_data"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./constants"; | ||
export * from "./open-next-cache-handler"; |
146 changes: 146 additions & 0 deletions
146
packages/cloudflare/src/cli/cache-handler/open-next-cache-handler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import type { | ||
CacheHandler, | ||
CacheHandlerContext, | ||
CacheHandlerValue, | ||
} from "next/dist/server/lib/incremental-cache"; | ||
import { | ||
NEXT_BODY_SUFFIX, | ||
NEXT_DATA_SUFFIX, | ||
NEXT_HTML_SUFFIX, | ||
RSC_PREFETCH_SUFFIX, | ||
RSC_SUFFIX, | ||
SEED_DATA_DIR, | ||
} from "./constants"; | ||
import { getSeedBodyFile, getSeedMetaFile, getSeedTextFile, parseCtx } from "./utils"; | ||
import type { IncrementalCacheValue } from "next/dist/server/response-cache"; | ||
import { KVNamespace } from "@cloudflare/workers-types"; | ||
|
||
type CacheEntry = { | ||
lastModified: number; | ||
value: IncrementalCacheValue | null; | ||
}; | ||
|
||
export class OpenNextCacheHandler implements CacheHandler { | ||
static maybeKVNamespace: KVNamespace | undefined = undefined; | ||
|
||
protected debug: boolean = !!process.env.NEXT_PRIVATE_DEBUG_CACHE; | ||
|
||
constructor(protected ctx: CacheHandlerContext) {} | ||
|
||
async get(...args: Parameters<CacheHandler["get"]>): Promise<CacheHandlerValue | null> { | ||
const [key, _ctx] = args; | ||
const ctx = parseCtx(_ctx); | ||
|
||
if (this.debug) console.log(`cache - get: ${key}, ${ctx?.kind}`); | ||
|
||
if (OpenNextCacheHandler.maybeKVNamespace !== undefined) { | ||
try { | ||
const value = await OpenNextCacheHandler.maybeKVNamespace.get<CacheEntry>(key, "json"); | ||
if (value) return value; | ||
} catch (e) { | ||
console.error(`Failed to get value for key = ${key}: ${e}`); | ||
} | ||
} | ||
|
||
// Check for seed data from the file-system. | ||
|
||
// we don't check for seed data for fetch or image cache entries | ||
if (ctx?.kind === "FETCH" || ctx?.kind === "IMAGE") return null; | ||
|
||
const seedKey = `http://assets.local/${SEED_DATA_DIR}/${key}`.replace(/\/\//g, "/"); | ||
|
||
if (ctx?.kind === "APP" || ctx?.kind === "APP_ROUTE") { | ||
const fallbackBody = await getSeedBodyFile(seedKey, NEXT_BODY_SUFFIX); | ||
if (fallbackBody) { | ||
const meta = await getSeedMetaFile(seedKey); | ||
return { | ||
lastModified: meta?.lastModified, | ||
value: { | ||
kind: (ctx.kind === "APP_ROUTE" ? ctx.kind : "ROUTE") as Extract< | ||
IncrementalCacheValue["kind"], | ||
"ROUTE" | ||
>, | ||
body: fallbackBody, | ||
status: meta?.status ?? 200, | ||
headers: meta?.headers ?? {}, | ||
}, | ||
}; | ||
} | ||
|
||
if (ctx.kind === "APP_ROUTE") { | ||
return null; | ||
} | ||
} | ||
|
||
const seedHtml = await getSeedTextFile(seedKey, NEXT_HTML_SUFFIX); | ||
if (!seedHtml) return null; // we're only checking for prerendered routes at the moment | ||
|
||
if (ctx?.kind === "PAGES" || ctx?.kind === "APP" || ctx?.kind === "APP_PAGE") { | ||
const metaPromise = getSeedMetaFile(seedKey); | ||
|
||
let pageDataPromise: Promise<Buffer | string | undefined> = Promise.resolve(undefined); | ||
if (!ctx.isFallback) { | ||
const rscSuffix = ctx.isRoutePPREnabled ? RSC_PREFETCH_SUFFIX : RSC_SUFFIX; | ||
|
||
if (ctx.kind === "APP_PAGE") { | ||
pageDataPromise = getSeedBodyFile(seedKey, rscSuffix); | ||
} else { | ||
pageDataPromise = getSeedTextFile(seedKey, ctx.kind === "APP" ? rscSuffix : NEXT_DATA_SUFFIX); | ||
} | ||
} | ||
|
||
const [meta, pageData] = await Promise.all([metaPromise, pageDataPromise]); | ||
|
||
return { | ||
lastModified: meta?.lastModified, | ||
value: { | ||
kind: (ctx.kind === "APP_PAGE" ? "APP_PAGE" : "PAGE") as Extract< | ||
IncrementalCacheValue["kind"], | ||
"PAGE" | ||
>, | ||
html: seedHtml, | ||
pageData: pageData ?? "", | ||
...(ctx.kind === "APP_PAGE" && { rscData: pageData }), | ||
postponed: meta?.postponed, | ||
status: meta?.status, | ||
headers: meta?.headers, | ||
}, | ||
}; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
async set(...args: Parameters<CacheHandler["set"]>) { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const [key, entry, _ctx] = args; | ||
|
||
if (OpenNextCacheHandler.maybeKVNamespace === undefined) { | ||
return; | ||
} | ||
|
||
if (this.debug) console.log(`cache - set: ${key}`); | ||
|
||
const data: CacheEntry = { | ||
lastModified: Date.now(), | ||
value: entry, | ||
}; | ||
|
||
try { | ||
await OpenNextCacheHandler.maybeKVNamespace.put(key, JSON.stringify(data)); | ||
} catch (e) { | ||
console.error(`Failed to set value for key = ${key}: ${e}`); | ||
} | ||
} | ||
|
||
async revalidateTag(...args: Parameters<CacheHandler["revalidateTag"]>) { | ||
const [tags] = args; | ||
if (OpenNextCacheHandler.maybeKVNamespace === undefined) { | ||
return; | ||
} | ||
|
||
if (this.debug) console.log(`cache - revalidateTag: ${JSON.stringify([tags].flat())}`); | ||
} | ||
|
||
resetRequestCache(): void {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { IncrementalCache } from "next/dist/server/lib/incremental-cache"; | ||
import { NEXT_META_SUFFIX } from "./constants"; | ||
|
||
type PrerenderedRouteMeta = { | ||
lastModified: number; | ||
status?: number; | ||
headers?: Record<string, string>; | ||
postponed?: string; | ||
}; | ||
|
||
type EntryKind = | ||
| "APP" // .body, .html - backwards compat | ||
| "PAGES" | ||
| "FETCH" | ||
| "APP_ROUTE" // .body | ||
| "APP_PAGE" // .html | ||
| "IMAGE" | ||
| undefined; | ||
|
||
async function getAsset<T>(key: string, cb: (resp: Response) => T): Promise<Awaited<T> | undefined> { | ||
const resp = await process.env.ASSETS.fetch(key); | ||
return resp.status === 200 ? await cb(resp) : undefined; | ||
} | ||
|
||
export function getSeedBodyFile(key: string, suffix: string) { | ||
return getAsset(`${key}${suffix}`, (resp) => resp.arrayBuffer() as Promise<Buffer>); | ||
} | ||
|
||
export function getSeedTextFile(key: string, suffix: string) { | ||
return getAsset(`${key}${suffix}`, (resp) => resp.text()); | ||
} | ||
|
||
export function getSeedMetaFile(key: string) { | ||
return getAsset(`${key}${NEXT_META_SUFFIX}`, (resp) => resp.json<PrerenderedRouteMeta>()); | ||
} | ||
|
||
export function parseCtx(ctx: Parameters<IncrementalCache["get"]>[1] = {}) { | ||
return { ...ctx, kind: ctx?.kindHint?.toUpperCase() } as | ||
| (typeof ctx & { kind?: EntryKind; isFallback?: boolean; isRoutePPREnabled?: boolean }) | ||
| undefined; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters