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.
- package/README.md +6 -7
- package/bin/astroid.mjs +107 -75
- package/dist/commerce/adapters.js +3 -3
- package/dist/commerce/checkout.js +1 -1
- package/dist/commerce/mirror.js +5 -4
- package/dist/commerce/roles.js +3 -3
- package/dist/config.d.ts +57 -14
- package/dist/config.js +27 -7
- package/dist/portal/guard.js +1 -1
- 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 +3 -0
- package/dist/project/index.js +3 -0
- package/dist/project/migrations.d.ts +26 -0
- package/dist/project/migrations.js +78 -0
- package/dist/project/scaffold.d.ts +23 -0
- package/dist/project/scaffold.js +86 -2
- 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/collections.d.ts +6 -0
- package/dist/schema/collections.js +14 -2
- package/dist/schema/framework.d.ts +4 -3
- package/dist/schema/framework.js +8 -7
- package/dist/worker/gateway.d.ts +12 -0
- package/dist/worker/gateway.js +32 -0
- package/dist/worker/generate.js +60 -6
- 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/dist/workflow/advance.js +1 -1
- package/dist/workflow/config.js +1 -1
- package/package.json +3 -3
- package/src/components/StageBar.astro +1 -1
- 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
|
|
39
|
-
|
|
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: "
|
|
45
|
+
key: "example",
|
|
47
46
|
archetype: "storefront",
|
|
48
|
-
theme: { name: "
|
|
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: "
|
|
58
|
+
key: "example-studio",
|
|
60
59
|
archetype: "portfolio",
|
|
61
|
-
theme: { name: "
|
|
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,
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
else
|
|
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) =>
|
|
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
|
|
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(
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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,
|
|
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
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
|
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
|
package/dist/commerce/mirror.js
CHANGED
|
@@ -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
|
|
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 (
|
|
24
|
-
// in schema.site.ts and joined in the site's loader).
|
|
25
|
-
// existing overlay shape predates Astroid and must
|
|
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.
|
package/dist/commerce/roles.js
CHANGED
|
@@ -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
|
-
//
|
|
13
|
-
// (
|
|
14
|
-
//
|
|
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
|
|
12
|
-
*
|
|
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
|
|
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`:
|
|
156
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
322
|
-
*
|
|
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 (
|
|
405
|
-
*
|
|
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
|
-
* `"
|
|
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 (
|
|
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
|
-
* (
|
|
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 (
|
|
12
|
-
//
|
|
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
|
-
//
|
|
27
|
-
//
|
|
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: "
|
|
72
|
+
* key: "example",
|
|
68
73
|
* archetype: "storefront",
|
|
69
|
-
* theme: { name: "
|
|
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 " +
|
package/dist/portal/guard.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Role-gated routing for the portal.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
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;
|