Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
19 changes: 19 additions & 0 deletions .changeset/khaki-plums-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"wrangler": minor
---

This adds support for more accurate types for service bindings when running `wrangler types`. Previously, running `wrangler types` with a config including a service binding would generate an `Env` type like this:

```ts
interface Env {
SERVICE_BINDING: Fetcher
}
```

This type was "correct", but didn't capture the possibility of using JSRPC to communicate with the service binding. Now, running `wrangler types -c wrangler.json -c ../service/wrangler.json` (the first config representing the current Worker, and any additional configs representing service bound Workers) will generate an `Env` type like this:

```ts
interface Env {
SERVICE_BINDING: Service<import("../service/src/index").Entrypoint>;
}
```
190 changes: 185 additions & 5 deletions packages/wrangler/src/__tests__/type-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ const bindingsConfigMock: Omit<
class_name: "DurableExternal",
script_name: "external-worker",
},
{
name: "REAL_DURABLE_EXTERNAL",
class_name: "RealDurableExternal",
script_name: "service_name_2",
},
],
},
workflows: [],
Expand All @@ -150,7 +155,19 @@ const bindingsConfigMock: Omit<
secret_name: "secret_name",
},
],
services: [{ binding: "SERVICE_BINDING", service: "SERVICE_NAME" }],
services: [
{ binding: "SERVICE_BINDING", service: "service_name" },
{
binding: "OTHER_SERVICE_BINDING",
service: "service_name_2",
entrypoint: "FakeEntrypoint",
},
{
binding: "OTHER_SERVICE_BINDING_ENTRYPOINT",
service: "service_name_2",
entrypoint: "RealEntrypoint",
},
],
analytics_engine_datasets: [
{
binding: "AE_DATASET_BINDING",
Expand Down Expand Up @@ -402,7 +419,7 @@ describe("generate types", () => {
name: "test-name",
main: "./index.ts",
...bindingsConfigMock,
unsafe: bindingsConfigMock.unsafe ?? {},
unsafe: bindingsConfigMock.unsafe,
} as unknown as TOML.JsonMap),
"utf-8"
);
Expand All @@ -422,10 +439,13 @@ describe("generate types", () => {
DURABLE_RE_EXPORT: DurableObjectNamespace<import(\\"./index\\").DurableReexport>;
DURABLE_NO_EXPORT: DurableObjectNamespace /* DurableNoexport */;
DURABLE_EXTERNAL: DurableObjectNamespace /* DurableExternal from external-worker */;
REAL_DURABLE_EXTERNAL: DurableObjectNamespace /* RealDurableExternal from service_name_2 */;
R2_BUCKET_BINDING: R2Bucket;
D1_TESTING_SOMETHING: D1Database;
SECRET: SecretsStoreSecret;
SERVICE_BINDING: Fetcher;
SERVICE_BINDING: Fetcher /* service_name */;
OTHER_SERVICE_BINDING: Service /* entrypoint FakeEntrypoint from service_name_2 */;
OTHER_SERVICE_BINDING_ENTRYPOINT: Service /* entrypoint RealEntrypoint from service_name_2 */;
AE_DATASET_BINDING: AnalyticsEngineDataset;
NAMESPACE_BINDING: DispatchNamespace;
LOGFWDR_SCHEMA: any;
Expand Down Expand Up @@ -490,7 +510,7 @@ describe("generate types", () => {
name: "test-name",
main: "./index.ts",
...bindingsConfigMock,
unsafe: bindingsConfigMock.unsafe ?? {},
unsafe: bindingsConfigMock.unsafe,
} as unknown as TOML.JsonMap),
"utf-8"
);
Expand All @@ -512,10 +532,13 @@ describe("generate types", () => {
DURABLE_RE_EXPORT: DurableObjectNamespace<import(\\"./index\\").DurableReexport>;
DURABLE_NO_EXPORT: DurableObjectNamespace /* DurableNoexport */;
DURABLE_EXTERNAL: DurableObjectNamespace /* DurableExternal from external-worker */;
REAL_DURABLE_EXTERNAL: DurableObjectNamespace /* RealDurableExternal from service_name_2 */;
R2_BUCKET_BINDING: R2Bucket;
D1_TESTING_SOMETHING: D1Database;
SECRET: SecretsStoreSecret;
SERVICE_BINDING: Fetcher;
SERVICE_BINDING: Fetcher /* service_name */;
OTHER_SERVICE_BINDING: Service /* entrypoint FakeEntrypoint from service_name_2 */;
OTHER_SERVICE_BINDING_ENTRYPOINT: Service /* entrypoint RealEntrypoint from service_name_2 */;
AE_DATASET_BINDING: AnalyticsEngineDataset;
NAMESPACE_BINDING: DispatchNamespace;
LOGFWDR_SCHEMA: any;
Expand Down Expand Up @@ -565,6 +588,163 @@ describe("generate types", () => {
`);
});

it("should handle multiple worker configs", async () => {
fs.mkdirSync("a");

fs.writeFileSync(
"./a/index.ts",
`import { DurableObject } from 'cloudflare:workers';
export default { async fetch () {} };
export class DurableDirect extends DurableObject {}`
);
fs.writeFileSync(
"./a/wrangler.toml",
TOML.stringify({
compatibility_date: "2022-01-12",
compatibility_flags: [
"nodejs_compat",
"nodejs_compat_populate_process_env",
],
name: "test-name",
main: "./index.ts",
...bindingsConfigMock,
unsafe: bindingsConfigMock.unsafe,
} as unknown as TOML.JsonMap),
"utf-8"
);
fs.writeFileSync("./a/.dev.vars", "SECRET=test", "utf-8");

fs.mkdirSync("b");

fs.writeFileSync("./b/index.ts", `export default { async fetch () {} };`);
fs.writeFileSync(
"./b/wrangler.toml",
TOML.stringify({
compatibility_date: "2022-01-12",
compatibility_flags: [
"nodejs_compat",
"nodejs_compat_populate_process_env",
],
name: "service_name",
main: "./index.ts",
vars: {
// This should not be included in the generated types
WORKER_B_VAR: "worker b var",
},
} as unknown as TOML.JsonMap),
"utf-8"
);
// This should not be included in the generated types
fs.writeFileSync("./b/.dev.vars", "SECRET_B=hidden", "utf-8");

fs.mkdirSync("c");

fs.writeFileSync(
"./c/index.ts",
`import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
export default { async fetch () {} };

export class RealDurableExternal extends DurableObject {}

export class RealEntrypoint extends WorkerEntrypoint {}
`
);
fs.writeFileSync(
"./c/wrangler.toml",
TOML.stringify({
compatibility_date: "2022-01-12",
compatibility_flags: [
"nodejs_compat",
"nodejs_compat_populate_process_env",
],
name: "service_name_2",
main: "./index.ts",
vars: {
// This should not be included in the generated types
WORKER_C_VAR: "worker c var",
},
} as unknown as TOML.JsonMap),
"utf-8"
);
// This should not be included in the generated types
fs.writeFileSync("./c/.dev.vars", "SECRET_C=hidden", "utf-8");

await runWrangler(
"types --include-runtime=false -c a/wrangler.toml -c b/wrangler.toml -c c/wrangler.toml --path a/worker-configuration.d.ts"
);
expect(std.out).toMatchInlineSnapshot(`
"- Found Worker 'service_name' at 'b/index.ts' (b/wrangler.toml)
- Found Worker 'service_name_2' at 'c/index.ts' (c/wrangler.toml)
Generating project types...

declare namespace Cloudflare {
interface Env {
TEST_KV_NAMESPACE: KVNamespace;
SOMETHING: \\"asdasdfasdf\\";
ANOTHER: \\"thing\\";
\\"some-other-var\\": \\"some-other-value\\";
OBJECT_VAR: {\\"enterprise\\":\\"1701-D\\",\\"activeDuty\\":true,\\"captian\\":\\"Picard\\"};
SECRET: string;
DURABLE_DIRECT_EXPORT: DurableObjectNamespace<import(\\"./index\\").DurableDirect>;
DURABLE_RE_EXPORT: DurableObjectNamespace /* DurableReexport */;
DURABLE_NO_EXPORT: DurableObjectNamespace /* DurableNoexport */;
DURABLE_EXTERNAL: DurableObjectNamespace /* DurableExternal from external-worker */;
REAL_DURABLE_EXTERNAL: DurableObjectNamespace<import(\\"../c/index\\").RealDurableExternal>;
R2_BUCKET_BINDING: R2Bucket;
D1_TESTING_SOMETHING: D1Database;
SECRET: SecretsStoreSecret;
SERVICE_BINDING: Fetcher /* service_name */;
OTHER_SERVICE_BINDING: Service /* entrypoint FakeEntrypoint from service_name_2 */;
OTHER_SERVICE_BINDING_ENTRYPOINT: Service<import(\\"../c/index\\").RealEntrypoint>;
AE_DATASET_BINDING: AnalyticsEngineDataset;
NAMESPACE_BINDING: DispatchNamespace;
LOGFWDR_SCHEMA: any;
SOME_DATA_BLOB1: ArrayBuffer;
SOME_DATA_BLOB2: ArrayBuffer;
SOME_TEXT_BLOB1: string;
SOME_TEXT_BLOB2: string;
testing_unsafe: any;
UNSAFE_RATELIMIT: RateLimit;
TEST_QUEUE_BINDING: Queue;
SEND_EMAIL_BINDING: SendEmail;
VECTORIZE_BINDING: VectorizeIndex;
HYPERDRIVE_BINDING: Hyperdrive;
MTLS_BINDING: Fetcher;
BROWSER_BINDING: Fetcher;
AI_BINDING: Ai;
IMAGES_BINDING: ImagesBinding;
VERSION_METADATA_BINDING: WorkerVersionMetadata;
ASSETS_BINDING: Fetcher;
PIPELINE: import(\\"cloudflare:pipelines\\").Pipeline<import(\\"cloudflare:pipelines\\").PipelineRecord>;
}
}
interface Env extends Cloudflare.Env {}
type StringifyValues<EnvType extends Record<string, unknown>> = {
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
};
declare namespace NodeJS {
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, \\"SOMETHING\\" | \\"ANOTHER\\" | \\"some-other-var\\" | \\"OBJECT_VAR\\" | \\"SECRET\\">> {}
}
declare module \\"*.txt\\" {
const value: string;
export default value;
}
declare module \\"*.webp\\" {
const value: ArrayBuffer;
export default value;
}
declare module \\"*.wasm\\" {
const value: WebAssembly.Module;
export default value;
}
────────────────────────────────────────────────────────────
✨ Types written to a/worker-configuration.d.ts

📣 Remember to rerun 'wrangler types' after you change your wrangler.toml file.
"
`);
});

it("should create a DTS file at the location that the command is executed from", async () => {
fs.writeFileSync("./index.ts", "export default { async fetch () {} };");
fs.writeFileSync(
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export function createCLIParser(argv: string[]) {
"config",
(configArgv) =>
configArgv["_"][0] === "dev" ||
configArgv["_"][0] === "types" ||
(configArgv["_"][0] === "pages" && configArgv["_"][1] === "dev")
)
)
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/type-generation/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const checkTypesDiff = async (config: Config, entry: Entry) => {
previousEnvInterface ?? "Env",
"worker-configuration.d.ts",
entry,
new Map(),
// don't log anything
false
);
Expand Down
Loading
Loading