astroidjs 0.17.0 → 0.19.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.
Files changed (44) hide show
  1. package/README.md +6 -7
  2. package/bin/astroid.mjs +107 -75
  3. package/dist/commerce/adapters.js +3 -3
  4. package/dist/commerce/checkout.js +1 -1
  5. package/dist/commerce/mirror.js +5 -4
  6. package/dist/commerce/roles.js +3 -3
  7. package/dist/config.d.ts +57 -14
  8. package/dist/config.js +27 -7
  9. package/dist/portal/guard.js +1 -1
  10. package/dist/project/build-output.d.ts +18 -0
  11. package/dist/project/build-output.js +64 -0
  12. package/dist/project/generate.d.ts +19 -0
  13. package/dist/project/generate.js +38 -4
  14. package/dist/project/index.d.ts +3 -0
  15. package/dist/project/index.js +3 -0
  16. package/dist/project/migrations.d.ts +26 -0
  17. package/dist/project/migrations.js +78 -0
  18. package/dist/project/scaffold.d.ts +23 -0
  19. package/dist/project/scaffold.js +86 -2
  20. package/dist/project/ship.d.ts +42 -0
  21. package/dist/project/ship.js +115 -0
  22. package/dist/queues/index.d.ts +2 -2
  23. package/dist/queues/index.js +2 -2
  24. package/dist/queues/messages.d.ts +6 -0
  25. package/dist/queues/messages.js +6 -0
  26. package/dist/queues/scaffold.js +30 -17
  27. package/dist/queues/webhook.d.ts +31 -2
  28. package/dist/queues/webhook.js +31 -0
  29. package/dist/schema/collections.d.ts +6 -0
  30. package/dist/schema/collections.js +14 -2
  31. package/dist/schema/framework.d.ts +4 -3
  32. package/dist/schema/framework.js +8 -7
  33. package/dist/worker/gateway.d.ts +12 -0
  34. package/dist/worker/gateway.js +32 -0
  35. package/dist/worker/generate.js +60 -6
  36. package/dist/worker/index.d.ts +1 -0
  37. package/dist/worker/index.js +1 -0
  38. package/dist/worker/routes.d.ts +1 -1
  39. package/dist/worker/routes.js +6 -1
  40. package/dist/workflow/advance.js +1 -1
  41. package/dist/workflow/config.js +1 -1
  42. package/package.json +3 -3
  43. package/src/components/StageBar.astro +1 -1
  44. package/src/components/media-meta.ts +1 -1
package/README.md CHANGED
@@ -35,17 +35,16 @@ project:** every site Astroid targets serves a single brand from a single deploy
35
35
  so the config describes one brand, not an array. What actually multiplexes is
36
36
  _editors_ (Louise's org plugin) and _audiences_ (a gated portal beside the public
37
37
  site)—both options on the one brand. The vocabulary is drawn from the real
38
- sites Astroid targets: a storefront (coracle.coffee), a wholesale front
39
- (ghostfire.coffee), an artist portfolio (themidwestartist.com), and a plain
40
- marketing baseline (louise-web).
38
+ sites Astroid was built from: a storefront, a wholesale front, an artist
39
+ portfolio, and a plain marketing baseline.
41
40
 
42
41
  ```ts
43
42
  import { defineAstroid } from "astroidjs";
44
43
 
45
44
  export default defineAstroid({
46
- key: "coracle",
45
+ key: "example",
47
46
  archetype: "storefront",
48
- theme: { name: "Coracle Coffee", colors: { brand: "#1f6f78" } },
47
+ theme: { name: "Example Organization", colors: { brand: "#5b4bff" } },
49
48
  sections: ["hero", "banner", "productGrid", "locationHours", "contact"],
50
49
  commerce: { provider: "square" },
51
50
  deploy: { platform: "cloudflare" },
@@ -56,9 +55,9 @@ A portfolio with a gated client area, for contrast:
56
55
 
57
56
  ```ts
