astroidjs 0.16.0 → 0.18.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/bin/astroid.mjs +221 -82
- package/dist/auth/index.js +2 -2
- package/dist/commerce/roles.js +1 -1
- package/dist/commerce/sync.js +2 -1
- package/dist/config.d.ts +26 -0
- package/dist/config.js +9 -9
- package/dist/project/build-output.d.ts +18 -0
- package/dist/project/build-output.js +64 -0
- package/dist/project/generate.d.ts +19 -0
- package/dist/project/generate.js +38 -4
- package/dist/project/index.d.ts +2 -0
- package/dist/project/index.js +2 -0
- package/dist/project/provision.d.ts +52 -1
- package/dist/project/provision.js +77 -7
- package/dist/project/release.d.ts +6 -0
- package/dist/project/release.js +41 -2
- package/dist/project/scaffold.d.ts +17 -0
- package/dist/project/scaffold.js +73 -0
- package/dist/project/ship.d.ts +42 -0
- package/dist/project/ship.js +115 -0
- package/dist/queues/index.d.ts +2 -2
- package/dist/queues/index.js +2 -2
- package/dist/queues/messages.d.ts +6 -0
- package/dist/queues/messages.js +6 -0
- package/dist/queues/scaffold.js +30 -17
- package/dist/queues/webhook.d.ts +31 -2
- package/dist/queues/webhook.js +31 -0
- package/dist/schema/framework.d.ts +4 -3
- package/dist/schema/framework.js +7 -6
- package/dist/worker/gateway.d.ts +12 -0
- package/dist/worker/gateway.js +32 -0
- package/dist/worker/generate.js +70 -13
- package/dist/worker/index.d.ts +1 -0
- package/dist/worker/index.js +1 -0
- package/dist/worker/routes.d.ts +1 -1
- package/dist/worker/routes.js +6 -1
- package/package.json +3 -3
package/bin/astroid.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// astroid generate [--config <path>] [--cwd <dir>] regenerate schema/worker/middleware from the config
|
|
7
7
|
// astroid doctor [--config <path>] [--cwd <dir>] validate config + bindings + generated-file freshness
|
|
8
8
|
// astroid dev [...astro args] generate, then `astro dev`
|
|
9
|
-
// astroid build [...astro args] generate,
|
|
9
|
+
// astroid build [...astro args] generate, `astro build`, then fix the built wrangler.json
|
|
10
10
|
// astroid deploy [--dry-run] [--yes] [--local] provision + migrate + secrets + deploy
|
|
11
11
|
// astroid ship production | preview migrate, then deploy or preview (Workers Builds)
|
|
12
12
|
//
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// CLI ships in, no dependency on node_modules layout (mirrors the louise bin).
|
|
17
17
|
|
|
18
18
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
19
|
+
import { randomBytes } from "node:crypto";
|
|
19
20
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
21
|
import { createRequire } from "node:module";
|
|
21
22
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
@@ -156,6 +157,8 @@ async function cmdDoctor(cwd, flags) {
|
|
|
156
157
|
astroidUsesQueues,
|
|
157
158
|
astroidCrons,
|
|
158
159
|
checkWranglerPreviews,
|
|
160
|
+
astroidRunsMigrations,
|
|
161
|
+
migrationsOwnershipError,
|
|
159
162
|
generateAstroidReleaseWorkflow,
|
|
160
163
|
ASTROID_RELEASE_WORKFLOW_PATH,
|
|
161
164
|
} = await import(GENERATORS_URL);
|
|
@@ -325,12 +328,21 @@ async function cmdDoctor(cwd, flags) {
|
|
|
325
328
|
// 3. migrations directory: the D1 `migrations_dir` wrangler.jsonc declares,
|
|
326
329
|
// or `migrations/`, the generated default, when it names none. A site that
|
|
327
330
|
// keeps its migrations under another name (drizzle/) isn't missing them.
|
|
331
|
+
// An app with `deploy.migrations: false` applies none, because another
|
|
332
|
+
// app owns the database's schema, so it needs no directory. It must not
|
|
333
|
+
// name one either: that contradiction means someone expects it to migrate.
|
|
328
334
|
const wranglerText = existsSync(wranglerPath) ? readFileSync(wranglerPath, "utf8") : "";
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
else
|
|
335
|
+
if (!astroidRunsMigrations(config)) {
|
|
336
|
+
const ownership = migrationsOwnershipError(config, wranglerText);
|
|
337
|
+
if (ownership) err(ownership);
|
|
338
|
+
else ok("deploy.migrations is false: another app migrates this database");
|
|
339
|
+
} else {
|
|
340
|
+
const migrationsDir =
|
|
341
|
+
wranglerText.match(/"migrations_dir"\s*:\s*"([^"]+)"/)?.[1]?.replace(/\/+$/, "") ??
|
|
342
|
+
"migrations";
|
|
343
|
+
if (existsSync(join(cwd, migrationsDir))) ok(`${migrationsDir}/ directory present`);
|
|
344
|
+
else warn(`no ${migrationsDir}/ directory — create your D1 schema migrations there.`);
|
|
345
|
+
}
|
|
334
346
|
|
|
335
347
|
// 4. Local secret provisioning—which modules will run dormant under
|
|
336
348
|
// `astroid dev`, and what to set to wake them.
|
|
@@ -397,7 +409,33 @@ async function cmdAstro(cwd, subcommand, flags, rest) {
|
|
|
397
409
|
);
|
|
398
410
|
}
|
|
399
411
|
const child = spawn(process.execPath, [astroBin, subcommand, ...rest], { stdio: "inherit", cwd });
|
|
400
|
-
child.on("exit", (code) =>
|
|
412
|
+
child.on("exit", async (code) => {
|
|
413
|
+
// A failed build keeps its exit code and its output as it left them.
|
|
414
|
+
if (subcommand === "build" && code === 0) await fixBuiltWranglerConfig(cwd);
|
|
415
|
+
process.exit(code ?? 0);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Delete `legacy_env` from the built Wrangler config, which current Wrangler
|
|
420
|
+
* rejects (see src/project/build-output.ts). Warns rather than failing: the
|
|
421
|
+
* build itself succeeded, and an older Wrangler deploys the file as is. */
|
|
422
|
+
async function fixBuiltWranglerConfig(cwd) {
|
|
423
|
+
const { WRANGLER_DEPLOY_REDIRECT, builtWranglerConfigPath, stripLegacyEnv } = await import(
|
|
424
|
+
GENERATORS_URL
|
|
425
|
+
);
|
|
426
|
+
const redirectPath = join(cwd, WRANGLER_DEPLOY_REDIRECT);
|
|
427
|
+
const redirect = existsSync(redirectPath) ? readFileSync(redirectPath, "utf8") : null;
|
|
428
|
+
const configPath = resolve(cwd, builtWranglerConfigPath(redirect));
|
|
429
|
+
try {
|
|
430
|
+
const next = stripLegacyEnv(readFileSync(configPath, "utf8"));
|
|
431
|
+
if (next === null) return;
|
|
432
|
+
writeFileSync(configPath, next);
|
|
433
|
+
out(`astroid: removed legacy_env from ${rel(cwd, configPath)}`);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
process.stderr.write(
|
|
436
|
+
`astroid: warning: couldn't check ${rel(cwd, configPath)} for legacy_env: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
401
439
|
}
|
|
402
440
|
|
|
403
441
|
/** Resolve a project-local CLI bin (astro, wrangler) to an absolute path via the
|
|
@@ -500,7 +538,9 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
500
538
|
const assumeYes = rest.includes("--yes") || rest.includes("-y");
|
|
501
539
|
const remoteArgs = rest.includes("--local") ? [] : ["--remote"];
|
|
502
540
|
|
|
503
|
-
await loadConfig(cwd, flags.config); // validates the
|
|
541
|
+
const { config } = await loadConfig(cwd, flags.config); // validates the shape, or throws
|
|
542
|
+
const { astroidRunsMigrations, ASTROID_SKIP_MIGRATIONS_NOTE } = await import(GENERATORS_URL);
|
|
543
|
+
const migrate = astroidRunsMigrations(config);
|
|
504
544
|
const wranglerPath = join(cwd, "wrangler.jsonc");
|
|
505
545
|
if (!existsSync(wranglerPath)) fail("wrangler.jsonc not found — run inside an Astroid project.");
|
|
506
546
|
|
|
@@ -520,7 +560,11 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
520
560
|
out(" Provision:");
|
|
521
561
|
if (plan.length === 0) out(" (all bindings already have ids)");
|
|
522
562
|
for (const s of plan) out(` wrangler ${s.args.join(" ")}`);
|
|
523
|
-
out(
|
|
563
|
+
out(
|
|
564
|
+
migrate
|
|
565
|
+
? `\n Migrate: wrangler d1 migrations apply DB ${remoteArgs.join(" ")}`.trimEnd()
|
|
566
|
+
: `\n Migrate: ${ASTROID_SKIP_MIGRATIONS_NOTE}`,
|
|
567
|
+
);
|
|
524
568
|
out(" Secrets: wrangler secret put SESSION_SECRET (prompted)");
|
|
525
569
|
out(" Deploy: wrangler deploy\n");
|
|
526
570
|
if (!facts.hasAccount) {
|
|
@@ -601,10 +645,14 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
601
645
|
}
|
|
602
646
|
}
|
|
603
647
|
|
|
604
|
-
// 2) Migrations.
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
648
|
+
// 2) Migrations, unless another app owns this database's schema.
|
|
649
|
+
if (!migrate) {
|
|
650
|
+
out(`\n${ASTROID_SKIP_MIGRATIONS_NOTE}`);
|
|
651
|
+
} else {
|
|
652
|
+
out(`\n▸ wrangler d1 migrations apply DB ${remoteArgs.join(" ")}`.trimEnd());
|
|
653
|
+
if (runInherit(["d1", "migrations", "apply", "DB", ...remoteArgs]).status !== 0)
|
|
654
|
+
fail("Migrations failed.");
|
|
655
|
+
}
|
|
608
656
|
|
|
609
657
|
// 3) Secrets (interactive; wrangler prompts for the value).
|
|
610
658
|
out("\n▸ wrangler secret put SESSION_SECRET");
|
|
@@ -656,7 +704,7 @@ Usage:
|
|
|
656
704
|
astroid generate [--config <path>] [--cwd <dir>] regenerate src/schema.ts, src/worker.ts, src/middleware.ts
|
|
657
705
|
astroid doctor [--config <path>] [--cwd <dir>] validate config, bindings, and generated-file freshness
|
|
658
706
|
astroid dev [...astro args] regenerate, then run \`astro dev\`
|
|
659
|
-
astroid build [...astro args] regenerate,
|
|
707
|
+
astroid build [...astro args] regenerate, run \`astro build\`, then drop legacy_env from its Wrangler config
|
|
660
708
|
astroid deploy [--dry-run] [--yes] [--local] provision bindings + migrate + secrets + deploy
|
|
661
709
|
astroid ship production | preview migrate D1, then deploy or preview (Workers Builds runs this)
|
|
662
710
|
astroid provision [--dry-run] [--yes] create the resources wrangler.jsonc names by placeholder, staging included
|
|
@@ -667,17 +715,42 @@ New project: pnpm create astroid@latest
|
|
|
667
715
|
// `astroid provision` creates what `wrangler.jsonc` still names by placeholder,
|
|
668
716
|
// top level and `previews` alike, and writes each new ID back in place of its
|
|
669
717
|
// placeholder. It never deploys, so it's safe to run before a site's first
|
|
670
|
-
// release, and re-running it only creates what's still missing.
|
|
671
|
-
//
|
|
718
|
+
// release, and re-running it only creates what's still missing. It creates the
|
|
719
|
+
// staging secrets that need no person (a random SESSION_SECRET and Turnstile's
|
|
720
|
+
// test secret), skipping any the store already has. Every other secret, and
|
|
721
|
+
// dashboard settings, need a person, so it prints those instead.
|
|
722
|
+
//
|
|
723
|
+
// Every wrangler call runs in the site's directory, so wrangler reads the same
|
|
724
|
+
// `wrangler.jsonc` for each and picks the same account: its `account_id`, which
|
|
725
|
+
// wrangler prefers to CLOUDFLARE_ACCOUNT_ID.
|
|
672
726
|
async function cmdProvision(cwd, rest) {
|
|
673
727
|
const dryRun = rest.includes("--dry-run");
|
|
674
728
|
const assumeYes = rest.includes("--yes") || rest.includes("-y");
|
|
675
729
|
const wranglerPath = join(cwd, "wrangler.jsonc");
|
|
676
730
|
if (!existsSync(wranglerPath)) fail("wrangler.jsonc not found — run inside an Astroid project.");
|
|
677
731
|
|
|
678
|
-
const {
|
|
732
|
+
const {
|
|
733
|
+
provisionPlan,
|
|
734
|
+
applyProvisionedId,
|
|
735
|
+
stagingSecretSteps,
|
|
736
|
+
secretNamesFromList,
|
|
737
|
+
TURNSTILE_TEST_SECRET,
|
|
738
|
+
} = await import(GENERATORS_URL);
|
|
679
739
|
let text = readFileSync(wranglerPath, "utf8");
|
|
680
740
|
const plan = provisionPlan(text);
|
|
741
|
+
const wranglerBin = resolveBin(cwd, "wrangler", "wrangler");
|
|
742
|
+
|
|
743
|
+
// Which staging secrets already exist, per store, so a re-run creates nothing
|
|
744
|
+
// twice. Listing is read-only, so a dry run does it too.
|
|
745
|
+
const existing = new Map();
|
|
746
|
+
if (wranglerBin) {
|
|
747
|
+
for (const storeId of new Set(plan.secrets.filter((s) => s.create).map((s) => s.storeId))) {
|
|
748
|
+
const names = listStoreSecrets(wranglerBin, cwd, storeId, secretNamesFromList);
|
|
749
|
+
if (names) existing.set(storeId, names);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const secretSteps = stagingSecretSteps(plan.secrets, existing);
|
|
753
|
+
const toCreate = secretSteps.filter((s) => s.status === "create");
|
|
681
754
|
|
|
682
755
|
out("astroid provision — plan:\n");
|
|
683
756
|
if (plan.steps.length === 0) out(" (nothing to create: every binding has an ID)");
|
|
@@ -685,15 +758,42 @@ async function cmdProvision(cwd, rest) {
|
|
|
685
758
|
const note = s.kind === "r2" ? " (an existing bucket is fine)" : "";
|
|
686
759
|
out(` wrangler ${s.args.join(" ")}${note}`);
|
|
687
760
|
}
|
|
688
|
-
|
|
761
|
+
const envAccount = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
762
|
+
if (!plan.hasAccount && !envAccount) {
|
|
689
763
|
out(
|
|
690
764
|
"\n ! No account_id in wrangler.jsonc and no CLOUDFLARE_ACCOUNT_ID, so wrangler picks one.",
|
|
691
765
|
);
|
|
766
|
+
} else if (plan.accountId && envAccount && envAccount !== plan.accountId) {
|
|
767
|
+
out(
|
|
768
|
+
`\n ! wrangler.jsonc's account_id (${plan.accountId}) wins over ` +
|
|
769
|
+
`CLOUDFLARE_ACCOUNT_ID (${envAccount}), so provision uses ${plan.accountId}.`,
|
|
770
|
+
);
|
|
692
771
|
}
|
|
693
|
-
|
|
694
|
-
|
|
772
|
+
if (secretSteps.length > 0) {
|
|
773
|
+
out("\nStaging secrets it creates itself:");
|
|
774
|
+
for (const { secret, status } of secretSteps) {
|
|
775
|
+
const what =
|
|
776
|
+
status === "create"
|
|
777
|
+
? `will create, with ${secret.create === "random" ? "a new random value" : "Turnstile's always-passes test secret"}`
|
|
778
|
+
: status === "exists"
|
|
779
|
+
? "exists, so it's left alone"
|
|
780
|
+
: wranglerBin
|
|
781
|
+
? "couldn't list the store, so it's left for you"
|
|
782
|
+
: "can't check without wrangler, so it's left for you";
|
|
783
|
+
out(` [staging] ${secret.secretName} in store ${secret.storeId}: ${what}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// The secrets a person still sets: every one provision doesn't create, and
|
|
787
|
+
// any it meant to but couldn't.
|
|
788
|
+
const printSecrets = (failed = []) => {
|
|
789
|
+
const byHand = [
|
|
790
|
+
...plan.secrets.filter((s) => !s.create),
|
|
791
|
+
...secretSteps.filter((s) => s.status === "unknown").map((s) => s.secret),
|
|
792
|
+
...failed,
|
|
793
|
+
];
|
|
794
|
+
if (byHand.length === 0) return;
|
|
695
795
|
out("\nSecrets Store secrets it binds; create any that don't exist yet:");
|
|
696
|
-
for (const s of
|
|
796
|
+
for (const s of byHand) {
|
|
697
797
|
out(
|
|
698
798
|
` [${s.environment}] wrangler secrets-store secret create ${s.storeId} ` +
|
|
699
799
|
`--name ${s.secretName} --scopes workers --remote`,
|
|
@@ -706,7 +806,7 @@ async function cmdProvision(cwd, rest) {
|
|
|
706
806
|
out("\n(dry run — nothing created)");
|
|
707
807
|
return;
|
|
708
808
|
}
|
|
709
|
-
if (plan.steps.length > 0 && !assumeYes) {
|
|
809
|
+
if ((plan.steps.length > 0 || toCreate.length > 0) && !assumeYes) {
|
|
710
810
|
if (!process.stdin.isTTY)
|
|
711
811
|
fail("Refusing to create resources non-interactively. Re-run with --yes.");
|
|
712
812
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -718,7 +818,6 @@ async function cmdProvision(cwd, rest) {
|
|
|
718
818
|
}
|
|
719
819
|
}
|
|
720
820
|
|
|
721
|
-
const wranglerBin = resolveBin(cwd, "wrangler", "wrangler");
|
|
722
821
|
if (!wranglerBin) fail("Could not find `wrangler` in this project.");
|
|
723
822
|
for (const s of plan.steps) {
|
|
724
823
|
out(`\n▸ wrangler ${s.args.join(" ")}`);
|
|
@@ -745,83 +844,123 @@ async function cmdProvision(cwd, rest) {
|
|
|
745
844
|
writeFileSync(wranglerPath, text);
|
|
746
845
|
out(` ↳ ${s.name} = ${id}`);
|
|
747
846
|
}
|
|
748
|
-
|
|
847
|
+
|
|
848
|
+
// The value goes to wrangler as an argument of a direct spawn, never through
|
|
849
|
+
// a shell, and is never printed: wrangler shows it as REDACTED.
|
|
850
|
+
const failed = [];
|
|
851
|
+
for (const { secret } of toCreate) {
|
|
852
|
+
const args = [
|
|
853
|
+
"secrets-store",
|
|
854
|
+
"secret",
|
|
855
|
+
"create",
|
|
856
|
+
secret.storeId,
|
|
857
|
+
"--name",
|
|
858
|
+
secret.secretName,
|
|
859
|
+
"--scopes",
|
|
860
|
+
"workers",
|
|
861
|
+
"--remote",
|
|
862
|
+
];
|
|
863
|
+
out(`\n▸ wrangler ${args.join(" ")} --value <${secret.create}>`);
|
|
864
|
+
const value =
|
|
865
|
+
secret.create === "random" ? randomBytes(48).toString("base64") : TURNSTILE_TEST_SECRET;
|
|
866
|
+
const res = spawnSync(process.execPath, [wranglerBin, ...args, "--value", value], {
|
|
867
|
+
cwd,
|
|
868
|
+
stdio: "inherit",
|
|
869
|
+
});
|
|
870
|
+
if (res.status !== 0) failed.push(secret);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
printSecrets(failed);
|
|
874
|
+
const unlisted = [
|
|
875
|
+
...new Set(secretSteps.filter((s) => s.status === "unknown").map((s) => s.secret.storeId)),
|
|
876
|
+
];
|
|
877
|
+
if (unlisted.length > 0 || failed.length > 0) {
|
|
878
|
+
if (unlisted.length > 0) {
|
|
879
|
+
out(
|
|
880
|
+
`\n✘ Couldn't list Secrets Store ${unlisted.join(", ")}, so its staging secrets ` +
|
|
881
|
+
"weren't created. Check that the store is in this account, then re-run.",
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (failed.length > 0) {
|
|
885
|
+
out(`\n✘ Couldn't create ${failed.map((s) => s.secretName).join(", ")}. Re-run to retry.`);
|
|
886
|
+
}
|
|
887
|
+
process.exitCode = 1;
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
749
890
|
out("\n✓ Provisioned. Commit wrangler.jsonc; the dashboard steps are in the site's RUNBOOK.");
|
|
750
891
|
}
|
|
751
892
|
|
|
893
|
+
/**
|
|
894
|
+
* The secret names in a Secrets Store, or null when it can't be listed. Wrangler
|
|
895
|
+
* prints a table, a page at a time, and fails on a page with no secrets, which
|
|
896
|
+
* is how an empty store, or the page after the last one, reads.
|
|
897
|
+
*/
|
|
898
|
+
function listStoreSecrets(wranglerBin, cwd, storeId, secretNamesFromList) {
|
|
899
|
+
const PER_PAGE = 100; // The API's maximum.
|
|
900
|
+
const names = new Set();
|
|
901
|
+
for (let page = 1; page <= 50; page++) {
|
|
902
|
+
const res = spawnSync(
|
|
903
|
+
process.execPath,
|
|
904
|
+
[
|
|
905
|
+
wranglerBin,
|
|
906
|
+
...["secrets-store", "secret", "list", storeId, "--remote"],
|
|
907
|
+
...["--per-page", String(PER_PAGE), "--page", String(page)],
|
|
908
|
+
],
|
|
909
|
+
{ cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
910
|
+
);
|
|
911
|
+
if (res.status !== 0) {
|
|
912
|
+
return `${res.stdout}${res.stderr}`.includes("returned no secrets") ? names : null;
|
|
913
|
+
}
|
|
914
|
+
const found = secretNamesFromList(res.stdout);
|
|
915
|
+
const before = names.size;
|
|
916
|
+
for (const name of found) names.add(name);
|
|
917
|
+
if (found.length < PER_PAGE || names.size === before) break;
|
|
918
|
+
}
|
|
919
|
+
return names;
|
|
920
|
+
}
|
|
921
|
+
|
|
752
922
|
// `astroid ship` is what Workers Builds runs, so the deploy logic lives in the
|
|
753
923
|
// repository instead of a dashboard field. An account move once rewrote a
|
|
754
924
|
// site's dashboard deploy command to a bare `wrangler deploy`, and migrations
|
|
755
|
-
// silently stopped. Migrations run first, so new code never meets an old schema
|
|
925
|
+
// silently stopped. Migrations run first, so new code never meets an old schema,
|
|
926
|
+
// unless `deploy.migrations` is false because another app owns the database.
|
|
756
927
|
//
|
|
757
928
|
// astroid ship production the deploy/production build: migrate D1, then deploy
|
|
758
929
|
// astroid ship preview every other branch: migrate the staging D1, then
|
|
759
930
|
// `wrangler preview`, named for the branch
|
|
760
|
-
async function cmdShip(cwd, target) {
|
|
931
|
+
async function cmdShip(cwd, target, flags) {
|
|
761
932
|
if (target !== "production" && target !== "preview") {
|
|
762
933
|
fail("Usage: astroid ship production | preview");
|
|
763
934
|
}
|
|
764
935
|
const wranglerBin = resolveBin(cwd, "wrangler", "wrangler");
|
|
765
936
|
if (!wranglerBin) fail("Could not find `wrangler` in this project.");
|
|
766
|
-
const run = (args) => {
|
|
767
|
-
out(`\n▸ wrangler ${args.join(" ")}`);
|
|
768
|
-
const res = spawnSync(process.execPath, [wranglerBin, ...args], { cwd, stdio: "inherit" });
|
|
769
|
-
if (res.status !== 0) process.exit(res.status ?? 1);
|
|
770
|
-
};
|
|
771
937
|
const wranglerPath = join(cwd, "wrangler.jsonc");
|
|
772
938
|
if (!existsSync(wranglerPath)) fail("wrangler.jsonc not found — run inside an Astroid project.");
|
|
773
939
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
940
|
+
// The plan is pure and tested in src/project/ship.ts; this only runs it.
|
|
941
|
+
const { astroidShipPlan } = await import(GENERATORS_URL);
|
|
942
|
+
const { config } = await loadConfig(cwd, flags.config);
|
|
943
|
+
const steps = astroidShipPlan(target, config, {
|
|
944
|
+
wrangler: readFileSync(wranglerPath, "utf8"),
|
|
945
|
+
root: cwd,
|
|
946
|
+
branch: process.env.WORKERS_CI_BRANCH,
|
|
947
|
+
});
|
|
948
|
+
for (const step of steps) {
|
|
949
|
+
if (step.note) {
|
|
950
|
+
out(`\n${step.note}`);
|
|
951
|
+
} else if (step.write) {
|
|
952
|
+
const abs = join(cwd, step.write.path);
|
|
953
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
954
|
+
writeFileSync(abs, step.write.contents);
|
|
955
|
+
} else {
|
|
956
|
+
out(`\n▸ wrangler ${step.run.join(" ")}`);
|
|
957
|
+
const res = spawnSync(process.execPath, [wranglerBin, ...step.run], {
|
|
958
|
+
cwd,
|
|
959
|
+
stdio: "inherit",
|
|
960
|
+
});
|
|
961
|
+
if (res.status !== 0) process.exit(res.status ?? 1);
|
|
962
|
+
}
|
|
778
963
|
}
|
|
779
|
-
|
|
780
|
-
// The staging database is declared only inside `previews`, and wrangler's
|
|
781
|
-
// migrations command reads top-level `d1_databases`. So write a throwaway
|
|
782
|
-
// config naming it, derived from wrangler.jsonc on every run, rather than a
|
|
783
|
-
// second committed file that could drift from the binding.
|
|
784
|
-
const { parseJsonc } = await import(GENERATORS_URL);
|
|
785
|
-
const config = parseJsonc(readFileSync(wranglerPath, "utf8"));
|
|
786
|
-
const prodDb = (config.d1_databases ?? []).find((d) => d.binding === "DB");
|
|
787
|
-
const stagingDb = (config.previews?.d1_databases ?? []).find((d) => d.binding === "DB");
|
|
788
|
-
if (stagingDb && prodDb) {
|
|
789
|
-
const migrationsDir = resolve(cwd, prodDb.migrations_dir ?? "migrations");
|
|
790
|
-
const tmp = join(cwd, ".wrangler", "astroid-preview-migrations.jsonc");
|
|
791
|
-
mkdirSync(dirname(tmp), { recursive: true });
|
|
792
|
-
writeFileSync(
|
|
793
|
-
tmp,
|
|
794
|
-
JSON.stringify(
|
|
795
|
-
{
|
|
796
|
-
d1_databases: [
|
|
797
|
-
{
|
|
798
|
-
binding: "PREVIEW_DB",
|
|
799
|
-
database_name: stagingDb.database_name,
|
|
800
|
-
database_id: stagingDb.database_id,
|
|
801
|
-
migrations_dir: migrationsDir,
|
|
802
|
-
},
|
|
803
|
-
],
|
|
804
|
-
},
|
|
805
|
-
null,
|
|
806
|
-
2,
|
|
807
|
-
),
|
|
808
|
-
);
|
|
809
|
-
run(["d1", "migrations", "apply", "PREVIEW_DB", "--remote", "--config", tmp]);
|
|
810
|
-
} else {
|
|
811
|
-
out("\n(no staging D1 in `previews`, so no staging migrations to apply)");
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// Workers Builds names the branch in WORKERS_CI_BRANCH; a Preview name is a
|
|
815
|
-
// DNS label, so a branch like feature/12-login becomes feature-12-login.
|
|
816
|
-
const branch = process.env.WORKERS_CI_BRANCH;
|
|
817
|
-
const name = branch
|
|
818
|
-
? branch
|
|
819
|
-
.toLowerCase()
|
|
820
|
-
.replace(/[^a-z0-9-]+/g, "-")
|
|
821
|
-
.replace(/^-+|-+$/g, "")
|
|
822
|
-
.slice(0, 63)
|
|
823
|
-
: undefined;
|
|
824
|
-
run(["preview", ...(name ? ["--name", name] : [])]);
|
|
825
964
|
}
|
|
826
965
|
|
|
827
966
|
async function main() {
|
|
@@ -846,7 +985,7 @@ async function main() {
|
|
|
846
985
|
await cmdDeploy(cwd, flags, rest);
|
|
847
986
|
break;
|
|
848
987
|
case "ship":
|
|
849
|
-
await cmdShip(cwd, rest[0]);
|
|
988
|
+
await cmdShip(cwd, rest[0], flags);
|
|
850
989
|
break;
|
|
851
990
|
case "provision":
|
|
852
991
|
await cmdProvision(cwd, rest);
|
package/dist/auth/index.js
CHANGED
|
@@ -50,10 +50,10 @@ export function assertAuthIsolation(config) {
|
|
|
50
50
|
return;
|
|
51
51
|
if (!portal.cookiePrefix || portal.cookiePrefix === ASTROID_EDITOR_COOKIE_PREFIX) {
|
|
52
52
|
throw new AstroidConfigError(`portal.cookiePrefix must be set and distinct from the editor's (${JSON.stringify(ASTROID_EDITOR_COOKIE_PREFIX)}). Two instances sharing a cookie prefix silently sign you out of one when ` +
|
|
53
|
-
'you sign into the other. Pick a project-specific prefix (
|
|
53
|
+
'you sign into the other. Pick a project-specific prefix (for example, "acme_shop").');
|
|
54
54
|
}
|
|
55
55
|
if (portal.tablePrefix === ASTROID_EDITOR_TABLE_PREFIX) {
|
|
56
|
-
throw new AstroidConfigError(`portal.tablePrefix must differ from the editor's (${JSON.stringify(ASTROID_EDITOR_TABLE_PREFIX)})
|
|
56
|
+
throw new AstroidConfigError(`portal.tablePrefix must differ from the editor's (${JSON.stringify(ASTROID_EDITOR_TABLE_PREFIX)}): a shared table prefix merges the two instances into one user table. ` +
|
|
57
57
|
"Leave it unset (the unprefixed `user`/`session` tables) or use a distinct prefix.");
|
|
58
58
|
}
|
|
59
59
|
}
|
package/dist/commerce/roles.js
CHANGED
|
@@ -109,7 +109,7 @@ export function assertCommerceRoles(commerce) {
|
|
|
109
109
|
invoicing: "invoicing",
|
|
110
110
|
pos: "locations/inventory",
|
|
111
111
|
}[role];
|
|
112
|
-
throw new AstroidConfigError(`commerce: ${provider} can't serve the "${role}" role
|
|
112
|
+
throw new AstroidConfigError(`commerce: ${provider} can't serve the "${role}" role, because its louise-toolkit client has no ${missing} API. Providers that can: ${able.join(", ")}.`);
|
|
113
113
|
}
|
|
114
114
|
};
|
|
115
115
|
// The shorthand assigns itself to a role it can serve, so it only has to be a
|
package/dist/commerce/sync.js
CHANGED
|
@@ -145,7 +145,8 @@ export async function astroidCatalogSync(items, options) {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
if (items.length > 0 && result.failed === items.length) {
|
|
148
|
-
|
|
148
|
+
const scope = items.length === 1 ? "the only item" : `all ${items.length} items`;
|
|
149
|
+
throw new AstroidUsageError(`Catalog sync failed for ${scope}, so nothing was written. ` +
|
|
149
150
|
`First error: ${result.errors[0]?.message ?? "unknown"}. ` +
|
|
150
151
|
"This is usually an unapplied migration (the catalog table doesn't exist yet) " +
|
|
151
152
|
"or an unavailable D1 binding.");
|
package/dist/config.d.ts
CHANGED
|
@@ -207,6 +207,12 @@ export interface QueuesConfig {
|
|
|
207
207
|
cron?: string | false;
|
|
208
208
|
/** Deliveries before Cloudflare routes a message to the DLQ. Default 5. */
|
|
209
209
|
maxRetries?: number;
|
|
210
|
+
/**
|
|
211
|
+
* Seconds Cloudflare waits before redelivering a failed message. Default
|
|
212
|
+
* 30. The queue owns retries, so keep a handler's own client retries off
|
|
213
|
+
* rather than stacking them on top of these.
|
|
214
|
+
*/
|
|
215
|
+
retryDelay?: number;
|
|
210
216
|
/** Messages per consumer invocation. Default 10. */
|
|
211
217
|
maxBatchSize?: number;
|
|
212
218
|
/** Seconds the consumer waits to fill a batch. Default 30. */
|
|
@@ -451,11 +457,29 @@ export interface PagesConfig {
|
|
|
451
457
|
*/
|
|
452
458
|
hooks?: boolean;
|
|
453
459
|
}
|
|
460
|
+
export interface StatusConfig {
|
|
461
|
+
/**
|
|
462
|
+
* Add the site's own checks to the public status route, from the
|
|
463
|
+
* scaffold-once `src/status-checks.ts`: for example, the catalog snapshot's
|
|
464
|
+
* age, or the last health scan's. The generated worker spreads them after
|
|
465
|
+
* Astroid's own `d1` and `content` checks.
|
|
466
|
+
*/
|
|
467
|
+
checks?: boolean;
|
|
468
|
+
}
|
|
454
469
|
export interface DeployConfig {
|
|
455
470
|
platform: "cloudflare";
|
|
456
471
|
/** Media base for R2 + `cf-image` resizing—matches Louise's media route
|
|
457
472
|
* (`media.<brand>/cdn-cgi/image`). Default `"/media"`. */
|
|
458
473
|
mediaBase?: string;
|
|
474
|
+
/**
|
|
475
|
+
* Whether `astroid ship` applies D1 migrations before it deploys. Default
|
|
476
|
+
* `true`. Set `false` for an app whose database another app migrates, such
|
|
477
|
+
* as a second Worker in the same repository that binds the first one's D1.
|
|
478
|
+
* One app owns a database's schema. When both migrate, one release tag runs
|
|
479
|
+
* two `wrangler d1 migrations apply` at once against one ledger, and a
|
|
480
|
+
* non-idempotent statement fails the second deploy.
|
|
481
|
+
*/
|
|
482
|
+
migrations?: boolean;
|
|
459
483
|
}
|
|
460
484
|
export interface AstroidConfig {
|
|
461
485
|
/**
|
|
@@ -531,6 +555,8 @@ export interface AstroidConfig {
|
|
|
531
555
|
media?: MediaConfig;
|
|
532
556
|
/** The editable `pages` collection's site-owned write hooks. */
|
|
533
557
|
pages?: PagesConfig;
|
|
558
|
+
/** The public status route's site-owned checks. */
|
|
559
|
+
status?: StatusConfig;
|
|
534
560
|
/**
|
|
535
561
|
* Force the contact form + `inquiries` table on or off. Omit to detect from
|
|
536
562
|
* the config (a `contact` section, or a wholesale-inquiry module). Set `true`
|
package/dist/config.js
CHANGED
|
@@ -102,7 +102,7 @@ function assertCrons(config) {
|
|
|
102
102
|
}
|
|
103
103
|
const owner = seen.get(expression);
|
|
104
104
|
if (owner) {
|
|
105
|
-
throw new AstroidConfigError(`Duplicate cron \`${expression}
|
|
105
|
+
throw new AstroidConfigError(`Duplicate cron \`${expression}\`: it already belongs to ${owner}. One \`scheduled\` ` +
|
|
106
106
|
"handler dispatches on the expression, so the first branch wins and this one would " +
|
|
107
107
|
"never run. Use a different minute.");
|
|
108
108
|
}
|
|
@@ -123,10 +123,10 @@ function assertTenancy(config) {
|
|
|
123
123
|
return;
|
|
124
124
|
const pattern = tenancy.hostPattern?.trim();
|
|
125
125
|
if (!pattern) {
|
|
126
|
-
throw new AstroidConfigError('`tenancy.hostPattern` is required
|
|
126
|
+
throw new AstroidConfigError('`tenancy.hostPattern` is required: a wildcard such as `"*.example.com"`');
|
|
127
127
|
}
|
|
128
128
|
if (!pattern.startsWith("*.")) {
|
|
129
|
-
throw new AstroidConfigError(`\`tenancy.hostPattern\` must be a wildcard starting with "*."
|
|
129
|
+
throw new AstroidConfigError(`\`tenancy.hostPattern\` must be a wildcard starting with "*.", but it's "${pattern}". ` +
|
|
130
130
|
"A fixed host is a custom domain: put it in `hosts` instead.");
|
|
131
131
|
}
|
|
132
132
|
const apex = pattern.slice(2);
|
|
@@ -145,17 +145,17 @@ function assertTenancy(config) {
|
|
|
145
145
|
// that renders the wrong page, with nothing pointing back at the config.
|
|
146
146
|
for (const [label, prefix] of Object.entries(tenancy.apps ?? {})) {
|
|
147
147
|
if (!label || label.includes(".")) {
|
|
148
|
-
throw new AstroidConfigError(`\`tenancy.apps\` label "${label}" must be a single subdomain label
|
|
149
|
-
"Cloudflare's wildcard matches one level.");
|
|
148
|
+
throw new AstroidConfigError(`\`tenancy.apps\` label "${label}" must be a single subdomain label, ` +
|
|
149
|
+
"because Cloudflare's wildcard matches one level.");
|
|
150
150
|
}
|
|
151
151
|
if ((tenancy.reserved ?? []).includes(label)) {
|
|
152
152
|
throw new AstroidConfigError(`"${label}" is in both \`tenancy.apps\` and \`tenancy.reserved\`. ` +
|
|
153
|
-
"An app label is implicitly reserved
|
|
153
|
+
"An app label is implicitly reserved, so keep it in `apps` only, or the " +
|
|
154
154
|
"two lists drift and `reserved` silently wins.");
|
|
155
155
|
}
|
|
156
156
|
if (!prefix.startsWith("/") || prefix === "/" || prefix.endsWith("/")) {
|
|
157
157
|
throw new AstroidConfigError(`\`tenancy.apps.${label}\` must be an internal path prefix like "/studio" ` +
|
|
158
|
-
`(leading slash, no trailing slash, not "/")
|
|
158
|
+
`(leading slash, no trailing slash, not "/"), but it's "${prefix}". ` +
|
|
159
159
|
'The rewrite is `prefix + pathname`, so "/" or a trailing slash produces "//…".');
|
|
160
160
|
}
|
|
161
161
|
}
|
|
@@ -212,9 +212,9 @@ export function defineAstroid(config) {
|
|
|
212
212
|
assertCrons(config);
|
|
213
213
|
assertTenancy(config);
|
|
214
214
|
if (config.portal?.gated) {
|
|
215
|
-
throw new AstroidConfigError("`portal.gated` is not implemented
|
|
215
|
+
throw new AstroidConfigError("`portal.gated` is not implemented: it is accepted but wires no guard, so the site " +
|
|
216
216
|
"would be fully public while appearing gated. Remove it, and gate the whole site by " +
|
|
217
|
-
'listing the prefixes you mean in `portal.routes` (
|
|
217
|
+
'listing the prefixes you mean in `portal.routes` (for example, `[{ prefix: "/" }]` with your ' +
|
|
218
218
|
"login and auth paths ahead of it).");
|
|
219
219
|
}
|
|
220
220
|
return config;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Where `@astrojs/cloudflare` writes the built config when there's no redirect. */
|
|
2
|
+
export declare const ASTROID_BUILT_WRANGLER_CONFIG = "dist/server/wrangler.json";
|
|
3
|
+
/** The redirect file Wrangler reads to find the built config. */
|
|
4
|
+
export declare const WRANGLER_DEPLOY_REDIRECT = ".wrangler/deploy/config.json";
|
|
5
|
+
/**
|
|
6
|
+
* The built config's path, relative to the project root, found the way
|
|
7
|
+
* Wrangler finds it: the `configPath` in `.wrangler/deploy/config.json`, which
|
|
8
|
+
* is relative to that file's own folder, else {@link ASTROID_BUILT_WRANGLER_CONFIG}.
|
|
9
|
+
* Pass the redirect's text, or `null` when there isn't one.
|
|
10
|
+
*/
|
|
11
|
+
export declare function builtWranglerConfigPath(redirect: string | null): string;
|
|
12
|
+
/**
|
|
13
|
+
* Remove `legacy_env` from a built Wrangler config. Returns the new text, or
|
|
14
|
+
* `null` when the field isn't there, so the caller leaves the file untouched.
|
|
15
|
+
* Keeps the file's indentation and every other key in its order. Throws when
|
|
16
|
+
* the text isn't a JSON object.
|
|
17
|
+
*/
|
|
18
|
+
export declare function stripLegacyEnv(text: string): string | null;
|