Skip to content

Commit cc9d071

Browse files
make site work with the Cloudflare OpenNext adapter
update the site application so that it can be build using the Cloudflare OpenNext adapter (`@opennextjs/cloudflare`) and thus deployed on Cloudflare Workers > [!Note] > This is very much a work-in-progress right now
1 parent 1b2acc4 commit cc9d071

24 files changed

Lines changed: 17996 additions & 9996 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ cache
3232
tsconfig.tsbuildinfo
3333

3434
dist/
35+
36+
# Ignore worker artifacts
37+
apps/site/.open-next
38+
apps/site/.wrangler
39+
apps/site/.cloudflare/.asset-manifests

apps/site/.cloudflare/node/fs.mjs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import fsPromises from 'fs/promises';
2+
3+
import pagesManifest from '../.asset-manifests/pages.mjs';
4+
import snippetsManifest from '../.asset-manifests/snippets.mjs';
5+
6+
export function readdir(path, options, cb) {
7+
const withFileTypes = !!options.withFileTypes;
8+
9+
if (!withFileTypes) {
10+
// TODO: also support withFileTypes false
11+
throw new Error('fs#readdir please call readdir with withFileTypes true');
12+
}
13+
14+
const result = findInDirentLikes(path);
15+
16+
const results =
17+
!result || result.type !== 'directory'
18+
? []
19+
: result.children.map(c => ({
20+
name: c.name,
21+
parentPath: c.parentPath,
22+
path: c.path,
23+
isFile: () => c.type === 'file',
24+
isDirectory: () => c.type === 'directory',
25+
}));
26+
27+
cb?.(null, results);
28+
}
29+
30+
function findInDirentLikes(path) {
31+
if (!path.startsWith('/pages') && !path.startsWith('/snippets')) {
32+
return null;
33+
}
34+
35+
// remove the leading `/`
36+
path = path.slice(1);
37+
38+
const paths = path.split('/');
39+
40+
const manifestType = paths.shift();
41+
42+
const manifest = manifestType === 'pages' ? pagesManifest : snippetsManifest;
43+
44+
return recursivelyFindInDirentLikes(paths, manifest);
45+
function recursivelyFindInDirentLikes(paths, direntLikes) {
46+
const [current, ...restOfPaths] = paths;
47+
const found = direntLikes.find(item => item.name === current);
48+
if (!found) return null;
49+
if (restOfPaths.length === 0) return found;
50+
if (found.type !== 'directory') return null;
51+
return recursivelyFindInDirentLikes(restOfPaths, found.children);
52+
}
53+
}
54+
55+
export function exists(path, cb) {
56+
const result = existsImpl(path);
57+
cb(result);
58+
}
59+
60+
export function existsSync(path) {
61+
const result = existsImpl(path);
62+
return result;
63+
}
64+
65+
function existsImpl(path) {
66+
if (!path.startsWith('/pages') && !path.startsWith('/snippets')) {
67+
return false;
68+
}
69+
return !!findInDirentLikes(path);
70+
}
71+
72+
export function realpathSync() {
73+
return true;
74+
}
75+
76+
const cloudflareContextSymbol = Symbol.for('__cloudflare-context__');
77+
78+
export function createReadStream(path) {
79+
const { env } = global[cloudflareContextSymbol];
80+
// Note: we only care about the url's path, the domain is not relevant here
81+
const url = new URL(`/${path}`, 'http://0.0.0.0');
82+
return env.ASSETS.fetch(url);
83+
}
84+
85+
export default {
86+
readdir,
87+
exists,
88+
existsSync,
89+
realpathSync,
90+
promises: fsPromises,
91+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const cloudflareContextSymbol = Symbol.for('__cloudflare-context__');
2+
3+
export async function readFile(path) {
4+
const { env } = global[cloudflareContextSymbol];
5+
6+
// Note: we only care about the url's path, the domain is not relevant here
7+
const url = new URL(`/${path}`, 'http://0.0.0.0');
8+
const response = await env.ASSETS.fetch(url);
9+
const text = await response.text();
10+
return text;
11+
}
12+
13+
export async function readdir() {
14+
return [];
15+
}
16+
17+
export async function exists() {
18+
return false;
19+
}
20+
21+
export default {
22+
readdir,
23+
exists,
24+
readFile,
25+
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
export function createInterface({ input }: { input: Promise<Response> }) {
2+
const resp = input.then(resp => resp.body!.getReader());
3+
let closed = false;
4+
let closeCallback: (...args: Array<unknown>) => void;
5+
6+
return {
7+
on: (
8+
event: 'line' | 'close' | string,
9+
callback: (...args: Array<unknown>) => void
10+
) => {
11+
if (event !== 'line' && event !== 'close') {
12+
throw new Error(
13+
`readline interface \`on\`, wrong event provided: ${event}`
14+
);
15+
}
16+
17+
if (event === 'line') {
18+
const textDecoder = new TextDecoder();
19+
let text = '';
20+
const lineCallback = (...args: Array<unknown>) => {
21+
if (!closed) {
22+
callback?.(...args);
23+
}
24+
};
25+
26+
const emitLines = (done = false) => {
27+
const newLineIdx = text.indexOf('\n');
28+
if (newLineIdx === -1) {
29+
if (done) {
30+
lineCallback(text);
31+
}
32+
return;
33+
}
34+
const toEmit = text.slice(0, newLineIdx);
35+
lineCallback(toEmit);
36+
text = text.slice(newLineIdx + 1);
37+
emitLines();
38+
};
39+
40+
const read = () => {
41+
resp.then(s => {
42+
s.read().then(({ done, value }) => {
43+
text += textDecoder.decode(value);
44+
emitLines();
45+
46+
if (!done) {
47+
read();
48+
} else {
49+
closeCallback?.();
50+
}
51+
});
52+
});
53+
};
54+
55+
return read();
56+
}
57+
58+
if (event === 'close') {
59+
closeCallback = callback;
60+
return;
61+
}
62+
},
63+
close: () => {
64+
closed = true;
65+
closeCallback?.();
66+
},
67+
};
68+
}
69+
70+
export default {
71+
createInterface,
72+
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//@ts-check
2+
3+
import nodeFs from 'node:fs/promises';
4+
import nodePath from 'node:path';
5+
6+
await collectAndCopyDirToAssets('./pages');
7+
await collectAndCopyDirToAssets('./snippets');
8+
9+
/**
10+
* @param {string} path
11+
* @returns {Promise<void>}
12+
*/
13+
async function collectAndCopyDirToAssets(path) {
14+
await nodeFs.cp(path, nodePath.join('./.open-next/assets', path), {
15+
recursive: true,
16+
force: true,
17+
});
18+
19+
const pagesChildren = await collectDirChildren(path);
20+
await nodeFs.mkdir('./.cloudflare/.asset-manifests/', { recursive: true });
21+
await nodeFs.writeFile(
22+
`./.cloudflare/.asset-manifests/${nodePath.basename(path)}.mjs`,
23+
`export default ${JSON.stringify(pagesChildren)}`
24+
);
25+
}
26+
27+
/**
28+
* @param {string} path
29+
* @returns {Promise<DirentLike[]>}
30+
*/
31+
async function collectDirChildren(path) {
32+
const dirContent = await nodeFs.readdir(path, { withFileTypes: true });
33+
34+
return Promise.all(
35+
dirContent.map(async item => {
36+
const base = {
37+
name: item.name,
38+
parentPath: item.parentPath,
39+
};
40+
if (item.isFile()) {
41+
return { ...base, type: 'file' };
42+
} else {
43+
const dirInfo = await collectDirChildren(
44+
`${item.parentPath}/${item.name}`
45+
);
46+
return { ...base, type: 'directory', children: dirInfo };
47+
}
48+
})
49+
);
50+
}
51+
52+
/**
53+
* @typedef {{ name: string, parentPath: string } } DirentLikeBase
54+
* @typedef {DirentLikeBase & { type: 'file' }} DirentLikeFile
55+
* @typedef {DirentLikeBase & { type: 'directory', children: DirentLike[] }} DirentLikeDir
56+
* @typedef {DirentLikeFile|DirentLikeDir} DirentLike
57+
*/

apps/site/app/[locale]/feed/[feed]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const GET = async (_: Request, props: StaticParams) => {
1414
const params = await props.params;
1515

1616
// Generate the Feed for the given feed type (blog, releases, etc)
17-
const websiteFeed = provideWebsiteFeeds(params.feed);
17+
const websiteFeed = await provideWebsiteFeeds(params.feed);
1818

1919
return new NextResponse(websiteFeed, {
2020
headers: { 'Content-Type': 'application/xml' },

apps/site/app/[locale]/next-data/api-data/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const getPathnameForApiFile = (name: string, version: string) =>
2121
// for a digest and metadata of all API pages from the Node.js Website
2222
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
2323
export const GET = async () => {
24-
const releases = provideReleaseData();
24+
const releases = await provideReleaseData();
2525

2626
const { versionWithPrefix } = releases.find(
2727
release => release.status === 'LTS'

apps/site/app/[locale]/next-data/blog-data/[category]/[page]/route.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@ export const GET = async (_: Request, props: StaticParams) => {
2020

2121
const requestedPage = Number(params.page);
2222

23-
const data =
24-
requestedPage >= 1
25-
? // This allows us to blindly get all blog posts from a given category
26-
// if the page number is 0 or something smaller than 1
27-
providePaginatedBlogPosts(params.category, requestedPage)
28-
: provideBlogPosts(params.category);
23+
const data = await (requestedPage >= 1
24+
? // This allows us to blindly get all blog posts from a given category
25+
// if the page number is 0 or something smaller than 1
26+
providePaginatedBlogPosts(params.category, requestedPage)
27+
: provideBlogPosts(params.category));
2928

3029
return Response.json(data, { status: data.posts.length ? 200 : 404 });
3130
};

apps/site/app/[locale]/next-data/download-snippets/route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const GET = async (_: Request, props: StaticParams) => {
1111
const params = await props.params;
1212

1313
// Retrieve all available Download snippets for a given locale if available
14-
const snippets = provideDownloadSnippets(params.locale);
14+
const snippets = await provideDownloadSnippets(params.locale);
1515

1616
// We append always the default/fallback snippets when a result is found
1717
return Response.json(snippets, {

apps/site/app/[locale]/next-data/release-data/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { defaultLocale } from '@/next.locales.mjs';
55
// for generating static data related to the Node.js Release Data
66
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
77
export const GET = async () => {
8-
const releaseData = provideReleaseData();
8+
const releaseData = await provideReleaseData();
99

1010
return Response.json(releaseData);
1111
};

0 commit comments

Comments
 (0)