58
57
  export default defineAstroid({
59
- key: "megbowen",
58
+ key: "example-studio",
60
59
  archetype: "portfolio",
61
- theme: { name: "Meg Bowen Studio", colors: { brand: "#2b2b2b" } },
60
+ theme: { name: "Example Studio", colors: { brand: "#2b2b2b" } },
62
61
  sections: ["hero", "gallery", "aboutIntro", "contact"],
63
62
  portal: { enabled: true },
64
63
  // The masters ARE the product here: 40 MB camera files upload once and only
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, then `astro build`
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
  //
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { execFileSync, spawn, spawnSync } from "node:child_process";
19
19
  import { randomBytes } from "node:crypto";
20
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
21
21
  import { createRequire } from "node:module";
22
22
  import { dirname, isAbsolute, join, resolve } from "node:path";
23
23
  import { createInterface } from "node:readline/promises";
@@ -87,6 +87,22 @@ async function loadConfig(cwd, explicit) {
87
87
  return { config, path };
88
88
  }
89
89
 
90
+ /**
91
+ * The scaffold files with each migration placed in the `DB` binding's
92
+ * `migrations_dir` and numbered past the site's own migrations. Without this a
93
+ * site whose migrations live elsewhere got files Wrangler never applied.
94
+ */
95
+ async function scaffoldFilesFor(cwd, files) {
96
+ const { astroidMigrationsDir, resolveAstroidScaffoldPaths } = await import(GENERATORS_URL);
97
+ const wranglerPath = join(cwd, "wrangler.jsonc");
98
+ const migrationsDir = astroidMigrationsDir(
99
+ existsSync(wranglerPath) ? readFileSync(wranglerPath, "utf8") : null,
100
+ );
101
+ const dirPath = join(cwd, migrationsDir);
102
+ const existing = existsSync(dirPath) ? readdirSync(dirPath) : [];
103
+ return resolveAstroidScaffoldPaths(files, { migrationsDir, existing });
104
+ }
105
+
90
106
  // --- commands --------------------------------------------------------------
91
107
  async function cmdGenerate(cwd, flags, { quiet = false } = {}) {
92
108
  const {
@@ -123,7 +139,7 @@ async function cmdGenerate(cwd, flags, { quiet = false } = {}) {
123
139
  // regenerated a project that couldn't resolve its own imports. Completing the
124
140
  // config change is what makes "one typed config" true.
125
141
  const created = [];
126
- for (const file of generateAstroidScaffoldFiles(config)) {
142
+ for (const file of await scaffoldFilesFor(cwd, generateAstroidScaffoldFiles(config))) {
127
143
  const abs = join(cwd, file.path);
128
144
  const exists = existsSync(abs);
129
145
  if (file.apply === "append-once") {
@@ -157,6 +173,8 @@ async function cmdDoctor(cwd, flags) {
157
173
  astroidUsesQueues,
158
174
  astroidCrons,
159
175
  checkWranglerPreviews,
176
+ astroidRunsMigrations,
177
+ migrationsOwnershipError,
160
178
  generateAstroidReleaseWorkflow,
161
179
  ASTROID_RELEASE_WORKFLOW_PATH,
162
180
  } = await import(GENERATORS_URL);
@@ -194,7 +212,7 @@ async function cmdDoctor(cwd, flags) {
194
212
  // that names a module whose seam was never written produces a project that
195
213
  // cannot resolve its own imports. This is precisely the state that used to
196
214
  // report "healthy" with two warnings and exit 0.
197
- for (const file of generateAstroidScaffoldFiles(config)) {
215
+ for (const file of await scaffoldFilesFor(cwd, generateAstroidScaffoldFiles(config))) {
198
216
  if (file.apply === "append-once") continue; // accumulated, not owned—see below
199
217
  if (existsSync(join(cwd, file.path))) ok(`${file.path} present`);
200
218
  else err(`${file.path} is missing (required by your config) — run \`astroid generate\`.`);
@@ -326,12 +344,21 @@ async function cmdDoctor(cwd, flags) {
326
344
  // 3. migrations directory: the D1 `migrations_dir` wrangler.jsonc declares,
327
345
  // or `migrations/`, the generated default, when it names none. A site that
328
346
  // keeps its migrations under another name (drizzle/) isn't missing them.
347
+ // An app with `deploy.migrations: false` applies none, because another
348
+ // app owns the database's schema, so it needs no directory. It must not
349
+ // name one either: that contradiction means someone expects it to migrate.
329
350
  const wranglerText = existsSync(wranglerPath) ? readFileSync(wranglerPath, "utf8") : "";
330
- const migrationsDir =
331
- wranglerText.match(/"migrations_dir"\s*:\s*"([^"]+)"/)?.[1]?.replace(/\/+$/, "") ??
332
- "migrations";
333
- if (existsSync(join(cwd, migrationsDir))) ok(`${migrationsDir}/ directory present`);
334
- else warn(`no ${migrationsDir}/ directory — create your D1 schema migrations there.`);
351
+ if (!astroidRunsMigrations(config)) {
352
+ const ownership = migrationsOwnershipError(config, wranglerText);
353
+ if (ownership) err(ownership);
354
+ else ok("deploy.migrations is false: another app migrates this database");
355
+ } else {
356
+ const migrationsDir =
357
+ wranglerText.match(/"migrations_dir"\s*:\s*"([^"]+)"/)?.[1]?.replace(/\/+$/, "") ??
358
+ "migrations";
359
+ if (existsSync(join(cwd, migrationsDir))) ok(`${migrationsDir}/ directory present`);
360
+ else warn(`no ${migrationsDir}/ directory — create your D1 schema migrations there.`);
361
+ }
335
362
 
336
363
  // 4. Local secret provisioning—which modules will run dormant under
337
364
  // `astroid dev`, and what to set to wake them.
@@ -398,7 +425,33 @@ async function cmdAstro(cwd, subcommand, flags, rest) {
398
425
  );
399
426
  }
400
427
  const child = spawn(process.execPath, [astroBin, subcommand, ...rest], { stdio: "inherit", cwd });
401
- child.on("exit", (code) => process.exit(code ?? 0));
428
+ child.on("exit", async (code) => {
429
+ // A failed build keeps its exit code and its output as it left them.
430
+ if (subcommand === "build" && code === 0) await fixBuiltWranglerConfig(cwd);
431
+ process.exit(code ?? 0);
432
+ });
433
+ }
434
+
435
+ /** Delete `legacy_env` from the built Wrangler config, which current Wrangler
436
+ * rejects (see src/project/build-output.ts). Warns rather than failing: the
437
+ * build itself succeeded, and an older Wrangler deploys the file as is. */
438
+ async function fixBuiltWranglerConfig(cwd) {
439
+ const { WRANGLER_DEPLOY_REDIRECT, builtWranglerConfigPath, stripLegacyEnv } = await import(
440
+ GENERATORS_URL
441
+ );
442
+ const redirectPath = join(cwd, WRANGLER_DEPLOY_REDIRECT);
443
+ const redirect = existsSync(redirectPath) ? readFileSync(redirectPath, "utf8") : null;
444
+ const configPath = resolve(cwd, builtWranglerConfigPath(redirect));
445
+ try {
446
+ const next = stripLegacyEnv(readFileSync(configPath, "utf8"));
447
+ if (next === null) return;
448
+ writeFileSync(configPath, next);
449
+ out(`astroid: removed legacy_env from ${rel(cwd, configPath)}`);
450
+ } catch (err) {
451
+ process.stderr.write(
452
+ `astroid: warning: couldn't check ${rel(cwd, configPath)} for legacy_env: ${err instanceof Error ? err.message : String(err)}\n`,
453
+ );
454
+ }
402
455
  }
403
456
 
404
457
  /** Resolve a project-local CLI bin (astro, wrangler) to an absolute path via the
@@ -501,7 +554,9 @@ async function cmdDeploy(cwd, flags, rest) {
501
554
  const assumeYes = rest.includes("--yes") || rest.includes("-y");
502
555
  const remoteArgs = rest.includes("--local") ? [] : ["--remote"];
503
556
 
504
- await loadConfig(cwd, flags.config); // validates the config (throws on a bad shape)
557
+ const { config } = await loadConfig(cwd, flags.config); // validates the shape, or throws
558
+ const { astroidRunsMigrations, ASTROID_SKIP_MIGRATIONS_NOTE } = await import(GENERATORS_URL);
559
+ const migrate = astroidRunsMigrations(config);
505
560
  const wranglerPath = join(cwd, "wrangler.jsonc");
506
561
  if (!existsSync(wranglerPath)) fail("wrangler.jsonc not found — run inside an Astroid project.");
507
562
 
@@ -521,7 +576,11 @@ async function cmdDeploy(cwd, flags, rest) {
521
576
  out(" Provision:");
522
577
  if (plan.length === 0) out(" (all bindings already have ids)");
523
578
  for (const s of plan) out(` wrangler ${s.args.join(" ")}`);
524
- out(`\n Migrate: wrangler d1 migrations apply DB ${remoteArgs.join(" ")}`.trimEnd());
579
+ out(
580
+ migrate
581
+ ? `\n Migrate: wrangler d1 migrations apply DB ${remoteArgs.join(" ")}`.trimEnd()
582
+ : `\n Migrate: ${ASTROID_SKIP_MIGRATIONS_NOTE}`,
583
+ );
525
584
  out(" Secrets: wrangler secret put SESSION_SECRET (prompted)");
526
585
  out(" Deploy: wrangler deploy\n");
527
586
  if (!facts.hasAccount) {
@@ -602,10 +661,14 @@ async function cmdDeploy(cwd, flags, rest) {
602
661
  }
603
662
  }
604
663
 
605
- // 2) Migrations.
606
- out(`\n▸ wrangler d1 migrations apply DB ${remoteArgs.join(" ")}`.trimEnd());
607
- if (runInherit(["d1", "migrations", "apply", "DB", ...remoteArgs]).status !== 0)
608
- fail("Migrations failed.");
664
+ // 2) Migrations, unless another app owns this database's schema.
665
+ if (!migrate) {
666
+ out(`\n${ASTROID_SKIP_MIGRATIONS_NOTE}`);
667
+ } else {
668
+ out(`\n▸ wrangler d1 migrations apply DB ${remoteArgs.join(" ")}`.trimEnd());
669
+ if (runInherit(["d1", "migrations", "apply", "DB", ...remoteArgs]).status !== 0)
670
+ fail("Migrations failed.");
671
+ }
609
672
 
610
673
  // 3) Secrets (interactive; wrangler prompts for the value).
611
674
  out("\n▸ wrangler secret put SESSION_SECRET");
@@ -657,7 +720,7 @@ Usage:
657
720
  astroid generate [--config <path>] [--cwd <dir>] regenerate src/schema.ts, src/worker.ts, src/middleware.ts
658
721
  astroid doctor [--config <path>] [--cwd <dir>] validate config, bindings, and generated-file freshness
659
722
  astroid dev [...astro args] regenerate, then run \`astro dev\`
660
- astroid build [...astro args] regenerate, then run \`astro build\`
723
+ astroid build [...astro args] regenerate, run \`astro build\`, then drop legacy_env from its Wrangler config
661
724
  astroid deploy [--dry-run] [--yes] [--local] provision bindings + migrate + secrets + deploy
662
725
  astroid ship production | preview migrate D1, then deploy or preview (Workers Builds runs this)
663
726
  astroid provision [--dry-run] [--yes] create the resources wrangler.jsonc names by placeholder, staging included
@@ -875,76 +938,45 @@ function listStoreSecrets(wranglerBin, cwd, storeId, secretNamesFromList) {
875
938
  // `astroid ship` is what Workers Builds runs, so the deploy logic lives in the
876
939
  // repository instead of a dashboard field. An account move once rewrote a
877
940
  // site's dashboard deploy command to a bare `wrangler deploy`, and migrations
878
- // silently stopped. Migrations run first, so new code never meets an old schema.
941
+ // silently stopped. Migrations run first, so new code never meets an old schema,
942
+ // unless `deploy.migrations` is false because another app owns the database.
879
943
  //
880
944
  // astroid ship production the deploy/production build: migrate D1, then deploy
881
945
  // astroid ship preview every other branch: migrate the staging D1, then
882
946
  // `wrangler preview`, named for the branch
883
- async function cmdShip(cwd, target) {
947
+ async function cmdShip(cwd, target, flags) {
884
948
  if (target !== "production" && target !== "preview") {
885
949
  fail("Usage: astroid ship production | preview");
886
950
  }
887
951
  const wranglerBin = resolveBin(cwd, "wrangler", "wrangler");
888
952
  if (!wranglerBin) fail("Could not find `wrangler` in this project.");
889
- const run = (args) => {
890
- out(`\n▸ wrangler ${args.join(" ")}`);
891
- const res = spawnSync(process.execPath, [wranglerBin, ...args], { cwd, stdio: "inherit" });
892
- if (res.status !== 0) process.exit(res.status ?? 1);
893
- };
894
953
  const wranglerPath = join(cwd, "wrangler.jsonc");
895
954
  if (!existsSync(wranglerPath)) fail("wrangler.jsonc not found — run inside an Astroid project.");
896
955
 
897
- if (target === "production") {
898
- run(["d1", "migrations", "apply", "DB", "--remote"]);
899
- run(["deploy"]);
900
- return;
956
+ // The plan is pure and tested in src/project/ship.ts; this only runs it.
957
+ const { astroidShipPlan } = await import(GENERATORS_URL);
958
+ const { config } = await loadConfig(cwd, flags.config);
959
+ const steps = astroidShipPlan(target, config, {
960
+ wrangler: readFileSync(wranglerPath, "utf8"),
961
+ root: cwd,
962
+ branch: process.env.WORKERS_CI_BRANCH,
963
+ });
964
+ for (const step of steps) {
965
+ if (step.note) {
966
+ out(`\n${step.note}`);
967
+ } else if (step.write) {
968
+ const abs = join(cwd, step.write.path);
969
+ mkdirSync(dirname(abs), { recursive: true });
970
+ writeFileSync(abs, step.write.contents);
971
+ } else {
972
+ out(`\n▸ wrangler ${step.run.join(" ")}`);
973
+ const res = spawnSync(process.execPath, [wranglerBin, ...step.run], {
974
+ cwd,
975
+ stdio: "inherit",
976
+ });
977
+ if (res.status !== 0) process.exit(res.status ?? 1);
978
+ }
901
979
  }
902
-
903
- // The staging database is declared only inside `previews`, and wrangler's
904
- // migrations command reads top-level `d1_databases`. So write a throwaway
905
- // config naming it, derived from wrangler.jsonc on every run, rather than a
906
- // second committed file that could drift from the binding.
907
- const { parseJsonc } = await import(GENERATORS_URL);
908
- const config = parseJsonc(readFileSync(wranglerPath, "utf8"));
909
- const prodDb = (config.d1_databases ?? []).find((d) => d.binding === "DB");
910
- const stagingDb = (config.previews?.d1_databases ?? []).find((d) => d.binding === "DB");
911
- if (stagingDb && prodDb) {
912
- const migrationsDir = resolve(cwd, prodDb.migrations_dir ?? "migrations");
913
- const tmp = join(cwd, ".wrangler", "astroid-preview-migrations.jsonc");
914
- mkdirSync(dirname(tmp), { recursive: true });
915
- writeFileSync(
916
- tmp,
917
- JSON.stringify(
918
- {
919
- d1_databases: [
920
- {
921
- binding: "PREVIEW_DB",
922
- database_name: stagingDb.database_name,
923
- database_id: stagingDb.database_id,
924
- migrations_dir: migrationsDir,
925
- },
926
- ],
927
- },
928
- null,
929
- 2,
930
- ),
931
- );
932
- run(["d1", "migrations", "apply", "PREVIEW_DB", "--remote", "--config", tmp]);
933
- } else {
934
- out("\n(no staging D1 in `previews`, so no staging migrations to apply)");
935
- }
936
-
937
- // Workers Builds names the branch in WORKERS_CI_BRANCH; a Preview name is a
938
- // DNS label, so a branch like feature/12-login becomes feature-12-login.
939
- const branch = process.env.WORKERS_CI_BRANCH;
940
- const name = branch
941
- ? branch
942
- .toLowerCase()
943
- .replace(/[^a-z0-9-]+/g, "-")
944
- .replace(/^-+|-+$/g, "")
945
- .slice(0, 63)
946
- : undefined;
947
- run(["preview", ...(name ? ["--name", name] : [])]);
948
980
  }
949
981
 
950
982
  async function main() {
@@ -969,7 +1001,7 @@ async function main() {
969
1001
  await cmdDeploy(cwd, flags, rest);
970
1002
  break;
971
1003
  case "ship":
972
- await cmdShip(cwd, rest[0]);
1004
+ await cmdShip(cwd, rest[0], flags);
973
1005
  break;
974
1006
  case "provision":
975
1007
  await cmdProvision(cwd, rest);
@@ -2,9 +2,9 @@
2
2
  //
3
3
  // Provider → `CatalogItem` normalizers.
4
4
  //
5
- // This is the file the whole module exists for. themidwestartist.com's loader
6
- // says it outright: coracle runs the same helper over Square, "only the
7
- // content/repo reads differ—issue: repo drift." Two sites, one intent, two
5
+ // This is the file the whole module exists for. One client site's loader says it
6
+ // outright: another site runs the same helper over Square, and only the content
7
+ // and repository reads differ, so the copies drift. Two sites, one intent, two
8
8
  // hand-written translations that drifted apart. The translation is mechanical,
9
9
  // so it belongs here once.
10
10
  //
@@ -3,7 +3,7 @@
3
3
  // Server-authoritative checkout.
4
4
  //
5
5
  // A cart arrives from the browser, so every number in it is a claim, not a fact.
6
- // The rule this encodes—taken from coracle.coffee's working checkout—is that
6
+ // The rule this encodes—taken from a client site's working checkout—is that
7
7
  // the client's price is a **staleness check**, never an input to the charge:
8
8
  // look the price up server-side, and if it disagrees with what the customer was
9
9
  // shown, refuse rather than silently charging a different amount. Refusing is
@@ -12,7 +12,7 @@
12
12
  // What the sites did NOT agree on is how much to store, and it turns out to be
13
13
  // one primitive with two settings rather than two designs:
14
14
  //
15
- // mirror: pulled + owned columns both live in D1 (themidwestartist.com).
15
+ // mirror: pulled + owned columns both live in D1.
16
16
  // Reads are one local query. The catalog can be stale between syncs.
17
17
  // overlay: only the owned columns live in D1, keyed by the provider's id.
18
18
  // The catalog is read live from the provider and joined at read
@@ -20,9 +20,10 @@
20
20
  // (cache accordingly).
21
21
  // live: Astroid manages NO catalog table at all: the catalog is read live
22
22
  // from the provider (cached), and any owner-side overlay table is
23
- // the SITE's own (coracle.coffee's `product_display_meta`, declared
24
- // in schema.site.ts and joined in the site's loader). Use when the
25
- // existing overlay shape predates Astroid and must be preserved 1:1.
23
+ // the SITE's own (for example, a `product_display_meta` table
24
+ // declared in schema.site.ts and joined in the site's loader).
25
+ // Use when the existing overlay shape predates Astroid and must
26
+ // be preserved 1:1.
26
27
  //
27
28
  // `overlay` is just `mirror` with an empty pulled set; `live` emits neither table
28
29
  // nor migration. One generator serves all three and a project switches by one word.
@@ -9,9 +9,9 @@
9
9
  // happens to do both. A single `CommerceProvider` abstraction that assumed
10
10
  // catalog + checkout would therefore have a permanent hole wherever Stripe sits.
11
11
  //
12
- // That's not hypothetical. themidwestartist.com runs Stripe for **invoicing**
13
- // (commissions, originals) alongside Fourthwall for the **storefront**
14
- // (merch)—two providers, one site, each doing the half it can do.
12
+ // A site that invoices through one provider (commissions, originals) and runs
13
+ // its storefront through another (merch) needs exactly this: two providers,
14
+ // one site, each doing the half it can do.
15
15
  //
16
16
  // So a project assigns providers to roles, and Astroid validates the assignment
17
17
  // against what each provider's client can actually serve.
package/dist/config.d.ts CHANGED
@@ -8,8 +8,8 @@ import type { PwaConfig } from "./pwa/generate.js";
8
8
  * The starting shape the front-end takes. Not a fork—each archetype is a preset
9
9
  * of defaults (which sections/modules are on, nav shape) that the site then tunes.
10
10
  * `marketing` = the lean brochure floor (louise-web, no commerce); `storefront` =
11
- * DTC shop (coracle); `wholesale` = B2B/private-label (ghostfire); `portfolio` =
12
- * gallery + prints + client portal (megbowen).
11
+ * DTC shop; `wholesale` = B2B/private-label; `portfolio` = gallery + prints +
12
+ * client portal.
13
13
  */
14
14
  export type Archetype = "marketing" | "storefront" | "wholesale" | "portfolio";
15
15
  /**
@@ -56,7 +56,7 @@ export declare const ASTROID_ARCHETYPE_SECTIONS: Record<Archetype, SectionKind[]
56
56
  * deploy and notice the absence.
57
57
  *
58
58
  * They are removed rather than left as TODOs. `orderTracking` in particular has
59
- * a real implementation waiting—`src/workflow/` is the ghostfire order tracker,
59
+ * a real implementation waiting—`src/workflow/` is a client site's order tracker,
60
60
  * generalized—but it is reached through `defineWorkflow`, not this flag, and
61
61
  * pretending otherwise is what made the flag misleading. Re-add each one in the
62
62
  * change that wires it.
@@ -152,8 +152,9 @@ export interface CommerceConfig {
152
152
  storefront?: CommerceProvider;
153
153
  /**
154
154
  * Invoices for work that isn't a catalog item—commissions, originals.
155
- * Independent of `storefront`: themidwestartist.com runs Stripe here and
156
- * Fourthwall as the storefront, because neither can do the other's job.
155
+ * Independent of `storefront`: a site can invoice through one provider (say,
156
+ * Stripe) and run its storefront through another (say, Fourthwall), because
157
+ * neither can do the other's job.
157
158
  */
158
159
  invoicing?: CommerceProvider;
159
160
  /**
@@ -161,7 +162,7 @@ export interface CommerceConfig {
161
162
  * counter or a market stall rather than through the site's own cart.
162
163
  *
163
164
  * Separate from `storefront` because they are genuinely different jobs and a
164
- * site commonly runs both. themidwestartist.com sells print-on-demand merch
165
+ * site commonly runs both. An artist's site might sell print-on-demand merch
165
166
  * through Fourthwall (`storefront`) while originals and self-stocked prints
166
167
  * live in Square (`pos`) across several shops and galleries—one catalog per
167
168
  * rail, neither able to do the other's job.
@@ -207,6 +208,12 @@ export interface QueuesConfig {
207
208
  cron?: string | false;
208
209
  /** Deliveries before Cloudflare routes a message to the DLQ. Default 5. */
209
210
  maxRetries?: number;
211
+ /**
212
+ * Seconds Cloudflare waits before redelivering a failed message. Default
213
+ * 30. The queue owns retries, so keep a handler's own client retries off
214
+ * rather than stacking them on top of these.
215
+ */
216
+ retryDelay?: number;
210
217
  /** Messages per consumer invocation. Default 10. */
211
218
  maxBatchSize?: number;
212
219
  /** Seconds the consumer waits to fill a batch. Default 30. */
@@ -317,9 +324,9 @@ export interface TenancyConfig {
317
324
  * sign-in) instead of JSON, and every data load on that host silently fails
318
325
  * while the same code works on the apex.
319
326
  *
320
- * That is not hypothetical—it is why this default exists (found on
321
- * themidwestartist.com's studio, where the whole admin app loaded and then
322
- * fetched nothing).
327
+ * That is not hypothetical—it is why this default exists (found on a client
328
+ * site's studio host, where the whole admin app loaded and then fetched
329
+ * nothing).
323
330
  *
324
331
  * Set `[]` to rewrite everything, or add prefixes for other host-agnostic
325
332
  * surfaces (`/_actions`, `/webhooks`). Matching is prefix-based on a path
@@ -401,8 +408,8 @@ export interface SettingsConfig {
401
408
  * on top of (or, with `columns: []`, instead of) Astroid's base columns. The
402
409
  * generated `settingsRoute` + Action accept these; the Settings panel writes
403
410
  * them through the `settingsExtension` groups a site supplies to
404
- * `mountSettings`. A site with a rich settings shape (coracle's footer columns,
405
- * hours table, ui strings, shop/order config) lists their top-level keys here.
411
+ * `mountSettings`. A site with a rich settings shape (footer columns, an hours
412
+ * table, UI strings, shop/order config) lists their top-level keys here.
406
413
  */
407
414
  customKeys?: string[];
408
415
  /** Extra media-library image keys beyond the base logo/favicon/OG defaults—*
@@ -450,17 +457,50 @@ export interface PagesConfig {
450
457
  * clamping a title, or filling a new page's defaults.
451
458
  */
452
459
  hooks?: boolean;
460
+ /**
461
+ * Reserved slugs this site serves as pages. `contact` and `login` are
462
+ * reserved because a new scaffold has file routes there, and `work` because
463
+ * a portfolio has its gallery there. A site without that file, whose page at
464
+ * the path comes from its own catch-all route, lists the slug here so the
465
+ * Pages route accepts it. Only those three can be allowed
466
+ * ({@link ASTROID_SCAFFOLD_ROUTE_SLUGS}): the platform serves the other
467
+ * reserved slugs, such as `api` and `sitemap.xml`, before any page.
468
+ */
469
+ allowSlugs?: string[];
470
+ }
471
+ /**
472
+ * The reserved slugs that come from a scaffolded file route rather than the
473
+ * platform, so a site without that file can allow them with `pages.allowSlugs`.
474
+ */
475
+ export declare const ASTROID_SCAFFOLD_ROUTE_SLUGS: readonly string[];
476
+ export interface StatusConfig {
477
+ /**
478
+ * Add the site's own checks to the public status route, from the
479
+ * scaffold-once `src/status-checks.ts`: for example, the catalog snapshot's
480
+ * age, or the last health scan's. The generated worker spreads them after
481
+ * Astroid's own `d1` and `content` checks.
482
+ */
483
+ checks?: boolean;
453
484
  }
454
485
  export interface DeployConfig {
455
486
  platform: "cloudflare";
456
487
  /** Media base for R2 + `cf-image` resizing—matches Louise's media route
457
488
  * (`media.<brand>/cdn-cgi/image`). Default `"/media"`. */
458
489
  mediaBase?: string;
490
+ /**
491
+ * Whether `astroid ship` applies D1 migrations before it deploys. Default
492
+ * `true`. Set `false` for an app whose database another app migrates, such
493
+ * as a second Worker in the same repository that binds the first one's D1.
494
+ * One app owns a database's schema. When both migrate, one release tag runs
495
+ * two `wrangler d1 migrations apply` at once against one ledger, and a
496
+ * non-idempotent statement fails the second deploy.
497
+ */
498
+ migrations?: boolean;
459
499
  }
460
500
  export interface AstroidConfig {
461
501
  /**
462
502
  * Stable project slug—the worker/D1/R2 base name and default subdomain (for example,
463
- * `"coracle"`). Required and non-empty; it drives the generated binding names.
503
+ * `"example"`). Required and non-empty; it drives the generated binding names.
464
504
  */
465
505
  key: string;
466
506
  /** Hostnames this site serves (prod + preview), for custom-domain routes. */
@@ -481,7 +521,7 @@ export interface AstroidConfig {
481
521
  * A site-provided section catalog that REPLACES the built-in one for
482
522
  * SERVER-side validation + sanitization of `pages.sections` (the generated
483
523
  * pages route + versions route). A site with bespoke section designs—its own
484
- * `.astro` components and field defs (coracle's 13 sections)—registers them
524
+ * `.astro` components and field defs (a dozen or more sections)—registers them
485
525
  * here so writes to its custom `_type`s validate instead of 422-ing against the
486
526
  * built-in vocabulary. The on-canvas editor already uses the site's catalog
487
527
  * (its `mountSections` call passes it); this closes the server half so both
@@ -531,11 +571,14 @@ export interface AstroidConfig {
531
571
  media?: MediaConfig;
532
572
  /** The editable `pages` collection's site-owned write hooks. */
533
573
  pages?: PagesConfig;
574
+ /** The public status route's site-owned checks. */
575
+ status?: StatusConfig;
534
576
  /**
535
577
  * Force the contact form + `inquiries` table on or off. Omit to detect from
536
578
  * the config (a `contact` section, or a wholesale-inquiry module). Set `true`
537
579
  * when a bespoke section captures inquiries under a name Astroid can't see
538
- * (coracle's custom `contactForm`); set `false` to suppress it entirely.
580
+ * (for example, a custom `contactForm` section); set `false` to suppress it
581
+ * entirely.
539
582
  */
540
583
  inquiries?: boolean;
541
584
  /** Installable-app settings. Only read when `modules` includes `"pwa"`. */
package/dist/config.js CHANGED
@@ -8,8 +8,8 @@
8
8
  // the Louise wiring (worker routes, middleware, Drizzle schema, theme tokens) a
9
9
  // site would otherwise hand-write per repo.
10
10
  //
11
- // ONE brand per project. Every site Astroid targets (coracle.coffee,
12
- // ghostfire.coffee, themidwestartist.com, louise-web) serves a single brand from a
11
+ // ONE brand per project. Every site Astroid targets (the client sites its
12
+ // patterns came from, and louise-web) serves a single brand from a
13
13
  // single deploy. The axis that genuinely multiplexes is *editors* (Louise's org
14
14
  // plugin, #100) and *audiences*—a gated portal alongside the public site, or a
15
15
  // per-merchant storefront on its own subdomain (`tenancy`)—not brands. So all
@@ -22,9 +22,9 @@
22
22
  // a second project.
23
23
  //
24
24
  // The vocabulary below is not invented: `Archetype`, `SectionKind`, and
25
- // `ModuleKind` are extracted from the real sites Astroid targets—a storefront
26
- // (coracle), a wholesale front (ghostfire), an artist portfolio (megbowen), and a
27
- // plain marketing baseline (louise-web).
25
+ // `ModuleKind` are extracted from the real sites Astroid targets—a storefront,
26
+ // a wholesale front, an artist portfolio, and a plain marketing baseline
27
+ // (louise-web).
28
28
  import { assertAuthIsolation } from "./auth/index.js";
29
29
  import { assertCommerceRoles } from "./commerce/roles.js";
30
30
  import { AstroidConfigError } from "./errors.js";
@@ -55,6 +55,11 @@ export const ASTROID_ARCHETYPE_SECTIONS = {
55
55
  wholesale: ["hero", "featureGrid", "aboutIntro", "contact"],
56
56
  portfolio: ["hero", "gallery", "aboutIntro", "contact"],
57
57
  };
58
+ /**
59
+ * The reserved slugs that come from a scaffolded file route rather than the
60
+ * platform, so a site without that file can allow them with `pages.allowSlugs`.
61
+ */
62
+ export const ASTROID_SCAFFOLD_ROUTE_SLUGS = ["contact", "login", "work"];
58
63
  /**
59
64
  * Define an Astroid project. An identity function in the shape of Astro's
60
65
  * `defineConfig`: it returns the config verbatim with full type-checking +
@@ -64,9 +69,9 @@ export const ASTROID_ARCHETYPE_SECTIONS = {
64
69
  *
65
70
  * ```ts
66
71
  * export default defineAstroid({
67
- * key: "coracle",
72
+ * key: "example",
68
73
  * archetype: "storefront",
69
- * theme: { name: "Coracle Coffee", colors: { brand: "#1f6f78" } },
74
+ * theme: { name: "Example Organization", colors: { brand: "#5b4bff" } },
70
75
  * sections: ["hero", "banner", "productGrid", "locationHours", "contact"],
71
76
  * commerce: { provider: "square" },
72
77
  * deploy: { platform: "cloudflare" },
@@ -109,6 +114,20 @@ function assertCrons(config) {
109
114
  seen.set(expression, "another entry in `crons`");
110
115
  }
111
116
  }
117
+ /**
118
+ * `pages.allowSlugs` may only name a slug reserved for a scaffolded file route.
119
+ * Allowing `api` or `sitemap.xml` would let an editor save a page nobody can
120
+ * reach, which is the silent failure the reserved list exists to prevent.
121
+ */
122
+ function assertAllowSlugs(config) {
123
+ for (const slug of config.pages?.allowSlugs ?? []) {
124
+ if (!ASTROID_SCAFFOLD_ROUTE_SLUGS.includes(slug)) {
125
+ throw new AstroidConfigError(`\`pages.allowSlugs\` can't allow "${slug}". Only the slugs reserved for a scaffolded ` +
126
+ `file route can be allowed (${ASTROID_SCAFFOLD_ROUTE_SLUGS.join(", ")}); the ` +
127
+ "platform serves the others before any page.");
128
+ }
129
+ }
130
+ }
112
131
  /**
113
132
  * The tenancy misconfigurations that fail late, or not at all.
114
133
  *
@@ -211,6 +230,7 @@ export function defineAstroid(config) {
211
230
  // an answer. Fail loudly, at config load, naming the workaround.
212
231
  assertCrons(config);
213
232
  assertTenancy(config);
233
+ assertAllowSlugs(config);
214
234
  if (config.portal?.gated) {
215
235
  throw new AstroidConfigError("`portal.gated` is not implemented: it is accepted but wires no guard, so the site " +
216
236
  "would be fully public while appearing gated. Remove it, and gate the whole site by " +
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Role-gated routing for the portal.
4
4
  //
5
- // coracle and ghostfire independently built the same thing: a declarative table
5
+ // Two client sites independently built the same thing: a declarative table
6
6
  // of `prefix → roles`, walked once per request. Declarative rather than a guard
7
7
  // call inside each page, because a guard you have to remember to write is a
8
8
  // guard someone eventually forgets—and the page that forgets it is the one
@@ -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;