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

feat: cloudflare pages adapter, alternate implementation #795

Merged
merged 3 commits into from
Jul 25, 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
96 changes: 88 additions & 8 deletions packages/waku/src/lib/builder/output-cloudflare.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,107 @@
import path from 'node:path';
import { existsSync, writeFileSync } from 'node:fs';

import {
existsSync,
readdirSync,
writeFileSync,
mkdirSync,
renameSync,
rmSync,
} from 'node:fs';
import type { ResolvedConfig } from '../config.js';
import { DIST_PUBLIC } from './constants.js';

// XXX this can be very limited. FIXME if anyone has better knowledge.
const WORKER_JS_NAME = '_worker.js';
const ROUTES_JSON_NAME = '_routes.json';

type StaticRoutes = { version: number; include: string[]; exclude: string[] };

export const emitCloudflareOutput = async (
rootDir: string,
config: ResolvedConfig,
serveJs: string,
) => {
const outDir = path.join(rootDir, config.distDir);

// Advanced-mode Cloudflare Pages imports _worker.js
// and can be configured with _routes.json to serve other static root files
mkdirSync(path.join(outDir, WORKER_JS_NAME));
const outPaths = readdirSync(outDir);
for (const p of outPaths) {
if (p === WORKER_JS_NAME) {
continue;
}
renameSync(path.join(outDir, p), path.join(outDir, WORKER_JS_NAME, p));
}

const workerEntrypoint = path.join(outDir, WORKER_JS_NAME, 'index.js');
if (!existsSync(workerEntrypoint)) {
writeFileSync(
workerEntrypoint,
`
import server from './${serveJs}'

export default {
...server
}
`,
);
}

// Create _routes.json if one doesn't already exist in the public dir
// https://developers.cloudflare.com/pages/functions/routing/#functions-invocation-routes
const routesFile = path.join(outDir, ROUTES_JSON_NAME);
const publicDir = path.join(outDir, WORKER_JS_NAME, DIST_PUBLIC);
if (!existsSync(path.join(publicDir, ROUTES_JSON_NAME))) {
const staticPaths: string[] = [];
const paths = readdirSync(publicDir, {
withFileTypes: true,
});
for (const p of paths) {
if (p.isDirectory()) {
const entry = `/${p.name}/*`;
if (!staticPaths.includes(entry)) {
staticPaths.push(entry);
}
} else {
if (p.name === WORKER_JS_NAME) {
return;
}
staticPaths.push(`/${p.name}`);
}
}
const staticRoutes: StaticRoutes = {
version: 1,
include: ['/*'],
exclude: staticPaths,
};
writeFileSync(routesFile, JSON.stringify(staticRoutes));
}

// Move the public files to the root of the dist folder
const publicPaths = readdirSync(
path.join(outDir, WORKER_JS_NAME, DIST_PUBLIC),
);
for (const p of publicPaths) {
renameSync(
path.join(outDir, WORKER_JS_NAME, DIST_PUBLIC, p),
path.join(outDir, p),
);
}
rmSync(path.join(outDir, WORKER_JS_NAME, DIST_PUBLIC), {
recursive: true,
force: true,
});

const wranglerTomlFile = path.join(rootDir, 'wrangler.toml');
if (!existsSync(wranglerTomlFile)) {
writeFileSync(
wranglerTomlFile,
`
# See https://developers.cloudflare.com/pages/functions/wrangler-configuration/
name = "waku-project"
main = "${config.distDir}/${serveJs}"
compatibility_date = "2023-12-06"
compatibility_date = "2024-04-03"
compatibility_flags = [ "nodejs_als" ]

[site]
bucket = "./${config.distDir}/${DIST_PUBLIC}"
pages_build_output_dir = "./dist"
`,
);
}
Expand Down
32 changes: 16 additions & 16 deletions packages/waku/src/lib/builder/serve-cloudflare.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import { Hono } from 'hono';
import { serveStatic } from 'hono/cloudflare-workers';
// @ts-expect-error no types
// eslint-disable-next-line import/no-unresolved
import manifest from '__STATIC_CONTENT_MANIFEST';

import { runner } from '../hono/runner.js';

const loadEntries = () => import(import.meta.env.WAKU_ENTRIES_FILE!);
let serveWaku: ReturnType<typeof runner> | undefined;
let staticContent: any;

const parsedManifest: Record<string, string> = JSON.parse(manifest);
export interface CloudflareEnv {
ASSETS: {
fetch: (input: RequestInit | URL, init?: RequestInit) => Promise<Response>;
};
}

const app = new Hono();
app.use('*', serveStatic({ root: './', manifest }));
export const app = new Hono<{
Bindings: CloudflareEnv & { [k: string]: unknown };
}>();
app.use('*', (c, next) => serveWaku!(c, next));
app.notFound(async (c) => {
const path = parsedManifest['404.html'];
const content: ArrayBuffer | undefined =
path && (await staticContent?.get(path, { type: 'arrayBuffer' }));
if (content) {
c.header('Content-Type', 'text/html; charset=utf-8');
return c.body(content, 404);
const assetsFetcher = c.env.ASSETS;
const url = new URL(c.req.raw.url);
const errorHtmlUrl = `${url.origin}/404.html`;
const notFoundStaticAssetResponse = await assetsFetcher.fetch(
new URL(errorHtmlUrl),
);
if (notFoundStaticAssetResponse && notFoundStaticAssetResponse.status < 400) {
return c.body(notFoundStaticAssetResponse.body, 404);
}
return c.text('404 Not Found', 404);
});
Expand All @@ -34,7 +35,6 @@ export default {
) {
if (!serveWaku) {
serveWaku = runner({ cmd: 'start', loadEntries, env });
staticContent = env.__STATIC_CONTENT;
}
return app.fetch(request, env, ctx);
},
Expand Down
8 changes: 1 addition & 7 deletions packages/waku/src/lib/plugins/vite-plugin-rsc-serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,12 @@ export function rscServePlugin(opts: {
opts.distPublic,
),
};
if (opts.serve === 'cloudflare' || opts.serve === 'partykit') {
if (opts.serve === 'partykit') {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@threepointone you might be interested in this PR, as you followed the previous cloudflare impl?

viteConfig.build ||= {};
viteConfig.build.rollupOptions ||= {};
viteConfig.build.rollupOptions.external ||= [];
if (Array.isArray(viteConfig.build.rollupOptions.external)) {
viteConfig.build.rollupOptions.external.push('hono');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't hono still need to be externalised here for the CFP build? 🤔

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let us know if we need it and we can have a follow-up PR.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be fine without (didn't experience any build or dev issues without it)

if (opts.serve === 'cloudflare') {
viteConfig.build.rollupOptions.external.push(
'hono/cloudflare-workers',
'__STATIC_CONTENT_MANIFEST',
);
}
} else {
throw new Error(
'Unsupported: build.rollupOptions.external is not an array',
Expand Down
Loading