Skip to content

Commit

Permalink
feat(deploy): add cloudflare context to platform object
Browse files Browse the repository at this point in the history
Example usage from a server component:

```ts
import { unstable_getPlatformObject } from 'waku/server';

const getData = async () => {
  const { env, executionCtx } = unstable_getPlatformObject() as { env: Env; executionCtx: ExecutionContext };
  executionCtx?.waitUntil(
    new Promise<void>((resolve) => {
      console.log("Waiting for 5 seconds");
      setTimeout(() => {
        console.log("OK, done waiting");
        resolve();
      }, 5000);
    }),
  );
  const { results } = await env.DB.prepare(
    "SELECT * FROM users WHERE user_id = ?",
  )
    .bind(userId)
    .all();
  return results;
};
```
  • Loading branch information
rmarscher committed Sep 14, 2024
1 parent 52973c8 commit bf95b66
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 16 deletions.
1 change: 1 addition & 0 deletions packages/waku/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"vite": "5.4.4"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240909.0",
"@netlify/functions": "^2.8.1",
"@swc/cli": "^0.4.0",
"rollup": "^4.21.3",
Expand Down
60 changes: 48 additions & 12 deletions packages/waku/src/lib/builder/serve-cloudflare.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,77 @@
import { Hono } from 'hono';
import { unstable_getPlatformObject } from 'waku/server';
import { runner } from '../hono/runner.js';
import type {
ExportedHandler,
fetch,
Request as CloudflareRequest,
Response as CloudflareResponse,
} from '@cloudflare/workers-types/experimental';

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

export interface CloudflareEnv {
ASSETS: {
fetch: (input: RequestInit | URL, init?: RequestInit) => Promise<Response>;
fetch: typeof fetch;
};
}

export const app = new Hono<{
Bindings: CloudflareEnv & { [k: string]: unknown };
}>();
app.use('*', (c, next) => serveWaku!(c, next));
app.use('*', (c, next) => {
if (!serveWaku) {
throw new Error('serveWaku is not initialized');
}
const platform = unstable_getPlatformObject();
platform.honoContext = c;
platform.cf = (c.req.raw as unknown as CloudflareRequest).cf;
platform.env = c.env;
platform.executionContext = c.executionCtx;
return serveWaku(c, next);
});
app.notFound(async (c) => {
const assetsFetcher = c.env.ASSETS;
const url = new URL(c.req.raw.url);
const errorHtmlUrl = `${url.origin}/404.html`;
const notFoundStaticAssetResponse = await assetsFetcher.fetch(
const notFoundStaticAssetResponse = (await assetsFetcher.fetch(
new URL(errorHtmlUrl),
);
)) as unknown as Response;
if (notFoundStaticAssetResponse && notFoundStaticAssetResponse.status < 400) {
return c.body(notFoundStaticAssetResponse.body, 404);
}
return c.text('404 Not Found', 404);
});

export default {
async fetch(
request: Request,
env: Record<string, string>,
ctx: Parameters<typeof app.fetch>[2],
) {
// Waku getEnv only supports strings
// Cloudflare injects bindings to env and JSON
// Use unstable_getPlatformObject() to access cloudflare env and execution context
// https://developers.cloudflare.com/workers/configuration/environment-variables/#add-environment-variables-via-wrangler
// https://developers.cloudflare.com/workers/runtime-apis/bindings/
const extractWakuEnv = (env: Record<string, unknown>): Record<string, string> =>
Object.fromEntries(
Object.entries(env).filter(([, value]) => typeof value === 'string'),
) as Record<string, string>;

const handler: ExportedHandler<CloudflareEnv & { [k: string]: never }> = {
async fetch(request, env, ctx) {
if (!serveWaku) {
serveWaku = runner({ cmd: 'start', loadEntries, env });
serveWaku = runner({
cmd: 'start',
loadEntries,
env: extractWakuEnv(env),
});
}
return app.fetch(request, env, ctx);
return app.fetch(
request as unknown as Request,
env,
ctx,
) as unknown as CloudflareResponse;
},
// It might be useful to have a way to populate other handlers tail, trace, scheduled, email, queue
// But it's not hard to create a separate worker with those handlers and access it via
// service bindings. https://developers.cloudflare.com/pages/functions/bindings/#service-bindings
};

export default handler;
13 changes: 9 additions & 4 deletions packages/waku/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,11 @@ export function unstable_getHeaders(): Record<string, string> {
>;
}

type PlatformObject = {
buildData?: Record<string, unknown>; // must be JSON serializable
type PlatformObject<
T = Record<string, unknown>,
BuildData = Record<string, unknown>,
> = {
buildData?: BuildData; // must be JSON serializable
buildOptions?: {
deploy?:
| 'vercel-static'
Expand All @@ -167,11 +170,13 @@ type PlatformObject = {
| 'buildClientBundle'
| 'buildDeploy';
};
} & Record<string, unknown>;
} & T;

(globalThis as any).__WAKU_PLATFORM_OBJECT__ ||= {};

// TODO tentative name
export function unstable_getPlatformObject(): PlatformObject {
export function unstable_getPlatformObject<
T = Record<string, unknown>,
>(): PlatformObject<T> {
return (globalThis as any).__WAKU_PLATFORM_OBJECT__;
}
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit bf95b66

Please sign in to comment.