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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { parseConfig, type ConfigValidation } from "./config";
|
|
2
|
+
import { isString, jsonObject, parseJsonValue, type JsonObject, type JsonValue } from "./json";
|
|
3
|
+
|
|
4
|
+
/** Blank out comments and trailing commas the way parseConfig does, then parse. Undefined when it is not a JSON object. */
|
|
5
|
+
function parseLoose(source: string): JsonObject | undefined {
|
|
6
|
+
let json = "";
|
|
7
|
+
let quoted = false;
|
|
8
|
+
for (let index = 0; index < source.length; index++) {
|
|
9
|
+
const character = source[index];
|
|
10
|
+
if (character === '"' && source[index - 1] !== "\\") quoted = !quoted;
|
|
11
|
+
if (!quoted && character === "/" && source[index + 1] === "/") {
|
|
12
|
+
index = source.indexOf("\n", index);
|
|
13
|
+
if (index < 0) break;
|
|
14
|
+
json += "\n";
|
|
15
|
+
} else if (!quoted && character === "/" && source[index + 1] === "*") {
|
|
16
|
+
index = source.indexOf("*/", index + 2);
|
|
17
|
+
if (index < 0) return undefined;
|
|
18
|
+
index++;
|
|
19
|
+
} else json += character;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return jsonObject(parseJsonValue(json.replace(/,\s*([}\]])/g, "$1")));
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ACCOUNT_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The `account` field of sproutboat.jsonc (like Wrangler's `account_id`): the Cloud team a deploy targets.
|
|
32
|
+
* It belongs to the CLI, not to the app, so the shared config schema does not know it.
|
|
33
|
+
*/
|
|
34
|
+
export function configAccount(source: string): string | undefined {
|
|
35
|
+
const account = parseLoose(source)?.account;
|
|
36
|
+
return isString(account) && account !== "" ? account : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `migrations_dir` (like Wrangler's, but top level: a Sproutboat app has its D1 databases by binding name only): the
|
|
41
|
+
* directory `d1 migrations` keeps its .sql files in, relative to the project. Defaults to `migrations`. Undefined when
|
|
42
|
+
* absent; an invalid value is reported by parseProjectConfig.
|
|
43
|
+
*/
|
|
44
|
+
export function configMigrationsDir(source: string): string | undefined {
|
|
45
|
+
const dir = parseLoose(source)?.migrations_dir;
|
|
46
|
+
return isString(dir) && dir !== "" ? dir : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MIGRATIONS_DIR_ERROR = 'migrations_dir must be a relative folder inside the project (for example "migrations")';
|
|
50
|
+
const validMigrationsDir = (dir: JsonValue | undefined): boolean =>
|
|
51
|
+
isString(dir) && dir !== "" && !dir.startsWith("/") && !/^[A-Za-z]:/.test(dir) && !dir.split(/[\\/]/).includes("..");
|
|
52
|
+
|
|
53
|
+
/** The `main` a project with assets and no `main` gets: a file that does not exist, written by `readProject` into `.sproutboat/`. */
|
|
54
|
+
export const ASSETS_ONLY_MAIN = "src/.assets-only.js";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The entry a Worker with only static assets runs, as Wrangler allows (`assets` and no `main`). The platform serves a matching
|
|
58
|
+
* file before any code runs; a request that matches none reaches this, and `env.<binding>.fetch` answers it with the project's
|
|
59
|
+
* `not_found_handling`: the single-page app shell, the nearest 404 page, or a plain 404.
|
|
60
|
+
*/
|
|
61
|
+
export function assetsOnlySource(binding: string): string {
|
|
62
|
+
return [
|
|
63
|
+
"// Written by sproutboat: this project has `assets` and no `main`, so it is a Worker with only static assets.",
|
|
64
|
+
"export default {",
|
|
65
|
+
" fetch(request, env) {",
|
|
66
|
+
` return env.${binding}.fetch(request);`,
|
|
67
|
+
" },",
|
|
68
|
+
"};",
|
|
69
|
+
"",
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `assets` with a directory and no `main`: the assets-only case. The entry and the binding it calls are filled in. */
|
|
74
|
+
function withAssetsOnlyEntry(config: JsonObject): JsonObject {
|
|
75
|
+
const assets = jsonObject(config.assets ?? null);
|
|
76
|
+
if (config.main !== undefined || assets === undefined) return config;
|
|
77
|
+
return { ...config, main: ASSETS_ONLY_MAIN, assets: { ...assets, binding: assets.binding ?? "ASSETS" } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* parseConfig for a file that may carry the CLI-owned fields `account` and `migrations_dir`: they are validated here and
|
|
82
|
+
* removed before the shared schema sees them.
|
|
83
|
+
*/
|
|
84
|
+
export function parseProjectConfig(source: string): ConfigValidation {
|
|
85
|
+
const loose = parseLoose(source);
|
|
86
|
+
if (loose === undefined) return parseConfig(source);
|
|
87
|
+
if (!("account" in loose || "migrations_dir" in loose)) {
|
|
88
|
+
const filled = withAssetsOnlyEntry(loose);
|
|
89
|
+
return filled === loose ? parseConfig(source) : parseConfig(JSON.stringify(filled));
|
|
90
|
+
}
|
|
91
|
+
const errors: string[] = [];
|
|
92
|
+
if ("account" in loose && (!isString(loose.account) || !ACCOUNT_RE.test(loose.account)))
|
|
93
|
+
errors.push("account must be the URL name of a team (lowercase letters, digits and dashes)");
|
|
94
|
+
if ("migrations_dir" in loose && !validMigrationsDir(loose.migrations_dir)) errors.push(MIGRATIONS_DIR_ERROR);
|
|
95
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
96
|
+
const rest = Object.fromEntries(
|
|
97
|
+
Object.entries(loose).filter(([key]) => key !== "account" && key !== "migrations_dir"),
|
|
98
|
+
);
|
|
99
|
+
return parseConfig(JSON.stringify(withAssetsOnlyEntry(rest)));
|
|
100
|
+
}
|
package/src/build.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { queueSettings, resourceRefs, type SproutboatConfig } from "./config";
|
|
|
7
7
|
import { ensureSqliteObject } from "./sqlite";
|
|
8
8
|
import { sqliteStamp } from "./sqlite";
|
|
9
9
|
import { bearsslStamp, ensureBearssl } from "./bearssl";
|
|
10
|
-
import { compileSprout, loadPrelude, porfforChannel, type Transport } from "./compile";
|
|
10
|
+
import { compileSprout, loadPrelude, ltoEnabled, porfforChannel, type Transport } from "./compile";
|
|
11
11
|
import { compileCached } from "./compile-cache";
|
|
12
12
|
import type { BuildTarget } from "./targets";
|
|
13
13
|
import {
|
|
@@ -51,6 +51,13 @@ export type BuildInput = {
|
|
|
51
51
|
* for the fast path deliberately.
|
|
52
52
|
*/
|
|
53
53
|
optimize?: "dev" | "release";
|
|
54
|
+
/**
|
|
55
|
+
* Link-time optimization for the alpha 22 standalone build. Defaults to on (`-flto -O3`, the release
|
|
56
|
+
* build). `false` keeps -O3 per unit but links without LTO: about 1.5 to 2x faster cold, a link of
|
|
57
|
+
* ~90 MB instead of 1.1 to 1.6 GB, and a binary that serves fewer requests per second. A separate
|
|
58
|
+
* input to the compile cache key, so the two kinds of binary never share an entry.
|
|
59
|
+
*/
|
|
60
|
+
lto?: boolean;
|
|
54
61
|
/** Reuse a previously compiled native sprout when only sidecar assets changed. */
|
|
55
62
|
reuseSproutPath?: string;
|
|
56
63
|
/** An isolated artifact directory, used by dev candidates to avoid mixing snapshots. */
|
|
@@ -297,6 +304,7 @@ export async function emitGenerated(input: BuildInput): Promise<{ generatedPath:
|
|
|
297
304
|
appName: input.config.name,
|
|
298
305
|
assets: bakedAssets,
|
|
299
306
|
optimize: input.optimize,
|
|
307
|
+
lto: input.lto,
|
|
300
308
|
versionMetadata,
|
|
301
309
|
emitOnly: true,
|
|
302
310
|
withRestore: input.withRestore,
|
|
@@ -334,6 +342,8 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
334
342
|
generatedSource,
|
|
335
343
|
target,
|
|
336
344
|
optimize: input.optimize ?? "release",
|
|
345
|
+
// a non-LTO link is a different binary from the release one; only the alpha 21 line's channels honour it
|
|
346
|
+
lto: porfforChannel(input.withRestore) !== "alpha-19" ? ltoEnabled({ ...input, target }) : true,
|
|
337
347
|
toolchain: toolchainStamp(),
|
|
338
348
|
toolchainPackageVersion,
|
|
339
349
|
porffor: [PORFFOR_COMMIT_FULL, PORFFOR_ARCHIVE_SHA256],
|
|
@@ -341,7 +351,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
341
351
|
channel: porfforChannel(input.withRestore),
|
|
342
352
|
// the alpha 22 channel's own inputs: the patched tree, the shim, the bridge, the build recipe, LTO
|
|
343
353
|
channelInputs:
|
|
344
|
-
porfforChannel(input.withRestore)
|
|
354
|
+
porfforChannel(input.withRestore) !== "alpha-19"
|
|
345
355
|
? [A21_STANDALONE_FINGERPRINT, process.env.SPROUTBOAT_FLTO ?? ""]
|
|
346
356
|
: null,
|
|
347
357
|
native: embedded ? [sqliteStamp(), bearsslStamp()] : subtle ? [bearsslStamp()] : [],
|
|
@@ -390,6 +400,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
390
400
|
],
|
|
391
401
|
extraCflags: tls ? ["-I", tls.includeDir] : [],
|
|
392
402
|
optimize: input.optimize,
|
|
403
|
+
lto: input.lto,
|
|
393
404
|
versionMetadata,
|
|
394
405
|
withRestore: input.withRestore,
|
|
395
406
|
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sproutboat.jsonc` bindings as the Sproutboat Cloud's app record carries them (sproutboat-cloud `src/record.ts`,
|
|
3
|
+
* `bindings`, `crons`, `queue_consumers`). The Cloud validates the same shape again on `PUT /v1/apps/<app>` and
|
|
4
|
+
* `POST /v1/apps/<app>/builds`; checking here means a mistake fails before anything is uploaded, in the CLI's words.
|
|
5
|
+
*
|
|
6
|
+
* Where a resource id comes from: on the Cloud each app's storage is private to it, so the binding is the name
|
|
7
|
+
* (`d1_databases: ["DB"]` is `d1/DB`, the name `d1 execute --remote` and `r2 object` address). A binding that needs a
|
|
8
|
+
* `resource_id` (kv, d1, r2, queue) gets its own name in lowercase, because the record's resource ids are lowercase;
|
|
9
|
+
* a queue gets its queue name. Nothing is provisioned: the host creates the storage on first use.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { queueSettings, resourceRefs, type SproutboatConfig } from "./config";
|
|
14
|
+
import { CloudError } from "./cloud";
|
|
15
|
+
|
|
16
|
+
export type RecordBinding = {
|
|
17
|
+
name: string;
|
|
18
|
+
kind: "kv" | "d1" | "r2" | "queue" | "do" | "ae" | "ratelimit" | "assets" | "secret";
|
|
19
|
+
resource_id: string | null;
|
|
20
|
+
config: Record<string, string | number>;
|
|
21
|
+
};
|
|
22
|
+
export type RecordQueueConsumer = {
|
|
23
|
+
queue: string;
|
|
24
|
+
max_batch_size: number;
|
|
25
|
+
max_batch_timeout_ms: number;
|
|
26
|
+
max_retries: number;
|
|
27
|
+
dead_letter_queue: string | null;
|
|
28
|
+
};
|
|
29
|
+
export type CloudBindings = {
|
|
30
|
+
bindings: RecordBinding[];
|
|
31
|
+
crons: string[];
|
|
32
|
+
queue_consumers: RecordQueueConsumer[];
|
|
33
|
+
/** Things the Cloud ignores, said once before the deploy. */
|
|
34
|
+
warnings: string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The record's own limits (sproutboat-cloud `src/record.ts`). */
|
|
38
|
+
export const MAX_BINDINGS = 64;
|
|
39
|
+
export const MAX_CRONS = 16;
|
|
40
|
+
export const MAX_QUEUE_CONSUMERS = 16;
|
|
41
|
+
const RESOURCE_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
42
|
+
const CRON_CHARS_RE = /^[0-9A-Za-z*,/ -]+$/;
|
|
43
|
+
|
|
44
|
+
/** Cloudflare's defaults for a queue consumer, which the runtime applies when sproutboat.jsonc leaves a setting out. */
|
|
45
|
+
const QUEUE_DEFAULTS = { maxBatchSize: 10, maxBatchTimeout: 0, maxRetries: 3 };
|
|
46
|
+
|
|
47
|
+
/** `resource_id` for a binding or queue name: lowercase, so `DB` is `db` and `EMAILS` is `emails`. */
|
|
48
|
+
function resourceIdFor(what: string, name: string): string {
|
|
49
|
+
const id = name.toLowerCase();
|
|
50
|
+
if (!RESOURCE_ID_RE.test(id))
|
|
51
|
+
throw new CloudError(
|
|
52
|
+
`${what} "${name}" cannot be a Cloud resource name: lowercased it must be 1 to 64 characters, start with a letter or digit, and use only letters, digits, "_" and "-"`,
|
|
53
|
+
);
|
|
54
|
+
return id;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A cron as the Cloud stores it: single spaces, five fields, at most 128 characters of the allowed set. */
|
|
58
|
+
function normalizeCron(expression: string): string {
|
|
59
|
+
const cron = expression.trim().split(/\s+/).join(" ");
|
|
60
|
+
if (cron.length > 128 || !CRON_CHARS_RE.test(cron) || cron.split(" ").length !== 5)
|
|
61
|
+
throw new CloudError(
|
|
62
|
+
`triggers.crons: "${expression}" is not a cron the Cloud accepts (5 fields of digits, letters and * , / -, at most 128 characters)`,
|
|
63
|
+
);
|
|
64
|
+
return cron;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type Mappable = Pick<
|
|
68
|
+
SproutboatConfig,
|
|
69
|
+
| "kv_namespaces"
|
|
70
|
+
| "d1_databases"
|
|
71
|
+
| "r2_buckets"
|
|
72
|
+
| "queues"
|
|
73
|
+
| "analytics_engine_datasets"
|
|
74
|
+
| "durable_objects"
|
|
75
|
+
| "ratelimiters"
|
|
76
|
+
| "secrets"
|
|
77
|
+
| "assets"
|
|
78
|
+
| "triggers"
|
|
79
|
+
>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Every binding kind in sproutboat.jsonc as a record entry, plus `crons` and one `queue_consumers` entry per queue
|
|
83
|
+
* (the worker consumes every queue it binds). `serviceBindings` is how many service bindings the deploy adds, so the
|
|
84
|
+
* 64-binding limit counts them. Throws CloudError for anything the Cloud would refuse.
|
|
85
|
+
*/
|
|
86
|
+
export function mapCloudBindings(config: Mappable, serviceBindings = 0): CloudBindings {
|
|
87
|
+
const bindings: RecordBinding[] = [];
|
|
88
|
+
const warnings: string[] = [];
|
|
89
|
+
const pinned: string[] = [];
|
|
90
|
+
|
|
91
|
+
for (const [kind, field] of [
|
|
92
|
+
["kv", "kv_namespaces"],
|
|
93
|
+
["d1", "d1_databases"],
|
|
94
|
+
["r2", "r2_buckets"],
|
|
95
|
+
] as const)
|
|
96
|
+
for (const ref of resourceRefs(config[field])) {
|
|
97
|
+
if (ref.id !== undefined) pinned.push(`${field}.${ref.binding}`);
|
|
98
|
+
bindings.push({
|
|
99
|
+
name: ref.binding,
|
|
100
|
+
kind,
|
|
101
|
+
resource_id: resourceIdFor(`${field} binding`, ref.binding),
|
|
102
|
+
config: {},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const queue_consumers: RecordQueueConsumer[] = [];
|
|
107
|
+
const queueNames = new Set<string>();
|
|
108
|
+
for (const queue of queueSettings(config.queues)) {
|
|
109
|
+
const resource = resourceIdFor("queue", queue.queue);
|
|
110
|
+
if (queueNames.has(resource))
|
|
111
|
+
throw new CloudError(
|
|
112
|
+
`queues: two bindings name the queue "${resource}" (queue names are lowercase on the Cloud)`,
|
|
113
|
+
);
|
|
114
|
+
queueNames.add(resource);
|
|
115
|
+
const settings: RecordBinding["config"] = { queue: queue.queue };
|
|
116
|
+
if (queue.retryDelay !== undefined) settings.retry_delay = queue.retryDelay;
|
|
117
|
+
bindings.push({ name: queue.binding, kind: "queue", resource_id: resource, config: settings });
|
|
118
|
+
queue_consumers.push({
|
|
119
|
+
queue: resource,
|
|
120
|
+
max_batch_size: queue.maxBatchSize ?? QUEUE_DEFAULTS.maxBatchSize,
|
|
121
|
+
max_batch_timeout_ms: Math.round((queue.maxBatchTimeout ?? QUEUE_DEFAULTS.maxBatchTimeout) * 1000),
|
|
122
|
+
max_retries: queue.maxRetries ?? QUEUE_DEFAULTS.maxRetries,
|
|
123
|
+
dead_letter_queue:
|
|
124
|
+
queue.deadLetterQueue === undefined ? null : resourceIdFor("dead_letter_queue", queue.deadLetterQueue),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
for (const ref of resourceRefs(config.queues)) if (ref.id !== undefined) pinned.push(`queues.${ref.binding}`);
|
|
128
|
+
|
|
129
|
+
for (const name of config.analytics_engine_datasets ?? [])
|
|
130
|
+
bindings.push({ name, kind: "ae", resource_id: null, config: {} });
|
|
131
|
+
for (const [name, className] of Object.entries(config.durable_objects ?? {}))
|
|
132
|
+
bindings.push({ name, kind: "do", resource_id: null, config: { class_name: className } });
|
|
133
|
+
for (const limiter of config.ratelimiters ?? [])
|
|
134
|
+
bindings.push({
|
|
135
|
+
name: limiter.binding,
|
|
136
|
+
kind: "ratelimit",
|
|
137
|
+
resource_id: null,
|
|
138
|
+
config: { limit: limiter.limit, period: limiter.period },
|
|
139
|
+
});
|
|
140
|
+
if (config.assets?.binding)
|
|
141
|
+
bindings.push({ name: config.assets.binding, kind: "assets", resource_id: null, config: {} });
|
|
142
|
+
for (const name of config.secrets ?? []) bindings.push({ name, kind: "secret", resource_id: null, config: {} });
|
|
143
|
+
|
|
144
|
+
if (bindings.length + serviceBindings > MAX_BINDINGS)
|
|
145
|
+
throw new CloudError(
|
|
146
|
+
`sproutboat.jsonc declares ${bindings.length + serviceBindings} bindings (secrets, services and assets count); the Cloud takes at most ${MAX_BINDINGS} per app`,
|
|
147
|
+
);
|
|
148
|
+
if (queue_consumers.length > MAX_QUEUE_CONSUMERS)
|
|
149
|
+
throw new CloudError(
|
|
150
|
+
`sproutboat.jsonc binds ${queue_consumers.length} queues, and each one is consumed by the app; the Cloud takes at most ${MAX_QUEUE_CONSUMERS} queue consumers per app`,
|
|
151
|
+
);
|
|
152
|
+
const crons = (config.triggers?.crons ?? []).map(normalizeCron);
|
|
153
|
+
if (crons.length > MAX_CRONS)
|
|
154
|
+
throw new CloudError(`triggers.crons lists ${crons.length} crons; the Cloud takes at most ${MAX_CRONS} per app`);
|
|
155
|
+
|
|
156
|
+
if (pinned.length > 0)
|
|
157
|
+
warnings.push(
|
|
158
|
+
`${pinned.join(", ")} pin${pinned.length === 1 ? "s" : ""} an account-level id; on the Cloud each app's storage is private to it, so the id is ignored and the app gets its own`,
|
|
159
|
+
);
|
|
160
|
+
return { bindings, crons, queue_consumers, warnings };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The names a module exports, as far as its source says. `null` when an `export *` hides some. */
|
|
164
|
+
function exportedNames(source: string): Set<string> | null {
|
|
165
|
+
const code = source
|
|
166
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
167
|
+
.replace(/(^|[^:])\/\/.*$/gm, "$1")
|
|
168
|
+
.replace(/"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/g, '""');
|
|
169
|
+
if (/\bexport\s*\*\s*(?!as\b)from\b/.test(code)) return null;
|
|
170
|
+
const names = new Set<string>();
|
|
171
|
+
for (const m of code.matchAll(
|
|
172
|
+
/\bexport\s+(?:default\s+)?(?:async\s+)?(?:class|function\*?|const|let|var)\s+([A-Za-z_$][\w$]*)/g,
|
|
173
|
+
))
|
|
174
|
+
names.add(m[1]!);
|
|
175
|
+
for (const m of code.matchAll(/\bexport\s*(?:type\s*)?\{([^}]*)\}/g))
|
|
176
|
+
for (const part of m[1]!.split(",")) {
|
|
177
|
+
const exported = part
|
|
178
|
+
.trim()
|
|
179
|
+
.split(/\s+as\s+/)
|
|
180
|
+
.at(-1)
|
|
181
|
+
?.trim();
|
|
182
|
+
if (exported) names.add(exported);
|
|
183
|
+
}
|
|
184
|
+
for (const m of code.matchAll(/\bexport\s*\*\s*as\s+([A-Za-z_$][\w$]*)\s+from\b/g)) names.add(m[1]!);
|
|
185
|
+
return names;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Every class named in `durable_objects` must be exported by the entry module, or the app starts and then fails at the
|
|
190
|
+
* first Durable Object call. Checked from the source text: a source with an `export *` cannot be judged and passes, and so
|
|
191
|
+
* does an entry the CLI cannot read (the build reports that itself).
|
|
192
|
+
*/
|
|
193
|
+
export function checkDurableObjectExports(config: Mappable & Pick<SproutboatConfig, "main">, directory: string): void {
|
|
194
|
+
const classes = Object.entries(config.durable_objects ?? {});
|
|
195
|
+
if (classes.length === 0) return;
|
|
196
|
+
const entry = resolve(directory, config.main);
|
|
197
|
+
if (!existsSync(entry)) return;
|
|
198
|
+
const exported = exportedNames(readFileSync(entry, "utf8"));
|
|
199
|
+
if (exported === null) return;
|
|
200
|
+
for (const [binding, className] of classes)
|
|
201
|
+
if (!exported.has(className))
|
|
202
|
+
throw new CloudError(
|
|
203
|
+
`durable_objects ${binding} names the class "${className}", which ${config.main} does not export; add \`export class ${className}\` (or re-export it) so the Cloud can run it`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The plan as lines, for `deploy` and `--dry-run`: what each binding becomes on the Cloud. */
|
|
208
|
+
export function describeCloudBindings(plan: CloudBindings): string[] {
|
|
209
|
+
const lines: string[] = [];
|
|
210
|
+
for (const b of plan.bindings) {
|
|
211
|
+
const target =
|
|
212
|
+
b.kind === "do"
|
|
213
|
+
? `class ${String(b.config.class_name)}`
|
|
214
|
+
: b.kind === "ratelimit"
|
|
215
|
+
? `${String(b.config.limit)} per ${String(b.config.period)} s`
|
|
216
|
+
: b.resource_id !== null
|
|
217
|
+
? `-> ${b.resource_id}`
|
|
218
|
+
: "";
|
|
219
|
+
lines.push(`${b.kind.padEnd(9)} ${b.name}${target ? ` ${target}` : ""}`);
|
|
220
|
+
}
|
|
221
|
+
for (const c of plan.queue_consumers)
|
|
222
|
+
lines.push(
|
|
223
|
+
`consumer ${c.queue} batch ${c.max_batch_size} / ${c.max_batch_timeout_ms} ms, ${c.max_retries} retries${c.dead_letter_queue ? `, dead letters to ${c.dead_letter_queue}` : ""}`,
|
|
224
|
+
);
|
|
225
|
+
for (const cron of plan.crons) lines.push(`cron ${cron}`);
|
|
226
|
+
return lines;
|
|
227
|
+
}
|