diff --git a/docs/integrations/cloudflare.md b/docs/integrations/cloudflare.md index 3acb16822..5b7a85030 100644 --- a/docs/integrations/cloudflare.md +++ b/docs/integrations/cloudflare.md @@ -52,6 +52,7 @@ This provides binding types and Workers runtime types. | `KV` | KV | `c.env.KV` or `void/kv` | | `STORAGE` | R2 | `c.env.STORAGE` or `void/storage` | | `AI` | Workers AI | `c.env.AI` or `void/ai` | +| `ASSETS` | Static assets | `c.env.ASSETS.fetch(request)` | | `QUEUE_*` | Queues | `defineQueue()` or `void/queues` | | Filename-derived | Durable Objects | A module in `durable-objects/` | diff --git a/docs/reference/api/env.md b/docs/reference/api/env.md index a329354eb..ba26e9f3d 100644 --- a/docs/reference/api/env.md +++ b/docs/reference/api/env.md @@ -45,6 +45,6 @@ Non-client keys are server values and use remote secret storage in production. ` /// ``` -Declares the `Cloudflare.Env` namespace with `DB`, `KV`, `STORAGE`, `AI`, and `SANDBOX` binding types. +Declares the `Cloudflare.Env` namespace with `DB`, `KV`, `STORAGE`, `AI`, `SANDBOX`, and `ASSETS` binding types. For handler context variables such as `c.set()` and `c.get()`, augment [`CloudContextVariables`](./types.md#cloudcontextvariables). This is separate from `Cloudflare.Env`, which types worker bindings. Framework adapters such as `@void/vue`, `@void/react`, and `@void/svelte` also augment `CloudContextVariables` to add the `shared` key used by `useShared()`. diff --git a/docs/reference/api/types.md b/docs/reference/api/types.md index b20f7691d..6444104f9 100644 --- a/docs/reference/api/types.md +++ b/docs/reference/api/types.md @@ -24,6 +24,7 @@ interface CloudEnv extends Env { STORAGE: R2Bucket; AI: Ai; SANDBOX: DurableObjectNamespace; + ASSETS: { fetch(request: Request): Promise }; [key: string]: unknown; }; Variables: CloudContextVariables; diff --git a/packages/void/src/runtime/env-public.ts b/packages/void/src/runtime/env-public.ts index 058c88380..dcb57b1f8 100644 --- a/packages/void/src/runtime/env-public.ts +++ b/packages/void/src/runtime/env-public.ts @@ -400,6 +400,7 @@ declare global { STORAGE: R2Bucket; AI: Ai; SANDBOX: DurableObjectNamespace; + ASSETS: Fetcher; /** @internal Service binding to proxy worker (AI, revalidation, future features) */ __VOID_PROXY?: Fetcher; /** @internal Custom proxy URL override */ diff --git a/packages/void/src/runtime/handler.ts b/packages/void/src/runtime/handler.ts index 9804e091b..01006ffc7 100644 --- a/packages/void/src/runtime/handler.ts +++ b/packages/void/src/runtime/handler.ts @@ -4,6 +4,7 @@ import type { HeadDescriptor } from '../pages/head.ts'; import type { AuthSession, AuthUser } from './auth.ts'; import { base64ToBytes } from './base64.ts'; import { convertReturnValue } from './response.ts'; +import type { AssetBinding } from './routing-worker.ts'; import { runValidation, type ValidatorSlots, type HandlerInput } from './validator.ts'; /** @@ -55,6 +56,8 @@ export interface CloudBindings extends VoidGeneratedEnvBindings { STORAGE: R2Bucket; AI: Ai; SANDBOX: DurableObjectNamespace; + /** Node, Bun and Deno targets serve static files through a provider that takes only a `Request`. */ + ASSETS: AssetBinding; [key: string]: unknown; } diff --git a/packages/void/test/integration/env-types-typecheck.test.ts b/packages/void/test/integration/env-types-typecheck.test.ts index 298b12b76..ddfa411b2 100644 --- a/packages/void/test/integration/env-types-typecheck.test.ts +++ b/packages/void/test/integration/env-types-typecheck.test.ts @@ -14,7 +14,14 @@ afterEach(() => { }); describe('generated env types in a consumer project', () => { - it('types schema keys on both env and handler c.env', () => { + // `wrangler types` declares an assets binding as `ASSETS: Fetcher`, which must merge with Void's. + it.each([ + ['', ''], + [ + ' next to wrangler types output', + 'declare global { namespace Cloudflare { interface Env { ASSETS: Fetcher } } }\n', + ], + ])('types schema keys and ASSETS on both env and handler c.env%s', (_, wranglerTypes) => { // This nested fixture resolves the workspace's installed void package just // as a consumer does, while keeping its source out of the playground routes. const root = mkdtempSync(join(workspaceRoot, 'playground/kitchen-sink/.tmp-env-types-')); @@ -33,7 +40,12 @@ describe('generated env types in a consumer project', () => { 'import { defineHandler } from "void";\n' + 'import { env } from "void/env";\n' + 'const direct: string = env.API_SECRET;\n' + - 'export const GET = defineHandler((c) => c.text(direct + c.env.API_SECRET.toUpperCase()));\n', + 'export const GET = defineHandler((c) => c.text(direct + c.env.API_SECRET.toUpperCase()));\n' + + 'export const POST = defineHandler((c) => c.env.ASSETS.fetch(c.req.raw));\n' + + '// @ts-expect-error Node, Bun and Deno targets fetch assets only by Request.\n' + + 'export const PUT = defineHandler((c) => c.env.ASSETS.fetch("/logo.png"));\n' + + 'export const fetchAsset = (bindings: Cloudflare.Env) => bindings.ASSETS.fetch("https://app.test/");\n' + + wranglerTypes, ); writeFileSync( join(root, 'tsconfig.json'),