sproutboat 0.54.0 → 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 +32 -0
- package/README.md +32 -8
- package/SURFACE.md +7 -7
- package/package.json +20 -12
- package/scripts/embed-templates.ts +87 -0
- package/src/account-field.ts +33 -2
- package/src/build.ts +3 -3
- package/src/cloud-bindings.ts +227 -0
- package/src/cloud-deploy.ts +67 -25
- package/src/cloud-domains.ts +55 -0
- package/src/cloud-fake.ts +90 -4
- package/src/compile.ts +18 -15
- package/src/dev.ts +67 -4
- package/src/init.ts +328 -0
- package/src/main.ts +112 -132
- package/src/options.ts +13 -4
- package/src/surface.ts +12 -11
- 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/main.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { mkdir, open, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, open, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { existsSync, rmSync } from "node:fs";
|
|
4
|
+
import { isIP } from "node:net";
|
|
4
5
|
import { basename, dirname, relative, resolve } from "node:path";
|
|
5
6
|
import { tmpdir } from "node:os";
|
|
6
7
|
import { isBoolean, isSafeInteger, isString, jsonObject, parseJsonValue, type JsonObject } from "./json";
|
|
@@ -8,7 +9,8 @@ import type { AssetFiles } from "./assets";
|
|
|
8
9
|
import { pinBindingId, resourceRefs, type SproutboatConfig } from "./config";
|
|
9
10
|
import { validateHttpSyncSource } from "./source";
|
|
10
11
|
import { buildArtifact, emitGenerated } from "./build";
|
|
11
|
-
import {
|
|
12
|
+
import { LEGACY_TYPES_FILE, TYPES_FILE } from "./types-gen";
|
|
13
|
+
import { checkTypes, parseTypesArgs, planTypes, refreshTypes, removeLegacyTypes, TYPES_USAGE } from "./types-cmd";
|
|
12
14
|
import { bundleHandler, BundleError, type BundleResult } from "./bundle";
|
|
13
15
|
import { runDev } from "./dev";
|
|
14
16
|
import { buildStandalone, emitStandalone, isReleaseBuild } from "./standalone-build";
|
|
@@ -29,7 +31,13 @@ import {
|
|
|
29
31
|
import { extractGlobalFlags, globalFlags, locateProject, setGlobalFlags } from "./global-flags";
|
|
30
32
|
import { checkOptions, ENDPOINT_COMMANDS } from "./options";
|
|
31
33
|
import { chooseTarget, TargetError, type Target } from "./target";
|
|
32
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
ASSETS_ONLY_MAIN,
|
|
36
|
+
assetsOnlySource,
|
|
37
|
+
configAccount,
|
|
38
|
+
configMigrationsDir,
|
|
39
|
+
parseProjectConfig,
|
|
40
|
+
} from "./account-field";
|
|
33
41
|
import { DATA_ERROR_STATUSES } from "./data-errors";
|
|
34
42
|
import {
|
|
35
43
|
cloudLogout,
|
|
@@ -58,6 +66,12 @@ import {
|
|
|
58
66
|
tailBuild,
|
|
59
67
|
waitForApply,
|
|
60
68
|
} from "./cloud-deploy";
|
|
69
|
+
import {
|
|
70
|
+
checkDurableObjectExports,
|
|
71
|
+
describeCloudBindings,
|
|
72
|
+
mapCloudBindings,
|
|
73
|
+
type CloudBindings,
|
|
74
|
+
} from "./cloud-bindings";
|
|
61
75
|
import {
|
|
62
76
|
APPLY_USAGE,
|
|
63
77
|
applyMigrations,
|
|
@@ -84,6 +98,7 @@ import {
|
|
|
84
98
|
putCloudSecret,
|
|
85
99
|
readSecretsFile,
|
|
86
100
|
} from "./cloud-secrets";
|
|
101
|
+
import { cloudDomainsUnsupported, describeCloudRoutes, listCloudRoutes } from "./cloud-domains";
|
|
87
102
|
import { parseBulkSecrets, type BulkPlan } from "./secrets-bulk";
|
|
88
103
|
import {
|
|
89
104
|
cloudTarget,
|
|
@@ -129,7 +144,7 @@ import {
|
|
|
129
144
|
import { notifyIfOutdated } from "./update-check";
|
|
130
145
|
import { controlVersionWarning } from "./api-version";
|
|
131
146
|
import { amber, bold, dim, leaf, ok, rose } from "./style";
|
|
132
|
-
import {
|
|
147
|
+
import { InitError, parseInitArgs, runInit, terminalInitIo } from "./init";
|
|
133
148
|
import { pathToFileURL } from "node:url";
|
|
134
149
|
import { constants as osConstants } from "node:os";
|
|
135
150
|
import { inspectToolchain } from "./toolchain";
|
|
@@ -310,28 +325,6 @@ function parseToken(source: string): string | undefined {
|
|
|
310
325
|
return record && isString(record.token) ? record.token : undefined;
|
|
311
326
|
}
|
|
312
327
|
|
|
313
|
-
const starterConfig = (name: string) => `{
|
|
314
|
-
"$schema": "https://sproutboat.com/schema.json",
|
|
315
|
-
"name": "${name}",
|
|
316
|
-
"main": "src/index.js",
|
|
317
|
-
"compatibility_date": "2026-09-28"
|
|
318
|
-
}
|
|
319
|
-
`;
|
|
320
|
-
const starterHandler = `export default {
|
|
321
|
-
fetch(request, env, ctx) {
|
|
322
|
-
return new Response("hello from Sproutboat");
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
`;
|
|
326
|
-
// .sproutboat/ holds build output (dist/) and `dev`'s local broker state
|
|
327
|
-
// (dev/, including its SQLite files) — neither belongs in version control.
|
|
328
|
-
// .dev.vars carries secret values for `sproutboat dev`, same convention as
|
|
329
|
-
// Wrangler's file of the same name.
|
|
330
|
-
const starterGitignore = `.sproutboat/
|
|
331
|
-
.dev.vars
|
|
332
|
-
node_modules/
|
|
333
|
-
`;
|
|
334
|
-
|
|
335
328
|
/** An operational failure — the command was invoked correctly but could not complete. Exit 1. */
|
|
336
329
|
function fail(message: string): never {
|
|
337
330
|
console.error(`${rose("✗")} ${message}`);
|
|
@@ -383,10 +376,20 @@ async function readProjectConfig(directory?: string) {
|
|
|
383
376
|
};
|
|
384
377
|
}
|
|
385
378
|
|
|
379
|
+
/** What to call the entry in messages: its path, or "assets only" when the project has no `main`. */
|
|
380
|
+
const entryLabel = (config: { main: string }) => (config.main === ASSETS_ONLY_MAIN ? "assets only" : config.main);
|
|
381
|
+
|
|
386
382
|
/** Full build input: configuration plus entry point, resolved imports and checks. */
|
|
387
383
|
async function readProject(directory?: string) {
|
|
388
384
|
const project = await readProjectConfig(directory);
|
|
389
|
-
|
|
385
|
+
let sourcePath = resolve(project.directory, project.config.main);
|
|
386
|
+
if (project.config.main === ASSETS_ONLY_MAIN) {
|
|
387
|
+
// A Worker with only static assets: there is no entry file, so the small one that hands every miss to the assets is written
|
|
388
|
+
// into .sproutboat/ (the local state folder) and bundled like any other.
|
|
389
|
+
sourcePath = resolve(project.directory, ".sproutboat", "assets-only.js");
|
|
390
|
+
await mkdir(dirname(sourcePath), { recursive: true });
|
|
391
|
+
await writeFile(sourcePath, assetsOnlySource(project.config.assets?.binding ?? "ASSETS"));
|
|
392
|
+
}
|
|
390
393
|
let source: string;
|
|
391
394
|
try {
|
|
392
395
|
source = await readFile(sourcePath, "utf8");
|
|
@@ -409,97 +412,57 @@ async function readProject(directory?: string) {
|
|
|
409
412
|
return { ...project, sourcePath, source, bundle };
|
|
410
413
|
}
|
|
411
414
|
|
|
412
|
-
async function init(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const directory = resolve(process.cwd(), name);
|
|
416
|
-
const configPath = resolve(directory, "sproutboat.jsonc");
|
|
417
|
-
const handlerPath = resolve(directory, "src/index.js");
|
|
418
|
-
// Check both targets before writing either — `name` can collide with an
|
|
419
|
-
// unrelated existing directory, and a second `wx` write failing partway
|
|
420
|
-
// through used to crash with a raw EEXIST stack trace after already having
|
|
421
|
-
// created sproutboat.jsonc, leaving a half-scaffolded project behind.
|
|
422
|
-
for (const [path, label] of [
|
|
423
|
-
[configPath, "sproutboat.jsonc"],
|
|
424
|
-
[handlerPath, "src/index.js"],
|
|
425
|
-
] as const) {
|
|
426
|
-
if (await Bun.file(path).exists()) fail(`${basename(directory)} already contains ${label}`);
|
|
427
|
-
}
|
|
428
|
-
// Plan package.json before writing anything, so an unreadable one stops init
|
|
429
|
-
// instead of leaving a half-scaffolded project.
|
|
430
|
-
const packagePath = resolve(directory, "package.json");
|
|
431
|
-
let existingPackage: string | undefined;
|
|
432
|
-
try {
|
|
433
|
-
existingPackage = await readFile(packagePath, "utf8");
|
|
434
|
-
} catch {
|
|
435
|
-
existingPackage = undefined;
|
|
436
|
-
}
|
|
437
|
-
let packagePlan: ReturnType<typeof planPackageJson>;
|
|
415
|
+
async function init(args: string[]) {
|
|
416
|
+
const parsed = parseInitArgs(args);
|
|
417
|
+
if ("error" in parsed) usageError(parsed.error, usageOf("init"));
|
|
438
418
|
try {
|
|
439
|
-
|
|
419
|
+
await runInit(parsed, terminalInitIo(CLI_VERSION));
|
|
440
420
|
} catch (cause) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
await writeFile(configPath, starterConfig(name), { flag: "wx" });
|
|
445
|
-
await writeFile(handlerPath, starterHandler, { flag: "wx" });
|
|
446
|
-
console.log(`Created ${basename(directory)}/sproutboat.jsonc`);
|
|
447
|
-
console.log(`Created ${basename(directory)}/src/index.js`);
|
|
448
|
-
// Unlike the two files above, an existing .gitignore here is not a sign this
|
|
449
|
-
// isn't a fresh project (`name` can collide with an unrelated directory) —
|
|
450
|
-
// leave it alone rather than failing init or clobbering it.
|
|
451
|
-
const gitignorePath = resolve(directory, ".gitignore");
|
|
452
|
-
if (!(await Bun.file(gitignorePath).exists())) {
|
|
453
|
-
await writeFile(gitignorePath, starterGitignore, { flag: "wx" });
|
|
454
|
-
console.log(`Created ${basename(directory)}/.gitignore`);
|
|
455
|
-
}
|
|
456
|
-
if (packagePlan.created || packagePlan.addedDependency || packagePlan.addedScripts.length > 0) {
|
|
457
|
-
await writeFile(packagePath, packagePlan.text);
|
|
458
|
-
console.log(`${packagePlan.created ? "Created" : "Updated"} ${basename(directory)}/package.json`);
|
|
459
|
-
}
|
|
460
|
-
for (const script of packagePlan.skippedScripts)
|
|
461
|
-
console.log(
|
|
462
|
-
amber(
|
|
463
|
-
` ! package.json already has a "${script}" script; left it as is (sproutboat's would be "sproutboat ${script}")`,
|
|
464
|
-
),
|
|
465
|
-
);
|
|
466
|
-
const manager = detectPackageManager(directory);
|
|
467
|
-
console.log(`\nNext: cd ${basename(directory)} && ${installCommand(manager)} && ${runScriptCommand(manager, "dev")}`);
|
|
468
|
-
console.log(dim(`After "sproutboat login", ship it with ${runScriptCommand(manager, "deploy")}.`));
|
|
421
|
+
if (cause instanceof InitError) fail(cause.message);
|
|
422
|
+
throw cause;
|
|
423
|
+
}
|
|
469
424
|
}
|
|
470
425
|
|
|
471
426
|
async function check(directory?: string) {
|
|
472
427
|
const project = await readProject(directory);
|
|
473
|
-
console.log(ok(`check passed — ${project.config.name} (${project.config
|
|
428
|
+
console.log(ok(`check passed — ${project.config.name} (${entryLabel(project.config)}, native-fetch)`));
|
|
474
429
|
}
|
|
475
430
|
|
|
476
|
-
async function types(
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
431
|
+
async function types(args: string[]) {
|
|
432
|
+
const parsed = parseTypesArgs(args);
|
|
433
|
+
if ("error" in parsed) usageError(parsed.error, TYPES_USAGE);
|
|
434
|
+
// A positional that is a directory is the project directory, as it was before the positional became the output file.
|
|
435
|
+
const project = await readProjectConfig(
|
|
436
|
+
parsed.positional !== undefined && existsSync(parsed.positional) && (await stat(parsed.positional)).isDirectory()
|
|
437
|
+
? parsed.positional
|
|
438
|
+
: undefined,
|
|
439
|
+
);
|
|
440
|
+
const plan = await planTypes(parsed, project.config, project.directory);
|
|
441
|
+
const shown = relative(process.cwd(), plan.path) || TYPES_FILE;
|
|
442
|
+
if (parsed.check) {
|
|
443
|
+
const state = await checkTypes(plan);
|
|
444
|
+
if (state === "current") {
|
|
445
|
+
console.log(ok(`types at ${shown} are up to date`));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
console.error(
|
|
449
|
+
`${rose("✗")} ${state === "missing" ? `${shown} does not exist` : `${shown} is out of date`}. Run \`sproutboat types\` to write it.`,
|
|
450
|
+
);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
await mkdir(dirname(plan.path), { recursive: true });
|
|
454
|
+
await writeFile(plan.path, plan.text);
|
|
455
|
+
console.log(ok(`wrote ${shown}`));
|
|
456
|
+
if (await removeLegacyTypes(project.directory))
|
|
457
|
+
console.log(dim(` removed ${LEGACY_TYPES_FILE}, which this file replaces`));
|
|
458
|
+
console.log(dim(' Commit it, and add it to the "include" of your tsconfig. Rerun after changing sproutboat.jsonc.'));
|
|
482
459
|
}
|
|
483
460
|
|
|
484
|
-
/**
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
* differs, which keeps it out of the way of git and of file watchers.
|
|
490
|
-
*/
|
|
491
|
-
async function refreshTypes(projectDir: string, config: SproutboatConfig): Promise<void> {
|
|
492
|
-
const path = resolve(projectDir, TYPES_FILE);
|
|
493
|
-
const wanted = generateTypes(config);
|
|
494
|
-
try {
|
|
495
|
-
if ((await readFile(path, "utf8")) === wanted) return;
|
|
496
|
-
} catch {
|
|
497
|
-
// No file yet: only write one if the project already opted in by having a
|
|
498
|
-
// tsconfig. Generating into a plain JavaScript project is noise.
|
|
499
|
-
if (!(await Bun.file(resolve(projectDir, "tsconfig.json")).exists())) return;
|
|
500
|
-
}
|
|
501
|
-
await writeFile(path, wanted);
|
|
502
|
-
console.log(dim(` ${TYPES_FILE} refreshed from sproutboat.jsonc`));
|
|
461
|
+
/** `dev` and `deploy`: keep the default generated types current (types-cmd.ts says when it does and does not). */
|
|
462
|
+
async function refreshProjectTypes(projectDir: string, config: SproutboatConfig): Promise<void> {
|
|
463
|
+
const { wrote, removedLegacy } = await refreshTypes(projectDir, config);
|
|
464
|
+
if (wrote) console.log(dim(` ${TYPES_FILE} refreshed from sproutboat.jsonc`));
|
|
465
|
+
if (removedLegacy) console.log(dim(` removed ${LEGACY_TYPES_FILE}, which it replaces`));
|
|
503
466
|
}
|
|
504
467
|
|
|
505
468
|
async function build(directory?: string, target: BuildTarget = "linux-x86_64") {
|
|
@@ -616,11 +579,13 @@ async function emitStandaloneUnit(directory: string | undefined): Promise<string
|
|
|
616
579
|
}
|
|
617
580
|
|
|
618
581
|
/** #62 — build for this machine, run it against a real broker, rebuild on save. */
|
|
619
|
-
const DEV_USAGE =
|
|
582
|
+
const DEV_USAGE =
|
|
583
|
+
"dev [project-dir] [--port <n>] [--ip <address>] [--no-watch] [-e, --env <name>] [--env-file <file>]...";
|
|
620
584
|
|
|
621
585
|
async function dev(args: string[]) {
|
|
622
586
|
let directory: string | undefined;
|
|
623
587
|
let portArg: string | undefined;
|
|
588
|
+
let host: string | undefined;
|
|
624
589
|
// --env and --env-file are global flags, already taken out of `args` (global-flags.ts).
|
|
625
590
|
const envName = globalFlags().env;
|
|
626
591
|
const envFiles = globalFlags().envFiles.map((file) => resolve(file));
|
|
@@ -632,15 +597,22 @@ async function dev(args: string[]) {
|
|
|
632
597
|
if (value === undefined || value.startsWith("--")) usageError(`${arg} needs a value`, DEV_USAGE);
|
|
633
598
|
at += 1;
|
|
634
599
|
portArg = value;
|
|
600
|
+
} else if (arg === "--ip") {
|
|
601
|
+
const value = args[at + 1];
|
|
602
|
+
if (value === undefined || value.startsWith("--")) usageError(`${arg} needs a value`, DEV_USAGE);
|
|
603
|
+
at += 1;
|
|
604
|
+
host = value;
|
|
635
605
|
} else if (!arg.startsWith("--") && directory === undefined) directory = arg;
|
|
636
606
|
}
|
|
637
607
|
const port = Number(portArg ?? 8787);
|
|
638
608
|
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) usageError(`invalid --port: ${portArg}`, DEV_USAGE);
|
|
609
|
+
if (host !== undefined && !isIP(host) && host !== "localhost")
|
|
610
|
+
usageError(`invalid --ip: ${host} (an IPv4 or IPv6 address, or localhost)`, DEV_USAGE);
|
|
639
611
|
if (envName !== undefined && !/^[A-Za-z0-9_-]+$/.test(envName))
|
|
640
612
|
usageError("--env takes a name (letters, digits, - and _)", DEV_USAGE);
|
|
641
613
|
for (const file of envFiles) if (!existsSync(file)) fail(`env file not found: ${file}`);
|
|
642
614
|
const project = await readProject(directory);
|
|
643
|
-
await
|
|
615
|
+
await refreshProjectTypes(project.directory, project.config);
|
|
644
616
|
console.log(dim(`Building ${project.config.name} for this machine (${hostTarget()})…`));
|
|
645
617
|
await runDev({
|
|
646
618
|
projectDir: project.directory,
|
|
@@ -648,6 +620,7 @@ async function dev(args: string[]) {
|
|
|
648
620
|
sourcePath: project.sourcePath,
|
|
649
621
|
source: project.bundle.code,
|
|
650
622
|
port,
|
|
623
|
+
host,
|
|
651
624
|
envFiles,
|
|
652
625
|
envName,
|
|
653
626
|
watch: !args.includes("--no-watch"),
|
|
@@ -655,7 +628,7 @@ async function dev(args: string[]) {
|
|
|
655
628
|
// files changed, so the bundle captured at startup is stale by definition.
|
|
656
629
|
rebuild: async () => {
|
|
657
630
|
const next = await readProject(directory);
|
|
658
|
-
await
|
|
631
|
+
await refreshProjectTypes(next.directory, next.config);
|
|
659
632
|
return { config: next.config, sourcePath: next.sourcePath, source: next.bundle.code };
|
|
660
633
|
},
|
|
661
634
|
});
|
|
@@ -890,7 +863,7 @@ async function deploy(args: string[]) {
|
|
|
890
863
|
artifactDir = built.artifact.artifactDir;
|
|
891
864
|
config = built.project.config;
|
|
892
865
|
// After provisioning: ids may have been pinned into the config just now.
|
|
893
|
-
await
|
|
866
|
+
await refreshProjectTypes(built.project.directory, built.project.config);
|
|
894
867
|
}
|
|
895
868
|
const manifest = Bun.file(resolve(artifactDir, "manifest.json"));
|
|
896
869
|
const sprout = Bun.file(resolve(artifactDir, "sprout"));
|
|
@@ -1247,25 +1220,14 @@ async function deployCloud(args: string[], apiUrl: string, flags: ReturnType<typ
|
|
|
1247
1220
|
const hosts = [hostFor(name, team)];
|
|
1248
1221
|
|
|
1249
1222
|
const cfg = project.config;
|
|
1250
|
-
const unsent = Object.entries({
|
|
1251
|
-
kv_namespaces: cfg.kv_namespaces?.length,
|
|
1252
|
-
d1_databases: cfg.d1_databases?.length,
|
|
1253
|
-
r2_buckets: cfg.r2_buckets?.length,
|
|
1254
|
-
queues: cfg.queues?.length,
|
|
1255
|
-
analytics_engine_datasets: cfg.analytics_engine_datasets?.length,
|
|
1256
|
-
durable_objects: Object.keys(cfg.durable_objects ?? {}).length,
|
|
1257
|
-
ratelimiters: cfg.ratelimiters?.length,
|
|
1258
|
-
"triggers.crons": cfg.triggers?.crons?.length,
|
|
1259
|
-
})
|
|
1260
|
-
.filter(([, count]) => (count ?? 0) > 0)
|
|
1261
|
-
.map(([key]) => key);
|
|
1262
|
-
if (unsent.length > 0)
|
|
1263
|
-
io.warn(
|
|
1264
|
-
`! ${unsent.join(", ")} in sproutboat.jsonc are not sent to the Cloud by the CLI yet; the app runs without them`,
|
|
1265
|
-
);
|
|
1266
|
-
|
|
1267
1223
|
const services = cfg.services ?? [];
|
|
1268
1224
|
guardSync(() => checkServiceBindings(name, services, cfg.compatibility_date));
|
|
1225
|
+
let cloudBindings!: CloudBindings;
|
|
1226
|
+
guardSync(() => {
|
|
1227
|
+
cloudBindings = mapCloudBindings(cfg, services.length);
|
|
1228
|
+
checkDurableObjectExports(cfg, project.directory);
|
|
1229
|
+
});
|
|
1230
|
+
for (const warning of cloudBindings.warnings) io.warn(`! ${warning}`);
|
|
1269
1231
|
let secretValues: Record<string, string> | undefined;
|
|
1270
1232
|
const secretsFile = flags["--secrets-file"];
|
|
1271
1233
|
if (secretsFile !== undefined) {
|
|
@@ -1299,6 +1261,7 @@ async function deployCloud(args: string[], apiUrl: string, flags: ReturnType<typ
|
|
|
1299
1261
|
console.log(`endpoint ${apiUrl}`);
|
|
1300
1262
|
console.log(`route https://${hosts[0]}`);
|
|
1301
1263
|
if (assets) console.log(`assets ${assets.files} files`);
|
|
1264
|
+
for (const line of describeCloudBindings(cloudBindings)) console.log(`binding ${line}`);
|
|
1302
1265
|
if (services.length > 0)
|
|
1303
1266
|
console.log(`services ${services.map((entry) => `${entry.binding} -> ${entry.service}`).join(", ")}`);
|
|
1304
1267
|
if (secretValues) console.log(`secrets ${Object.keys(secretValues).length} from ${secretsFile} (set, others kept)`);
|
|
@@ -1336,7 +1299,7 @@ async function deployCloud(args: string[], apiUrl: string, flags: ReturnType<typ
|
|
|
1336
1299
|
};
|
|
1337
1300
|
if (fromSource) {
|
|
1338
1301
|
const sourcePath = await emitStandaloneUnit(directory);
|
|
1339
|
-
console.log(` Bundling ${cfg
|
|
1302
|
+
console.log(` Bundling ${entryLabel(cfg)} (${formatKiB(Bun.file(sourcePath).size)})`);
|
|
1340
1303
|
const controller = new AbortController();
|
|
1341
1304
|
const interrupt = () => controller.abort();
|
|
1342
1305
|
process.once("SIGINT", interrupt);
|
|
@@ -1348,6 +1311,7 @@ async function deployCloud(args: string[], apiUrl: string, flags: ReturnType<typ
|
|
|
1348
1311
|
tokenFromEnv,
|
|
1349
1312
|
project: name,
|
|
1350
1313
|
services,
|
|
1314
|
+
cloudBindings,
|
|
1351
1315
|
compatibilityDate: cfg.compatibility_date,
|
|
1352
1316
|
secrets: secretValues,
|
|
1353
1317
|
hosts,
|
|
@@ -1383,6 +1347,7 @@ async function deployCloud(args: string[], apiUrl: string, flags: ReturnType<typ
|
|
|
1383
1347
|
assets,
|
|
1384
1348
|
noWait,
|
|
1385
1349
|
services,
|
|
1350
|
+
cloudBindings,
|
|
1386
1351
|
compatibilityDate: cfg.compatibility_date,
|
|
1387
1352
|
secrets: secretValues,
|
|
1388
1353
|
},
|
|
@@ -1663,9 +1628,24 @@ async function domains(args: string[]) {
|
|
|
1663
1628
|
args[0] && !args[0].startsWith("-") && ["list", "add", "verify", "delete"].includes(args[0])
|
|
1664
1629
|
? args.shift()!
|
|
1665
1630
|
: "list";
|
|
1631
|
+
const flags = takeFlags(args, ["--account"] as const);
|
|
1666
1632
|
const host = sub === "list" ? undefined : args.shift();
|
|
1667
1633
|
if (sub !== "list" && !host)
|
|
1668
1634
|
usageError(`domains ${sub}: missing <hostname>`, `domains ${sub} <hostname> [project-dir]`);
|
|
1635
|
+
const { apiUrl: targetUrl } = await resolveApiUrl();
|
|
1636
|
+
if ((await deployKind(targetUrl, false)) === "cloud") {
|
|
1637
|
+
await guard(async () => {
|
|
1638
|
+
if (host !== undefined) throw new CloudError(cloudDomainsUnsupported(sub, host));
|
|
1639
|
+
const project = await readProjectConfig(args[0]);
|
|
1640
|
+
const credentials = await cloudCredentials(targetUrl, flags["--account"] ?? project.account);
|
|
1641
|
+
for (const line of describeCloudRoutes(
|
|
1642
|
+
project.config.name,
|
|
1643
|
+
await listCloudRoutes(credentials, project.config.name),
|
|
1644
|
+
))
|
|
1645
|
+
console.log(line);
|
|
1646
|
+
});
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1669
1649
|
const [project, { apiUrl, token }] = await Promise.all([readProjectConfig(args[0]), apiCredentials()]);
|
|
1670
1650
|
const base = `${apiUrl}/api/projects/${project.config.name}/domains`;
|
|
1671
1651
|
const auth = { "x-api-key": token };
|
|
@@ -2527,13 +2507,13 @@ if (!(command === "tail" && args.some((arg) => arg === "--file" || arg.startsWit
|
|
|
2527
2507
|
|
|
2528
2508
|
switch (resolveCommand(command)) {
|
|
2529
2509
|
case "init":
|
|
2530
|
-
await init(args
|
|
2510
|
+
await init(args);
|
|
2531
2511
|
break;
|
|
2532
2512
|
case "check":
|
|
2533
2513
|
await check(args[0]);
|
|
2534
2514
|
break;
|
|
2535
2515
|
case "types":
|
|
2536
|
-
await types(args
|
|
2516
|
+
await types(args);
|
|
2537
2517
|
break;
|
|
2538
2518
|
case "dev":
|
|
2539
2519
|
await dev(args);
|
package/src/options.ts
CHANGED
|
@@ -28,10 +28,19 @@ export type Options = {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export const COMMAND_OPTIONS = {
|
|
31
|
-
|
|
31
|
+
// As in create-cloudflare: --template picks a starter, --git and --install (and their --no-) decide the two steps
|
|
32
|
+
// after scaffolding, -y takes every default. --list-templates prints the starters and exits.
|
|
33
|
+
init: {
|
|
34
|
+
value: ["--template"],
|
|
35
|
+
flag: ["--list-templates", "--git", "--no-git", "--install", "--no-install", "--yes", "-y", "--accept-defaults"],
|
|
36
|
+
},
|
|
32
37
|
check: {},
|
|
33
|
-
types
|
|
34
|
-
|
|
38
|
+
// The booleans take `--strict-vars=false` as in Wrangler (types-cmd.ts reads the pair); `--no-...` is the short way.
|
|
39
|
+
types: {
|
|
40
|
+
value: ["--env-interface", "--strict-vars", "--include-runtime", "--include-env"],
|
|
41
|
+
flag: ["--check", "--no-strict-vars", "--no-include-runtime", "--no-include-env"],
|
|
42
|
+
},
|
|
43
|
+
dev: { value: ["--port", "--ip"], flag: ["--no-watch"] },
|
|
35
44
|
run: { value: ["--binary", "--port", "--data-dir", "--require"], flag: ["--check"] },
|
|
36
45
|
build: {
|
|
37
46
|
value: ["--target"],
|
|
@@ -56,7 +65,7 @@ export const COMMAND_OPTIONS = {
|
|
|
56
65
|
flag: ["--recent", "--sprout", "--no-follow"],
|
|
57
66
|
},
|
|
58
67
|
delete: { value: ["--name"], flag: ["--yes", "-y"] },
|
|
59
|
-
domains: {},
|
|
68
|
+
domains: { value: ["--account"] },
|
|
60
69
|
// `secret` is the same command (surface.ts); `list | put | delete | bulk` share these, `--value` is put's.
|
|
61
70
|
secrets: { value: ["--value", "--account"], flag: ["--no-deploy"] },
|
|
62
71
|
kv: {
|
package/src/surface.ts
CHANGED
|
@@ -92,10 +92,10 @@ export const COMMANDS: readonly Command[] = [
|
|
|
92
92
|
group: "Develop",
|
|
93
93
|
emoji: "🌱",
|
|
94
94
|
short: "Scaffold a new project",
|
|
95
|
-
args: "[name]",
|
|
95
|
+
args: "[name] [--template <name>] [--list-templates] [--git | --no-git] [--install | --no-install] [-y, --yes]",
|
|
96
96
|
brief: "[name]",
|
|
97
97
|
summary:
|
|
98
|
-
"Scaffold sproutboat.jsonc, src/index.js and package.json (sproutboat devDependency, dev, deploy and check scripts) in
|
|
98
|
+
"Scaffold a project in ./<name> (the last path segment is the app's name), with the shape of `npm create cloudflare`. Without --template it writes sproutboat.jsonc, src/index.js and package.json (sproutboat devDependency, dev, deploy and check scripts). `--template <name>` writes a starter instead, standard Workers TypeScript with tests, a README and a current worker-configuration.d.ts: realtime-room (a chat room on Durable Objects and hibernating WebSockets), api-d1 (a JSON API on D1 with migrations and validation), site-api (a static site and its API in one Worker) or jobs (a cron trigger, a queue and KV); `--list-templates` prints them with their descriptions. In a terminal it asks for the template, the directory, whether to install dependencies and whether to use git (both default to yes); `-y` takes every default without asking, and anywhere else nothing runs that a flag did not ask for. `--install` runs the project's package manager (npm, pnpm, yarn or bun, by lockfile, else the tool that launched the CLI); `--git` runs `git init` and makes a first commit, and does nothing inside an existing repository. It never overwrites a file.",
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
name: "check",
|
|
@@ -110,21 +110,21 @@ export const COMMANDS: readonly Command[] = [
|
|
|
110
110
|
name: "types",
|
|
111
111
|
group: "Develop",
|
|
112
112
|
emoji: "🧾",
|
|
113
|
-
short: "Write
|
|
114
|
-
args: "[
|
|
115
|
-
brief: "[
|
|
113
|
+
short: "Write worker-configuration.d.ts for your editor",
|
|
114
|
+
args: "[path] [--env-interface <name>] [--strict-vars[=false]] [--include-runtime[=false]] [--include-env[=false]] [--check]",
|
|
115
|
+
brief: "[path]",
|
|
116
116
|
summary:
|
|
117
|
-
|
|
117
|
+
'Generate the project\'s `Env` as TypeScript from sproutboat.jsonc, like `wrangler types`: the file is worker-configuration.d.ts unless <path> names another; it declares `interface Env` (and `Cloudflare.Env`, which `import { env } from "cloudflare:workers"` reads) with a type per binding, literal types for vars (--strict-vars=false makes them `string`), and a Durable Object binding typed with its class when the entry is TypeScript. It references the runtime types shipped with the CLI (--include-runtime=false leaves that out, for a project that brings its own) instead of copying them in, and there is no --include-main-module. Type a handler as `export default { ... } satisfies ExportedHandler<Env>`. --check exits 1 when the file is missing or out of date, by the hash in its first lines. KV, D1, R2, Durable Object and Fetcher types follow the compatibility date, as the runtime does. `dev` and `deploy` refresh the default file when it was written by a plain `sproutboat types` and the config has moved on; they leave one made with other flags alone. A positional that is an existing directory is still read as the project directory. The file earlier releases wrote, sproutboat-env.d.ts, is removed when it is still as generated.',
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
name: "dev",
|
|
121
121
|
group: "Develop",
|
|
122
122
|
emoji: "⚡",
|
|
123
123
|
short: "Run the app locally, rebuilding on save",
|
|
124
|
-
args: "[project-dir] [--port <n>] [--no-watch] [-e, --env <name>] [--env-file <file>]...",
|
|
124
|
+
args: "[project-dir] [--port <n>] [--ip <address>] [--no-watch] [-e, --env <name>] [--env-file <file>]...",
|
|
125
125
|
brief: "[project-dir]",
|
|
126
126
|
summary:
|
|
127
|
-
"Run the project on this machine with local storage, rebuilding on save. Secrets come from .dev.vars or the .env files (never both), as in Wrangler; a declared secret with no value is a warning.",
|
|
127
|
+
"Run the project on this machine with local storage, rebuilding on save. It listens on 127.0.0.1 unless `--ip` names another address (`--ip 0.0.0.0` is reachable from other machines on the network, as in Wrangler). Secrets come from .dev.vars or the .env files (never both), as in Wrangler; a declared secret with no value is a warning.",
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
name: "run",
|
|
@@ -223,9 +223,10 @@ export const COMMANDS: readonly Command[] = [
|
|
|
223
223
|
group: "Configure",
|
|
224
224
|
emoji: "🌐",
|
|
225
225
|
short: "Attach custom domains",
|
|
226
|
-
args: "<list | add <host> | verify <host> | delete <host>> [project-dir] [--api-url <url>]",
|
|
226
|
+
args: "<list | add <host> | verify <host> | delete <host>> [project-dir] [--api-url <url>] [--account <team>]",
|
|
227
227
|
brief: "<list | add | verify | delete>",
|
|
228
|
-
summary:
|
|
228
|
+
summary:
|
|
229
|
+
"List the project's domains, or attach one (TXT-verified). On the Cloud only list works: custom domains are added by an operator, and deploys keep them.",
|
|
229
230
|
},
|
|
230
231
|
{
|
|
231
232
|
name: "secrets",
|
|
@@ -351,7 +352,7 @@ export const ENV_VARS: readonly EnvVar[] = [
|
|
|
351
352
|
{
|
|
352
353
|
name: "SPROUTBOAT_PORFFOR_CHANNEL",
|
|
353
354
|
purpose:
|
|
354
|
-
"Porffor channel for standalone builds: `alpha-
|
|
355
|
+
"Porffor channel for standalone builds: `alpha-23` (default), `alpha-22` or `alpha-19` (the oldest compiler, kept as a fallback for one release).",
|
|
355
356
|
},
|
|
356
357
|
{
|
|
357
358
|
name: "SPROUTBOAT_RELEASE",
|