sproutboat 0.53.1 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -1
- package/README.md +122 -9
- package/SURFACE.md +42 -22
- package/package.json +20 -12
- package/scripts/embed-templates.ts +87 -0
- package/src/account-field.ts +100 -0
- package/src/build.ts +13 -2
- package/src/cloud-bindings.ts +227 -0
- package/src/cloud-deploy.ts +794 -0
- package/src/cloud-domains.ts +55 -0
- package/src/cloud-fake.ts +719 -0
- package/src/cloud-secrets.ts +121 -0
- package/src/cloud.ts +375 -0
- package/src/compile.ts +34 -18
- package/src/credentials.ts +455 -52
- package/src/d1-cli.ts +455 -0
- package/src/data-errors.ts +139 -0
- package/src/deployments-cli.ts +184 -0
- package/src/dev.ts +67 -4
- package/src/fake-login.ts +28 -0
- package/src/global-flags.ts +113 -0
- package/src/init-package.ts +2 -1
- package/src/init.ts +328 -0
- package/src/main.ts +1247 -193
- package/src/options.ts +242 -0
- package/src/r2-objects.ts +365 -0
- package/src/run.ts +12 -6
- package/src/secrets-bulk.ts +51 -0
- package/src/sql-split.ts +68 -0
- package/src/standalone-build.ts +21 -3
- package/src/surface.ts +192 -64
- package/src/target.ts +95 -0
- package/src/templates-embedded.ts +72 -0
- package/src/templates.ts +53 -0
- package/src/toolchain.ts +3 -0
- package/src/types-cmd.ts +193 -0
- package/src/types-gen.ts +104 -68
- package/types/cloudflare-workers.d.ts +43 -0
- package/types/sproutboat.d.ts +160 -3
package/src/types-cmd.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sproutboat types` command: its arguments, the file it writes, and `--check`.
|
|
3
|
+
*
|
|
4
|
+
* Wrangler's contract (`wrangler types [path]`): the positional is the declaration file to write, default
|
|
5
|
+
* `worker-configuration.d.ts`; `--env-interface`, `--strict-vars`, `--include-runtime`, `--include-env` and `--check`
|
|
6
|
+
* behave as there. Two differences are on purpose and said in the help: `--include-runtime` references the runtime
|
|
7
|
+
* types shipped with the CLI instead of copying them in, and there is no `--include-main-module` (nothing here reads
|
|
8
|
+
* `ctx.exports`).
|
|
9
|
+
*
|
|
10
|
+
* Earlier releases took a project directory as the one positional. A positional that is an existing directory is
|
|
11
|
+
* still read that way.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, statSync } from "node:fs";
|
|
14
|
+
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
16
|
+
import type { SproutboatConfig } from "./config";
|
|
17
|
+
import {
|
|
18
|
+
exportedClasses,
|
|
19
|
+
generateTypes,
|
|
20
|
+
hashOf,
|
|
21
|
+
isCurrent,
|
|
22
|
+
LEGACY_TYPES_FILE,
|
|
23
|
+
LEGACY_TYPES_HEADER,
|
|
24
|
+
TYPES_FILE,
|
|
25
|
+
type TypesOptions,
|
|
26
|
+
} from "./types-gen";
|
|
27
|
+
|
|
28
|
+
export const TYPES_USAGE =
|
|
29
|
+
"types [path] [--env-interface <name>] [--no-strict-vars] [--no-include-runtime] [--no-include-env] [--check]";
|
|
30
|
+
|
|
31
|
+
export type TypesArgs = {
|
|
32
|
+
/** The declaration file, as typed; or the project directory when it names an existing directory. */
|
|
33
|
+
positional?: string;
|
|
34
|
+
options: Omit<TypesOptions, "command" | "mainModule">;
|
|
35
|
+
check: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type BooleanOption = "strictVars" | "includeRuntime" | "includeEnv";
|
|
39
|
+
const BOOLEAN_FLAGS = new Map<string, BooleanOption>([
|
|
40
|
+
["--strict-vars", "strictVars"],
|
|
41
|
+
["--include-runtime", "includeRuntime"],
|
|
42
|
+
["--include-env", "includeEnv"],
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** Parse what is left after the global flags and `checkOptions`, where `--flag=value` is already `--flag value`. */
|
|
46
|
+
export function parseTypesArgs(args: readonly string[]): TypesArgs | { error: string } {
|
|
47
|
+
const out: TypesArgs = { options: {}, check: false };
|
|
48
|
+
for (let at = 0; at < args.length; at += 1) {
|
|
49
|
+
const arg = args[at]!;
|
|
50
|
+
if (arg === "--check") out.check = true;
|
|
51
|
+
else if (arg === "--env-interface") {
|
|
52
|
+
const value = args[(at += 1)];
|
|
53
|
+
if (value === undefined || value.startsWith("--")) return { error: "--env-interface needs a name" };
|
|
54
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value)) return { error: `"${value}" is not a valid interface name` };
|
|
55
|
+
out.options.envInterface = value;
|
|
56
|
+
} else if (BOOLEAN_FLAGS.has(arg) || (arg.startsWith("--no-") && BOOLEAN_FLAGS.has(`--${arg.slice(5)}`))) {
|
|
57
|
+
const negated = !BOOLEAN_FLAGS.has(arg);
|
|
58
|
+
const key = BOOLEAN_FLAGS.get(negated ? `--${arg.slice(5)}` : arg)!;
|
|
59
|
+
if (negated) {
|
|
60
|
+
out.options[key] = false;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const next = args[at + 1];
|
|
64
|
+
// `--strict-vars=false` arrives as `--strict-vars false`; a bare `--strict-vars` means true.
|
|
65
|
+
if (next === "true" || next === "false") {
|
|
66
|
+
out.options[key] = next === "true";
|
|
67
|
+
at += 1;
|
|
68
|
+
} else if (next === undefined || next.startsWith("--")) out.options[key] = true;
|
|
69
|
+
else return { error: `${arg} takes true or false, not "${next}"` };
|
|
70
|
+
} else if (arg.startsWith("-")) return { error: `unknown option ${arg}` };
|
|
71
|
+
else if (out.positional === undefined) out.positional = arg;
|
|
72
|
+
else return { error: `unexpected argument "${arg}"` };
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The command line as it is recorded in the generated file's header: the flags that differ from the defaults. */
|
|
78
|
+
function commandLine(parsed: TypesArgs, path: string | undefined): string {
|
|
79
|
+
const { envInterface, strictVars, includeRuntime, includeEnv } = parsed.options;
|
|
80
|
+
return [
|
|
81
|
+
"sproutboat types",
|
|
82
|
+
...(path ? [path] : []),
|
|
83
|
+
...(envInterface && envInterface !== "Env" ? [`--env-interface=${envInterface}`] : []),
|
|
84
|
+
...(strictVars === false ? ["--strict-vars=false"] : []),
|
|
85
|
+
...(includeRuntime === false ? ["--include-runtime=false"] : []),
|
|
86
|
+
...(includeEnv === false ? ["--include-env=false"] : []),
|
|
87
|
+
].join(" ");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type TypesPlan = {
|
|
91
|
+
/** Absolute path of the declaration file. */
|
|
92
|
+
path: string;
|
|
93
|
+
/** What `generateTypes` writes for it. */
|
|
94
|
+
text: string;
|
|
95
|
+
/** The positional was a project directory, which this release still reads (the file is then the default one). */
|
|
96
|
+
legacyDirectory: boolean;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The project directory and output file for a positional, and the file's text.
|
|
101
|
+
*
|
|
102
|
+
* `projectDirectory` is where `sproutboat.jsonc` is (already resolved from `-c` / `--cwd` and a directory
|
|
103
|
+
* positional); the output path is relative to the working directory, as in Wrangler.
|
|
104
|
+
*/
|
|
105
|
+
export async function planTypes(
|
|
106
|
+
parsed: TypesArgs,
|
|
107
|
+
config: SproutboatConfig,
|
|
108
|
+
projectDirectory: string,
|
|
109
|
+
cwd: string = process.cwd(),
|
|
110
|
+
): Promise<TypesPlan> {
|
|
111
|
+
const positional = parsed.positional;
|
|
112
|
+
const asDirectory = positional !== undefined && isDirectory(resolve(cwd, positional));
|
|
113
|
+
const path =
|
|
114
|
+
positional === undefined || asDirectory ? resolve(projectDirectory, TYPES_FILE) : resolve(cwd, positional);
|
|
115
|
+
|
|
116
|
+
let mainModule: TypesOptions["mainModule"];
|
|
117
|
+
if (/\.(?:ts|mts|tsx)$/.test(config.main)) {
|
|
118
|
+
const entry = resolve(projectDirectory, config.main);
|
|
119
|
+
try {
|
|
120
|
+
const classes = exportedClasses(await readFile(entry, "utf8"));
|
|
121
|
+
if (classes.length) {
|
|
122
|
+
const rel = relative(dirname(path), entry.replace(/\.[^./\\]+$/, ""))
|
|
123
|
+
.split(sep)
|
|
124
|
+
.join("/");
|
|
125
|
+
mainModule = { importPath: rel.startsWith(".") ? rel : `./${rel}`, classes };
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// No entry file yet: the bindings are still worth typing.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const text = generateTypes(config, {
|
|
132
|
+
...parsed.options,
|
|
133
|
+
mainModule,
|
|
134
|
+
command: commandLine(parsed, positional !== undefined && !asDirectory ? positional : undefined),
|
|
135
|
+
});
|
|
136
|
+
return { path, text, legacyDirectory: asDirectory };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `--check`: whether the file at `path` is the one `plan` would write. Missing or hand-written is not current. */
|
|
140
|
+
export async function checkTypes(plan: TypesPlan): Promise<"current" | "stale" | "missing"> {
|
|
141
|
+
if (!existsSync(plan.path)) return "missing";
|
|
142
|
+
return isCurrent(await readFile(plan.path, "utf8"), plan.text) ? "current" : "stale";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isDirectory(path: string): boolean {
|
|
146
|
+
try {
|
|
147
|
+
return statSync(path).isDirectory();
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Remove the file earlier releases wrote, when it is still exactly what they generated. */
|
|
154
|
+
export async function removeLegacyTypes(projectDir: string): Promise<boolean> {
|
|
155
|
+
const legacy = resolve(projectDir, LEGACY_TYPES_FILE);
|
|
156
|
+
try {
|
|
157
|
+
if (!(await readFile(legacy, "utf8")).startsWith(LEGACY_TYPES_HEADER)) return false;
|
|
158
|
+
await rm(legacy);
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Rewrite the generated types when the config has moved on, and only when the default file is the one a plain
|
|
167
|
+
* `sproutboat types` wrote. Called from `dev` and `deploy` so a binding added to sproutboat.jsonc cannot sit
|
|
168
|
+
* unreflected in the editor. It writes only when the content differs, which keeps it out of the way of git and of
|
|
169
|
+
* file watchers, and leaves a file made with other flags (or by hand) alone: Wrangler never touches it either.
|
|
170
|
+
* Returns what it did, for the caller to print.
|
|
171
|
+
*/
|
|
172
|
+
export async function refreshTypes(
|
|
173
|
+
projectDir: string,
|
|
174
|
+
config: SproutboatConfig,
|
|
175
|
+
): Promise<{ wrote: boolean; removedLegacy: boolean }> {
|
|
176
|
+
const parsed = parseTypesArgs([]);
|
|
177
|
+
if ("error" in parsed) return { wrote: false, removedLegacy: false };
|
|
178
|
+
const plan = await planTypes(parsed, config, projectDir, projectDir);
|
|
179
|
+
let have: string | undefined;
|
|
180
|
+
try {
|
|
181
|
+
have = await readFile(plan.path, "utf8");
|
|
182
|
+
} catch {
|
|
183
|
+
// No file yet: only write one if the project already opted in with a tsconfig. A plain JavaScript project gets none.
|
|
184
|
+
if (!existsSync(resolve(projectDir, "tsconfig.json"))) return { wrote: false, removedLegacy: false };
|
|
185
|
+
}
|
|
186
|
+
if (have !== undefined) {
|
|
187
|
+
const customCommand = !(have.split("\n", 2)[1] ?? "").includes("`sproutboat types`");
|
|
188
|
+
if (hashOf(have) === undefined || customCommand || hashOf(have) === hashOf(plan.text))
|
|
189
|
+
return { wrote: false, removedLegacy: false };
|
|
190
|
+
}
|
|
191
|
+
await writeFile(plan.path, plan.text);
|
|
192
|
+
return { wrote: true, removedLegacy: await removeLegacyTypes(projectDir) };
|
|
193
|
+
}
|
package/src/types-gen.ts
CHANGED
|
@@ -1,45 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `sproutboat types` — the project's `
|
|
2
|
+
* `sproutboat types` — the project's `Env`, as TypeScript, the way `wrangler types` writes it.
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
* the
|
|
4
|
+
* The output is `worker-configuration.d.ts` (Wrangler's file name), a script file that declares an `Env` interface
|
|
5
|
+
* and `Cloudflare.Env` (what `import { env } from "cloudflare:workers"` reads) from `sproutboat.jsonc`, then
|
|
6
|
+
* references the ambient types shipped in `types/sproutboat.d.ts`. Binding *names* version with the project and
|
|
7
|
+
* binding *shapes* with the CLI. A handler is typed with `satisfies ExportedHandler<Env>`.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* the CLI and binding *names* version with the project.
|
|
11
|
-
*
|
|
12
|
-
* Names need no quoting: `parseConfig` already holds every binding and var to
|
|
13
|
-
* /^[A-Z][A-Z0-9_]*$/, so all of them are plain identifiers.
|
|
9
|
+
* Names need no quoting: `parseConfig` already holds every binding and var to /^[A-Z][A-Z0-9_]*$/, so all of them
|
|
10
|
+
* are plain identifiers.
|
|
14
11
|
*/
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
15
13
|
import { resourceRefs, type SproutboatConfig } from "./config";
|
|
16
14
|
|
|
17
|
-
/** The file
|
|
18
|
-
export const TYPES_FILE = "
|
|
15
|
+
/** The file `sproutboat types` writes by default, relative to the project directory. */
|
|
16
|
+
export const TYPES_FILE = "worker-configuration.d.ts";
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
/** The file earlier releases wrote. It is only ever removed when it still carries its generated header. */
|
|
19
|
+
export const LEGACY_TYPES_FILE = "sproutboat-env.d.ts";
|
|
20
|
+
export const LEGACY_TYPES_HEADER = "// Generated by `sproutboat types`. Do not edit.";
|
|
21
|
+
|
|
22
|
+
export type TypesOptions = {
|
|
23
|
+
/** Name of the interface (`--env-interface`). Default `Env`. */
|
|
24
|
+
envInterface?: string;
|
|
25
|
+
/** Literal types for `vars` (`--strict-vars`, default on); off, every var is a `string`. */
|
|
26
|
+
strictVars?: boolean;
|
|
27
|
+
/** Reference the runtime types shipped with the CLI (`--include-runtime`, default on). */
|
|
28
|
+
includeRuntime?: boolean;
|
|
29
|
+
/** Write the `Env` types (`--include-env`, default on). */
|
|
30
|
+
includeEnv?: boolean;
|
|
31
|
+
/** The command as typed, for the header. */
|
|
32
|
+
command?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The entry module, so a Durable Object binding is typed with its class and `stub.method()` is checked.
|
|
35
|
+
* `importPath` is relative to the output file and has no extension; only a TypeScript entry is offered.
|
|
36
|
+
*/
|
|
37
|
+
mainModule?: { importPath: string; classes: readonly string[] };
|
|
38
|
+
};
|
|
26
39
|
|
|
27
40
|
type Entry = { name: string; type: string; note?: string };
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
43
|
+
|
|
44
|
+
/** Whether a binding's calls return Promises at the project's compatibility date. */
|
|
30
45
|
function asyncAt(config: SproutboatConfig, date: string): boolean {
|
|
31
46
|
return config.compatibility_date >= date;
|
|
32
47
|
}
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
/** `export class Name` declarations of an entry module, for typing Durable Object bindings. */
|
|
50
|
+
export function exportedClasses(source: string): string[] {
|
|
51
|
+
return [...source.matchAll(/^\s*export\s+(?:abstract\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)/gm)].map(
|
|
52
|
+
(match) => match[1]!,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function entries(config: SproutboatConfig, options: TypesOptions): Entry[] {
|
|
35
57
|
const out: Entry[] = [];
|
|
58
|
+
const strictVars = options.strictVars ?? true;
|
|
36
59
|
|
|
37
60
|
for (const [name, value] of Object.entries(config.vars ?? {})) {
|
|
38
|
-
out.push({ name, type:
|
|
39
|
-
}
|
|
40
|
-
for (const name of config.secrets ?? []) {
|
|
41
|
-
out.push({ name, type: "string", note: "secret, set with `sproutboat secrets put`" });
|
|
61
|
+
out.push({ name, type: strictVars ? JSON.stringify(value) : "string" });
|
|
42
62
|
}
|
|
63
|
+
for (const name of config.secrets ?? []) out.push({ name, type: "string", note: "secret" });
|
|
43
64
|
for (const { binding } of resourceRefs(config.kv_namespaces))
|
|
44
65
|
out.push({ name: binding, type: asyncAt(config, "2026-10-25") ? "KVNamespaceAsync" : "KVNamespace" });
|
|
45
66
|
for (const { binding } of resourceRefs(config.d1_databases))
|
|
@@ -47,63 +68,78 @@ function entries(config: SproutboatConfig): Entry[] {
|
|
|
47
68
|
for (const { binding } of resourceRefs(config.r2_buckets))
|
|
48
69
|
out.push({ name: binding, type: asyncAt(config, "2026-10-26") ? "R2BucketAsync" : "R2Bucket" });
|
|
49
70
|
for (const { binding } of resourceRefs(config.queues)) out.push({ name: binding, type: "Queue" });
|
|
50
|
-
for (const name of config.analytics_engine_datasets ?? []) {
|
|
51
|
-
out.push({ name, type: "AnalyticsEngineDataset" });
|
|
52
|
-
}
|
|
71
|
+
for (const name of config.analytics_engine_datasets ?? []) out.push({ name, type: "AnalyticsEngineDataset" });
|
|
53
72
|
for (const [binding, className] of Object.entries(config.durable_objects ?? {})) {
|
|
54
|
-
|
|
73
|
+
if (!asyncAt(config, "2026-10-28")) {
|
|
74
|
+
out.push({ name: binding, type: "DurableObjectNamespace", note: className });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const typed = options.mainModule?.classes.includes(className) && IDENTIFIER.test(className);
|
|
78
|
+
out.push({
|
|
79
|
+
name: binding,
|
|
80
|
+
type: typed
|
|
81
|
+
? `DurableObjectNamespaceAsync<import(${JSON.stringify(options.mainModule!.importPath)}).${className}>`
|
|
82
|
+
: "DurableObjectNamespaceAsync",
|
|
83
|
+
note: typed ? undefined : className,
|
|
84
|
+
});
|
|
55
85
|
}
|
|
56
86
|
for (const service of config.services ?? []) {
|
|
57
|
-
out.push({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
out.push({
|
|
88
|
+
name: service.binding,
|
|
89
|
+
type: asyncAt(config, "2026-10-22") ? "FetcherAsync" : "Fetcher",
|
|
90
|
+
note: `service: ${service.service}`,
|
|
91
|
+
});
|
|
61
92
|
}
|
|
93
|
+
for (const limiter of config.ratelimiters ?? []) out.push({ name: limiter.binding, type: "RateLimit" });
|
|
62
94
|
if (config.assets?.binding) {
|
|
63
|
-
out.push({ name: config.assets.binding, type: "
|
|
64
|
-
}
|
|
65
|
-
if (config.version_metadata) {
|
|
66
|
-
out.push({ name: config.version_metadata, type: "VersionMetadata" });
|
|
95
|
+
out.push({ name: config.assets.binding, type: asyncAt(config, "2026-10-22") ? "FetcherAsync" : "Fetcher" });
|
|
67
96
|
}
|
|
97
|
+
if (config.version_metadata) out.push({ name: config.version_metadata, type: "VersionMetadata" });
|
|
68
98
|
return out;
|
|
69
99
|
}
|
|
70
100
|
|
|
101
|
+
const sha = (text: string) => createHash("sha256").update(text).digest("hex").slice(0, 32);
|
|
102
|
+
|
|
103
|
+
/** The hash a generated file carries in its first line, or `undefined` for anything else. */
|
|
104
|
+
export function hashOf(file: string): string | undefined {
|
|
105
|
+
return /^\/\* eslint-disable \*\/\n\/\/ Generated by Sproutboat by running `[^\n]*` \(hash: ([0-9a-f]{32})\)\n/.exec(
|
|
106
|
+
file,
|
|
107
|
+
)?.[1];
|
|
108
|
+
}
|
|
109
|
+
|
|
71
110
|
/**
|
|
72
|
-
* The contents of `
|
|
111
|
+
* The contents of `worker-configuration.d.ts` for `config`.
|
|
73
112
|
*
|
|
74
|
-
* Pure, so the
|
|
75
|
-
* what is already on disk without writing.
|
|
113
|
+
* Pure, so the tests can assert on the text and `dev` can compare it with what is on disk without writing.
|
|
76
114
|
*/
|
|
77
|
-
export function generateTypes(config: SproutboatConfig): string {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
: " // No bindings declared in sproutboat.jsonc yet.";
|
|
84
|
-
|
|
85
|
-
const handlerNote =
|
|
86
|
-
config.compatibility_date >= "2026-09-28"
|
|
87
|
-
? "The same object is passed as the second handler argument: fetch(request, env, ctx)."
|
|
88
|
-
: "The handler signature is fetch(request, ctx); env is only a global.";
|
|
115
|
+
export function generateTypes(config: SproutboatConfig, options: TypesOptions = {}): string {
|
|
116
|
+
const name = options.envInterface ?? "Env";
|
|
117
|
+
if (!IDENTIFIER.test(name)) throw new Error(`"${name}" is not a valid interface name`);
|
|
118
|
+
const includeRuntime = options.includeRuntime ?? true;
|
|
119
|
+
const includeEnv = options.includeEnv ?? true;
|
|
120
|
+
const legacy = config.compatibility_date < "2026-09-28";
|
|
89
121
|
|
|
90
|
-
|
|
91
|
-
/// <reference types="sproutboat/types" />
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
122
|
+
const parts: string[] = [];
|
|
123
|
+
if (includeRuntime) parts.push('/// <reference types="sproutboat/types" />');
|
|
124
|
+
if (includeEnv) {
|
|
125
|
+
const rows = entries(config, options);
|
|
126
|
+
const base = `__Base${name}`;
|
|
127
|
+
const members = rows.map((row) => `\t${row.name}: ${row.type};${row.note ? ` /* ${row.note} */` : ""}`);
|
|
128
|
+
parts.push(
|
|
129
|
+
`interface ${base} {${members.length ? `\n${members.join("\n")}\n` : ""}}`,
|
|
130
|
+
`declare namespace Cloudflare {\n\tinterface Env extends ${base} {}\n}`,
|
|
131
|
+
`interface ${name} extends ${base} {}`,
|
|
132
|
+
);
|
|
133
|
+
// Before compatibility date 2026-09-28 the handler is `fetch(request, ctx)` and `env` is only a global.
|
|
134
|
+
if (legacy) parts.push(`declare const env: ${name};`);
|
|
135
|
+
}
|
|
136
|
+
const body = parts.join("\n") + "\n";
|
|
137
|
+
const command = options.command ?? "sproutboat types";
|
|
138
|
+
return `/* eslint-disable */\n// Generated by Sproutboat by running \`${command}\` (hash: ${sha(body)})\n${body}`;
|
|
105
139
|
}
|
|
106
140
|
|
|
107
|
-
|
|
108
|
-
|
|
141
|
+
/** Whether `file` is what `generateTypes` would write for the same input, by the hash in its header. */
|
|
142
|
+
export function isCurrent(file: string, wanted: string): boolean {
|
|
143
|
+
const have = hashOf(file);
|
|
144
|
+
return have !== undefined && have === hashOf(wanted);
|
|
109
145
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `cloudflare:workers` module, as the runtime provides it: the base classes Workers code extends and the
|
|
3
|
+
* `env` import. A script file on purpose: an ambient `declare module` is only a declaration here, not an
|
|
4
|
+
* augmentation. `sproutboat.d.ts` references it, so the one reference a project's generated file carries is enough.
|
|
5
|
+
*
|
|
6
|
+
* `Cloudflare.Env` is declared by the project's `worker-configuration.d.ts`.
|
|
7
|
+
*/
|
|
8
|
+
/// <reference path="./sproutboat.d.ts" />
|
|
9
|
+
|
|
10
|
+
declare module "cloudflare:workers" {
|
|
11
|
+
/** The bindings of this project, same object as the second handler argument. */
|
|
12
|
+
export const env: Cloudflare.Env;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A Durable Object class, from compatibility date 2026-10-29 (2026-10-28 for the Promise storage): `this.ctx` and
|
|
16
|
+
* `this.env` are set, and any method is callable as `await stub.method()`.
|
|
17
|
+
*/
|
|
18
|
+
export abstract class DurableObject<Bindings = Cloudflare.Env> {
|
|
19
|
+
constructor(ctx: DurableObjectStateAsync, env: Bindings);
|
|
20
|
+
protected ctx: DurableObjectStateAsync;
|
|
21
|
+
protected env: Bindings;
|
|
22
|
+
fetch?(request: Request): Response | Promise<Response>;
|
|
23
|
+
/** Runs after `storage.setAlarm`, with no request in flight. */
|
|
24
|
+
alarm?(info?: { retryCount: number; isRetry: boolean }): void | Promise<void>;
|
|
25
|
+
webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise<void>;
|
|
26
|
+
webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A Worker as a class: `export default class extends WorkerEntrypoint<Env>`. Its methods are what a service
|
|
31
|
+
* binding calls over RPC (from compatibility date 2026-11-18).
|
|
32
|
+
*/
|
|
33
|
+
export abstract class WorkerEntrypoint<Bindings = Cloudflare.Env> {
|
|
34
|
+
constructor(ctx: ExecutionContext, env: Bindings);
|
|
35
|
+
protected ctx: ExecutionContext;
|
|
36
|
+
protected env: Bindings;
|
|
37
|
+
fetch?(request: Request): Response | Promise<Response>;
|
|
38
|
+
scheduled?(controller: ScheduledController): void | Promise<void>;
|
|
39
|
+
queue?(batch: MessageBatch): void | Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class RpcTarget {}
|
|
43
|
+
}
|
package/types/sproutboat.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Shipped with the CLI rather than as a separate package, so the types always
|
|
5
5
|
* describe the runtime that the installed CLI compiles. `sproutboat types`
|
|
6
|
-
* generates a project's `
|
|
7
|
-
* declares `
|
|
6
|
+
* generates a project's `worker-configuration.d.ts`, which references this file
|
|
7
|
+
* and declares `Env` with that project's bindings, as `wrangler types` does.
|
|
8
8
|
*
|
|
9
9
|
* KV, D1 and R2 are synchronous before the compatibility date that makes each one return a
|
|
10
10
|
* Promise (D1 2026-10-24, KV 2026-10-25, R2 2026-10-26): `KVNamespace`, `D1Database` and
|
|
@@ -13,9 +13,24 @@
|
|
|
13
13
|
* project's date. `await` works at every date. Before compatibility date 2026-09-28, `env` is a
|
|
14
14
|
* global only and `ctx` is the second handler argument. From that date, `env`
|
|
15
15
|
* is also passed as the second argument and `ctx` as the third.
|
|
16
|
+
*
|
|
17
|
+
* Durable Objects follow the same rule: from 2026-10-28 storage and `stub.fetch()` return Promises
|
|
18
|
+
* (`DurableObjectNamespaceAsync`, `DurableObjectStorageAsync`, `DurableObjectStateAsync`), from
|
|
19
|
+
* 2026-10-29 a class that extends `DurableObject` from "cloudflare:workers" is called over RPC, and from
|
|
20
|
+
* 2026-10-22 `fetch()` on a service or assets binding returns a Promise (`FetcherAsync`).
|
|
16
21
|
*/
|
|
17
22
|
|
|
23
|
+
/// <reference path="./cloudflare-workers.d.ts" />
|
|
24
|
+
|
|
18
25
|
declare global {
|
|
26
|
+
/**
|
|
27
|
+
* The project's bindings. Empty here; `sproutboat types` writes `interface Env extends ...` into
|
|
28
|
+
* `worker-configuration.d.ts`, which merges with this one, and `import { env } from "cloudflare:workers"` reads it.
|
|
29
|
+
*/
|
|
30
|
+
namespace Cloudflare {
|
|
31
|
+
interface Env {}
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
// ---------------------------------------------------------------- KV
|
|
20
35
|
|
|
21
36
|
interface KVPutOptions {
|
|
@@ -262,6 +277,8 @@ declare global {
|
|
|
262
277
|
}
|
|
263
278
|
|
|
264
279
|
interface R2ObjectBodyAsync extends R2ObjectAsync {
|
|
280
|
+
/** Copies `httpMetadata` (content type, cache control, ...) onto `headers`, as on Workers. */
|
|
281
|
+
writeHttpMetadata(headers: Headers): void;
|
|
265
282
|
body: ReadableStream;
|
|
266
283
|
bodyUsed: boolean;
|
|
267
284
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
@@ -419,6 +436,118 @@ declare global {
|
|
|
419
436
|
getWebSocketAutoResponse(): WebSocketRequestResponsePair | null;
|
|
420
437
|
}
|
|
421
438
|
|
|
439
|
+
// ------------------------------------- Durable Objects, from 2026-10-28
|
|
440
|
+
|
|
441
|
+
/** Workers RPC: the methods of a class, as Promise-returning stub methods. */
|
|
442
|
+
type RpcMethods<T> = {
|
|
443
|
+
[
|
|
444
|
+
K in keyof T as K extends
|
|
445
|
+
| "fetch"
|
|
446
|
+
| "alarm"
|
|
447
|
+
| "webSocketMessage"
|
|
448
|
+
| "webSocketClose"
|
|
449
|
+
| "webSocketError"
|
|
450
|
+
| "scheduled"
|
|
451
|
+
| "queue"
|
|
452
|
+
| "ctx"
|
|
453
|
+
| "env"
|
|
454
|
+
? never
|
|
455
|
+
: T[K] extends (...args: never[]) => unknown
|
|
456
|
+
? K
|
|
457
|
+
: never
|
|
458
|
+
]: T[K] extends (...args: infer Args) => infer Result ? (...args: Args) => Promise<Awaited<Result>> : never;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* What `namespace.get(id)` returns from 2026-10-28: `fetch()` is a Promise, and the methods of a
|
|
463
|
+
* class that extends `DurableObject` are callable as `await stub.method(...)` from 2026-10-29. Give
|
|
464
|
+
* the class as the type argument: `DurableObjectNamespaceAsync<Counter>`.
|
|
465
|
+
*/
|
|
466
|
+
type DurableObjectStubAsync<T = undefined> = (T extends undefined ? unknown : RpcMethods<T>) & {
|
|
467
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
468
|
+
readonly id: DurableObjectId;
|
|
469
|
+
readonly name?: string;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
interface DurableObjectNamespaceAsync<T = undefined> {
|
|
473
|
+
idFromName(name: string): DurableObjectId;
|
|
474
|
+
/** A 64-digit hex id, from `id.toString()`. */
|
|
475
|
+
idFromString(id: string): DurableObjectId;
|
|
476
|
+
newUniqueId(): DurableObjectId;
|
|
477
|
+
get(id: DurableObjectId): DurableObjectStubAsync<T>;
|
|
478
|
+
/** `get(idFromName(name))`, from 2026-10-29. */
|
|
479
|
+
getByName(name: string): DurableObjectStubAsync<T>;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
interface DurableObjectListOptions {
|
|
483
|
+
prefix?: string;
|
|
484
|
+
start?: string;
|
|
485
|
+
startAfter?: string;
|
|
486
|
+
end?: string;
|
|
487
|
+
limit?: number;
|
|
488
|
+
reverse?: boolean;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
interface DurableObjectTransaction {
|
|
492
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
493
|
+
put<T>(key: string, value: T): Promise<void>;
|
|
494
|
+
delete(key: string): Promise<boolean>;
|
|
495
|
+
list<T = unknown>(options?: DurableObjectListOptions): Promise<Map<string, T>>;
|
|
496
|
+
rollback(): void;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
type SqlStorageValue = ArrayBuffer | string | number | null;
|
|
500
|
+
|
|
501
|
+
interface SqlStorageCursor<T = Record<string, SqlStorageValue>> extends Iterable<T> {
|
|
502
|
+
readonly columnNames: string[];
|
|
503
|
+
readonly rowsRead: number;
|
|
504
|
+
readonly rowsWritten: number;
|
|
505
|
+
toArray(): T[];
|
|
506
|
+
one(): T;
|
|
507
|
+
raw<R extends SqlStorageValue[] = SqlStorageValue[]>(): IterableIterator<R>;
|
|
508
|
+
next(): IteratorResult<T>;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** From 2026-10-29: each object has its own SQLite database. */
|
|
512
|
+
interface SqlStorage {
|
|
513
|
+
exec<T = Record<string, SqlStorageValue>>(query: string, ...bindings: unknown[]): SqlStorageCursor<T>;
|
|
514
|
+
readonly databaseSize: number;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Storage from 2026-10-28: every call returns a Promise, and an object keeps its own input gate. */
|
|
518
|
+
interface DurableObjectStorageAsync {
|
|
519
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
520
|
+
get<T = unknown>(keys: string[]): Promise<Map<string, T>>;
|
|
521
|
+
put<T>(key: string, value: T): Promise<void>;
|
|
522
|
+
put(entries: Record<string, unknown>): Promise<void>;
|
|
523
|
+
delete(key: string): Promise<boolean>;
|
|
524
|
+
delete(keys: string[]): Promise<number>;
|
|
525
|
+
deleteAll(): Promise<void>;
|
|
526
|
+
list<T = unknown>(options?: DurableObjectListOptions): Promise<Map<string, T>>;
|
|
527
|
+
transaction<T>(closure: (txn: DurableObjectTransaction) => Promise<T>): Promise<T>;
|
|
528
|
+
/** Synchronous transaction over `sql`; rolls back if `closure` throws. */
|
|
529
|
+
transactionSync<T>(closure: () => T): T;
|
|
530
|
+
sync(): Promise<void>;
|
|
531
|
+
/** At most one alarm is pending per object; a later `setAlarm` replaces it. */
|
|
532
|
+
setAlarm(scheduledTime: number | Date): Promise<void>;
|
|
533
|
+
getAlarm(): Promise<number | null>;
|
|
534
|
+
deleteAlarm(): Promise<void>;
|
|
535
|
+
readonly sql: SqlStorage;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
interface DurableObjectStateAsync {
|
|
539
|
+
readonly id: DurableObjectId;
|
|
540
|
+
readonly storage: DurableObjectStorageAsync;
|
|
541
|
+
/** Holds every other event out of the object until `callback` settles. Use it for start-up work. */
|
|
542
|
+
blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T>;
|
|
543
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
544
|
+
acceptWebSocket(ws: WebSocket, tags?: string[]): void;
|
|
545
|
+
getWebSockets(tag?: string): WebSocket[];
|
|
546
|
+
getTags(ws: WebSocket): string[];
|
|
547
|
+
setWebSocketAutoResponse(pair?: WebSocketRequestResponsePair): void;
|
|
548
|
+
getWebSocketAutoResponse(): WebSocketRequestResponsePair | null;
|
|
549
|
+
}
|
|
550
|
+
|
|
422
551
|
// ---------------------------------------------------------- WebSockets
|
|
423
552
|
|
|
424
553
|
/**
|
|
@@ -502,11 +631,20 @@ declare global {
|
|
|
502
631
|
|
|
503
632
|
// ------------------------------------ Fetchers: services and assets
|
|
504
633
|
|
|
505
|
-
/** A service binding, and the shape of the static-asset binding. */
|
|
634
|
+
/** A service binding, and the shape of the static-asset binding, before 2026-10-22. */
|
|
506
635
|
interface Fetcher {
|
|
507
636
|
fetch(request: Request): Response;
|
|
508
637
|
}
|
|
509
638
|
|
|
639
|
+
/**
|
|
640
|
+
* From 2026-10-22 `fetch()` on a service or assets binding returns a Promise. A service binding to a
|
|
641
|
+
* `WorkerEntrypoint` also exposes its methods over RPC from 2026-11-18: give the class as the type
|
|
642
|
+
* argument, `FetcherAsync<QuoteService>`, and `await env.PEER.random()` is checked.
|
|
643
|
+
*/
|
|
644
|
+
type FetcherAsync<T = undefined> = (T extends undefined ? unknown : RpcMethods<T>) & {
|
|
645
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
646
|
+
};
|
|
647
|
+
|
|
510
648
|
// --------------------------------------------------------- Handlers
|
|
511
649
|
|
|
512
650
|
interface ScheduledEvent {
|
|
@@ -515,6 +653,15 @@ declare global {
|
|
|
515
653
|
scheduledTime: number;
|
|
516
654
|
}
|
|
517
655
|
|
|
656
|
+
/** What `scheduled(controller, env, ctx)` receives, from compatibility date 2026-09-28. */
|
|
657
|
+
interface ScheduledController {
|
|
658
|
+
/** The expression that fired, as written in `triggers.crons`. */
|
|
659
|
+
readonly cron: string;
|
|
660
|
+
readonly scheduledTime: number;
|
|
661
|
+
/** Do not retry this run if the handler throws. */
|
|
662
|
+
noRetry(): void;
|
|
663
|
+
}
|
|
664
|
+
|
|
518
665
|
/** Work that outlives the response, drained in-process and capped at 25s. */
|
|
519
666
|
interface ExecutionContext {
|
|
520
667
|
waitUntil(promise: Promise<unknown>): void;
|
|
@@ -529,6 +676,16 @@ declare global {
|
|
|
529
676
|
queue?(batch: MessageBatch<QueueBody>, ctx: ExecutionContext): void | Promise<void>;
|
|
530
677
|
}
|
|
531
678
|
|
|
679
|
+
/**
|
|
680
|
+
* The default export of a Worker, as in Workers: `satisfies ExportedHandler<Env>`. Compatibility dates
|
|
681
|
+
* from 2026-09-28; `Env` comes from `sproutboat types`.
|
|
682
|
+
*/
|
|
683
|
+
interface ExportedHandler<Bindings = unknown, QueueBody = unknown> {
|
|
684
|
+
fetch?(request: Request, env: Bindings, ctx: ExecutionContext): Response | Promise<Response>;
|
|
685
|
+
scheduled?(controller: ScheduledController, env: Bindings, ctx: ExecutionContext): void | Promise<void>;
|
|
686
|
+
queue?(batch: MessageBatch<QueueBody>, env: Bindings, ctx: ExecutionContext): void | Promise<void>;
|
|
687
|
+
}
|
|
688
|
+
|
|
532
689
|
/** Handler signature for compatibility dates from 2026-09-28. */
|
|
533
690
|
interface WorkersHandler<Bindings = Record<string, unknown>, QueueBody = unknown> {
|
|
534
691
|
fetch(request: Request, env: Bindings, ctx: ExecutionContext): Response | Promise<Response>;
|