Skip to content

Commit 9a1f014

Browse files
authored
feat(wrangler): Add programmatic type generation API (#13717)
1 parent e539008 commit 9a1f014

7 files changed

Lines changed: 821 additions & 125 deletions

File tree

.changeset/many-baths-arrive.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Add an experimental `experimental_generateTypes()` programmatic API.
6+
7+
Wrangler now exposes `experimental_generateTypes()` from the package root so you can generate Worker types in code using the same logic as `wrangler types`. The API supports the same core type-generation options (include env/runtime toggles) and returns structured output with separate `env` and `runtime` content alongside the combined formatted output.

packages/wrangler/src/__tests__/type-generation.test.ts

Lines changed: 285 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from "node:fs";
22
import { http, HttpResponse } from "msw";
33
import { afterAll, beforeAll, beforeEach, describe, it, vi } from "vitest";
4+
import { experimental_generateTypes } from "../api";
45
import {
56
constructTSModuleGlob,
67
constructTypeKey,
@@ -544,7 +545,7 @@ const bindingsConfigMock: Omit<
544545
vpc_networks: [],
545546
};
546547

547-
describe("generate types", () => {
548+
describe("generate types - CLI", () => {
548549
let spy: MockInstance;
549550
const std = mockConsoleMethods();
550551
const originalColumns = process.stdout.columns;
@@ -3549,6 +3550,289 @@ describe("generate types", () => {
35493550
});
35503551
});
35513552

3553+
describe("generate types - API", () => {
3554+
runInTempDir();
3555+
3556+
beforeEach(() => {
3557+
for (const configPath of [
3558+
"./wrangler.jsonc",
3559+
"./wrangler.json",
3560+
"./wrangler.toml",
3561+
]) {
3562+
if (fs.existsSync(configPath)) {
3563+
fs.unlinkSync(configPath);
3564+
}
3565+
}
3566+
3567+
vi.spyOn(generateRuntime, "generateRuntimeTypes").mockImplementation(
3568+
async () => ({
3569+
runtimeHeader: "// Runtime types generated with workerd@",
3570+
runtimeTypes: "<runtime types go here>",
3571+
})
3572+
);
3573+
});
3574+
3575+
it("returns combined and split `env` output", async ({ expect }) => {
3576+
fs.writeFileSync(
3577+
"./wrangler.jsonc",
3578+
JSON.stringify({
3579+
compatibility_date: "2026-01-01",
3580+
vars: {
3581+
myVar: "hello",
3582+
},
3583+
}),
3584+
"utf-8"
3585+
);
3586+
3587+
const generated = await experimental_generateTypes({
3588+
includeRuntime: false,
3589+
});
3590+
3591+
expect(generated.path).toBe("worker-configuration.d.ts");
3592+
expect(generated.runtime).toBeNull();
3593+
expect(generated.env).toContain('myVar: "hello";');
3594+
expect(generated.content).toContain("/* eslint-disable */");
3595+
expect(generated.content).toContain(
3596+
"interface Env extends Cloudflare.Env {}"
3597+
);
3598+
expect(fs.existsSync("./worker-configuration.d.ts")).toBe(false);
3599+
});
3600+
3601+
it("returns `runtime` output when `env` types are excluded", async ({
3602+
expect,
3603+
}) => {
3604+
fs.writeFileSync(
3605+
"./wrangler.jsonc",
3606+
JSON.stringify({
3607+
compatibility_date: "2026-01-01",
3608+
}),
3609+
"utf-8"
3610+
);
3611+
3612+
const result = await experimental_generateTypes({
3613+
includeEnv: false,
3614+
includeRuntime: true,
3615+
});
3616+
3617+
expect(result.env).toBeNull();
3618+
expect(result.runtime).toBe("<runtime types go here>");
3619+
expect(result.content).toContain("// Begin runtime types");
3620+
expect(result.content).toContain("<runtime types go here>");
3621+
});
3622+
3623+
it("supports `strictVars=false` and custom env interface/path", async ({
3624+
expect,
3625+
}) => {
3626+
fs.writeFileSync(
3627+
"./wrangler.jsonc",
3628+
JSON.stringify({
3629+
compatibility_date: "2026-01-01",
3630+
vars: {
3631+
myVar: "hello",
3632+
},
3633+
}),
3634+
"utf-8"
3635+
);
3636+
3637+
const result = await experimental_generateTypes({
3638+
envInterface: "CustomEnv",
3639+
includeRuntime: false,
3640+
path: "./types/custom-worker.d.ts",
3641+
strictVars: false,
3642+
});
3643+
3644+
expect(result.path).toBe("./types/custom-worker.d.ts");
3645+
expect(result.content).toContain(
3646+
"interface CustomEnv extends Cloudflare.Env {}"
3647+
);
3648+
expect(result.env).toContain("myVar: string;");
3649+
expect(fs.existsSync("./types/custom-worker.d.ts")).toBe(false);
3650+
});
3651+
3652+
it("builds `env` header command from API options, not process args", async ({
3653+
expect,
3654+
}) => {
3655+
fs.writeFileSync(
3656+
"./wrangler.jsonc",
3657+
JSON.stringify({
3658+
compatibility_date: "2026-01-01",
3659+
vars: {
3660+
myVar: "hello",
3661+
},
3662+
}),
3663+
"utf-8"
3664+
);
3665+
3666+
const previousArgv = process.argv;
3667+
process.argv = ["node", "vitest", "run"];
3668+
3669+
try {
3670+
const result = await experimental_generateTypes({
3671+
envInterface: "CustomEnv",
3672+
includeRuntime: false,
3673+
path: "./types/custom-worker.d.ts",
3674+
strictVars: false,
3675+
});
3676+
3677+
expect(result.content).toMatch(
3678+
/\/\/ Generated by Wrangler by running `wrangler types --include-runtime=false --strict-vars=false --env-interface=CustomEnv \.\/types\/custom-worker\.d\.ts` \(hash: [a-f0-9]{32}\)/
3679+
);
3680+
} finally {
3681+
process.argv = previousArgv;
3682+
}
3683+
});
3684+
3685+
it("supports multi-config service resolution", async ({ expect }) => {
3686+
fs.mkdirSync("./primary", { recursive: true });
3687+
fs.mkdirSync("./secondary", { recursive: true });
3688+
3689+
// Primary worker
3690+
fs.writeFileSync(
3691+
"./primary/wrangler.jsonc",
3692+
JSON.stringify({
3693+
compatibility_date: "2026-01-01",
3694+
main: "./index.ts",
3695+
name: "primary",
3696+
services: [
3697+
{
3698+
binding: "SECONDARY",
3699+
service: "secondary",
3700+
},
3701+
],
3702+
}),
3703+
"utf-8"
3704+
);
3705+
fs.writeFileSync(
3706+
"./primary/index.ts",
3707+
"export default { async fetch() { return new Response('ok'); } };",
3708+
"utf-8"
3709+
);
3710+
3711+
// Secondary worker
3712+
fs.writeFileSync(
3713+
"./secondary/wrangler.jsonc",
3714+
JSON.stringify({
3715+
compatibility_date: "2026-01-01",
3716+
main: "./worker.ts",
3717+
name: "secondary",
3718+
}),
3719+
"utf-8"
3720+
);
3721+
fs.writeFileSync(
3722+
"./secondary/worker.ts",
3723+
"export default { async fetch() { return new Response('ok'); } };",
3724+
"utf-8"
3725+
);
3726+
3727+
const result = await experimental_generateTypes({
3728+
config: ["./primary/wrangler.jsonc", "./secondary/wrangler.jsonc"],
3729+
includeRuntime: false,
3730+
path: "./primary/worker-configuration.d.ts",
3731+
});
3732+
3733+
expect(result.env).toContain(
3734+
'SECONDARY: Service<typeof import("../secondary/worker").default>;'
3735+
);
3736+
});
3737+
3738+
it("validates API options", async ({ expect }) => {
3739+
await expect(experimental_generateTypes({})).rejects.toThrow(
3740+
"No config file detected. This command requires a Wrangler configuration file."
3741+
);
3742+
3743+
await expect(
3744+
experimental_generateTypes({
3745+
includeEnv: false,
3746+
includeRuntime: false,
3747+
})
3748+
).rejects.toThrow(
3749+
"You cannot run this command without including either Env or Runtime types"
3750+
);
3751+
3752+
await expect(
3753+
experimental_generateTypes({
3754+
path: "worker-configuration.txt",
3755+
})
3756+
).rejects.toThrow(
3757+
"The provided output path 'worker-configuration.txt' does not point to a declaration file - please use the '.d.ts' extension"
3758+
);
3759+
3760+
await expect(
3761+
experimental_generateTypes({
3762+
envInterface: "123Bad",
3763+
})
3764+
).rejects.toThrow(
3765+
/The provided env-interface value .* does not satisfy the validation regex/
3766+
);
3767+
});
3768+
3769+
it("matches CLI output for `env` only generation", async ({ expect }) => {
3770+
fs.writeFileSync(
3771+
"./wrangler.jsonc",
3772+
JSON.stringify({
3773+
compatibility_date: "2026-01-01",
3774+
vars: {
3775+
myVar: "hello",
3776+
},
3777+
}),
3778+
"utf-8"
3779+
);
3780+
3781+
// Manual process arguments controlling required to control codegen header output
3782+
const previousArgv = process.argv;
3783+
process.argv = ["node", "wrangler", "types", "--include-runtime=false"];
3784+
3785+
try {
3786+
await runWrangler("types --include-runtime=false");
3787+
3788+
const cliOutput = fs.readFileSync("./worker-configuration.d.ts", "utf-8");
3789+
3790+
process.argv = ["node", "wrangler", "types", "--include-runtime=false"];
3791+
const apiOutput = await experimental_generateTypes({
3792+
includeRuntime: false,
3793+
});
3794+
3795+
expect(apiOutput.content).toBe(cliOutput);
3796+
} finally {
3797+
process.argv = previousArgv;
3798+
}
3799+
});
3800+
3801+
it("matches CLI output for `runtime` + `env` generation", async ({
3802+
expect,
3803+
}) => {
3804+
fs.writeFileSync(
3805+
"./wrangler.jsonc",
3806+
JSON.stringify({
3807+
compatibility_date: "2026-01-01",
3808+
vars: {
3809+
myVar: "hello",
3810+
},
3811+
}),
3812+
"utf-8"
3813+
);
3814+
3815+
// Manual process arguments controlling required to control codegen header output
3816+
const previousArgv = process.argv;
3817+
process.argv = ["node", "wrangler", "types", "custom-types.d.ts"];
3818+
3819+
try {
3820+
await runWrangler("types custom-types.d.ts");
3821+
3822+
const cliOutput = fs.readFileSync("./custom-types.d.ts", "utf-8");
3823+
3824+
process.argv = ["node", "wrangler", "types", "custom-types.d.ts"];
3825+
const apiOutput = await experimental_generateTypes({
3826+
path: "custom-types.d.ts",
3827+
});
3828+
3829+
expect(apiOutput.content).toBe(cliOutput);
3830+
} finally {
3831+
process.argv = previousArgv;
3832+
}
3833+
});
3834+
});
3835+
35523836
describe("pipeline schema type generation", () => {
35533837
const std = mockConsoleMethods();
35543838
const originalColumns = process.stdout.columns;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
generateTypesFromWranglerOptions,
3+
type GenerateTypesOptions,
4+
} from "../type-generation";
5+
6+
export type Experimental_GenerateTypesOptions = GenerateTypesOptions;
7+
8+
interface GenerateTypesResult {
9+
/**
10+
* Combined formatted output containing all generated sections.
11+
*/
12+
content: string;
13+
14+
/**
15+
* Generated environment/bindings types, or `null` when env types are excluded.
16+
*/
17+
env: string | null;
18+
19+
/**
20+
* Target declaration file path associated with this generation run.
21+
*/
22+
path: string;
23+
24+
/**
25+
* Generated runtime types, or `null` when runtime types are excluded.
26+
*/
27+
runtime: string | null;
28+
}
29+
30+
export type Experimental_GenerateTypesResult = GenerateTypesResult;
31+
32+
/**
33+
* Generate types from your Worker configuration
34+
*
35+
* @description Programmatically generate TypeScript type definitions for your
36+
* Worker, using the same logic that powers the `wrangler types` CLI command.
37+
*
38+
* @param options - Type generation configuration options that mirror the `wrangler types` CLI flags
39+
*
40+
* @returns Structured output containing combined content & split env/runtime sections.
41+
*/
42+
export async function generateTypes(
43+
options: Experimental_GenerateTypesOptions
44+
): Promise<Experimental_GenerateTypesResult> {
45+
const generated = await generateTypesFromWranglerOptions(options);
46+
return {
47+
content: generated.content,
48+
env: generated.env,
49+
path: generated.path,
50+
runtime: generated.runtime,
51+
};
52+
}

packages/wrangler/src/api/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
export { unstable_dev } from "./dev";
22
export type { Unstable_DevWorker, Unstable_DevOptions } from "./dev";
33
export { unstable_pages } from "./pages";
4+
export { generateTypes as experimental_generateTypes } from "./generate-types";
5+
export type {
6+
Experimental_GenerateTypesOptions,
7+
Experimental_GenerateTypesResult,
8+
} from "./generate-types";
49
export {
510
uploadMTlsCertificate,
611
uploadMTlsCertificateFromFs,

0 commit comments

Comments
 (0)