Skip to content

Commit

Permalink
Add Cloudflare env and 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 as CloudflareEnv, executionCtx as ExecutionContext } = unstable_getPlatformObject();
  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;
};
```

Server console output:

```
Waiting for 5 seconds
[wrangler:inf] GET /about 200 OK (30ms)
[wrangler:inf] GET /assets/jsx-runtime-BjG_zV1W.js 304 Not Modified (6ms)
[wrangler:inf] GET /assets/index-CbskofAj.js 304 Not Modified (7ms)
[wrangler:inf] GET /assets/_layout-Shb4QlRw.css 304 Not Modified (9ms)
[wrangler:inf] GET /assets/rsc2-c69df7b3e.js 200 OK (11ms)
[wrangler:inf] GET /assets/client-kczTGGZ_.js 304 Not Modified (16ms)
[wrangler:inf] GET /assets/indexHtml-DCalQi_d.js 304 Not Modified (18ms)
[wrangler:inf] GET /assets/client-CMyJdxTj.js 304 Not Modified (21ms)
[wrangler:inf] GET /assets/rsc0-ba005381c.js 304 Not Modified (4ms)
[wrangler:inf] GET /images/favicon.png 304 Not Modified (3ms)
OK, done waiting
```

It is necessary to cast the Cloudflare types for now.
See https://developers.cloudflare.com/workers/languages/typescript/#generate-types-that-match-your-workers-configuration-experimental
for info on generating types for your project.
  • Loading branch information
rmarscher committed Sep 14, 2024
1 parent 52973c8 commit 9d9d5a7
Show file tree
Hide file tree
Showing 4 changed files with 62 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
56 changes: 44 additions & 12 deletions packages/waku/src/lib/builder/serve-cloudflare.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,73 @@
import { Hono } from 'hono';
import { unstable_getPlatformObject } from 'waku/server';
import { runner } from '../hono/runner.js';
import type {
ExportedHandler,
fetch,
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.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 like scheduled, cron, etc.
};

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 9d9d5a7

Please sign in to comment.