astroidjs 0.17.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 +88 -72
- package/dist/config.d.ts +26 -0
- 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/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 +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/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
|
//
|
|
@@ -157,6 +157,8 @@ async function cmdDoctor(cwd, flags) {
|
|
|
157
157
|
astroidUsesQueues,
|
|
158
158
|
astroidCrons,
|
|
159
159
|
checkWranglerPreviews,
|
|
160
|
+
astroidRunsMigrations,
|
|
161
|
+
migrationsOwnershipError,
|
|
160
162
|
generateAstroidReleaseWorkflow,
|
|
161
163
|
ASTROID_RELEASE_WORKFLOW_PATH,
|
|
162
164
|
} = await import(GENERATORS_URL);
|
|
@@ -326,12 +328,21 @@ async function cmdDoctor(cwd, flags) {
|
|
|
326
328
|
// 3. migrations directory: the D1 `migrations_dir` wrangler.jsonc declares,
|
|
327
329
|
// or `migrations/`, the generated default, when it names none. A site that
|
|
328
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.
|
|
329
334
|
const wranglerText = existsSync(wranglerPath) ? readFileSync(wranglerPath, "utf8") : "";
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
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
|
+
}
|
|
335
346
|
|
|
336
347
|
// 4. Local secret provisioning—which modules will run dormant under
|
|
337
348
|
// `astroid dev`, and what to set to wake them.
|
|
@@ -398,7 +409,33 @@ async function cmdAstro(cwd, subcommand, flags, rest) {
|
|
|
398
409
|
);
|
|
399
410
|
}
|
|
400
411
|
const child = spawn(process.execPath, [astroBin, subcommand, ...rest], { stdio: "inherit", cwd });
|
|
401
|
-
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
|
+
}
|
|
402
439
|
}
|
|
403
440
|
|
|
404
441
|
/** Resolve a project-local CLI bin (astro, wrangler) to an absolute path via the
|
|
@@ -501,7 +538,9 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
501
538
|
const assumeYes = rest.includes("--yes") || rest.includes("-y");
|
|
502
539
|
const remoteArgs = rest.includes("--local") ? [] : ["--remote"];
|
|
503
540
|
|
|
504
|
-
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);
|
|
505
544
|
const wranglerPath = join(cwd, "wrangler.jsonc");
|
|
506
545
|
if (!existsSync(wranglerPath)) fail("wrangler.jsonc not found — run inside an Astroid project.");
|
|
507
546
|
|
|
@@ -521,7 +560,11 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
521
560
|
out(" Provision:");
|
|
522
561
|
if (plan.length === 0) out(" (all bindings already have ids)");
|
|
523
562
|
for (const s of plan) out(` wrangler ${s.args.join(" ")}`);
|
|
524
|
-
out(
|
|
563
|
+
out(
|
|
564
|
+
migrate
|
|
565
|
+
? `\n Migrate: wrangler d1 migrations apply DB ${remoteArgs.join(" ")}`.trimEnd()
|
|
566
|
+
: `\n Migrate: ${ASTROID_SKIP_MIGRATIONS_NOTE}`,
|
|
567
|
+
);
|
|
525
568
|
out(" Secrets: wrangler secret put SESSION_SECRET (prompted)");
|
|
526
569
|
out(" Deploy: wrangler deploy\n");
|
|
527
570
|
if (!facts.hasAccount) {
|
|
@@ -602,10 +645,14 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
602
645
|
}
|
|
603
646
|
}
|
|
604
647
|
|
|
605
|
-
// 2) Migrations.
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
+
}
|
|
609
656
|
|
|
610
657
|
// 3) Secrets (interactive; wrangler prompts for the value).
|
|
611
658
|
out("\n▸ wrangler secret put SESSION_SECRET");
|
|
@@ -657,7 +704,7 @@ Usage:
|
|
|
657
704
|
astroid generate [--config <path>] [--cwd <dir>] regenerate src/schema.ts, src/worker.ts, src/middleware.ts
|
|
658
705
|
astroid doctor [--config <path>] [--cwd <dir>] validate config, bindings, and generated-file freshness
|
|
659
706
|
astroid dev [...astro args] regenerate, then run \`astro dev\`
|
|
660
|
-
astroid build [...astro args] regenerate,
|
|
707
|
+
astroid build [...astro args] regenerate, run \`astro build\`, then drop legacy_env from its Wrangler config
|
|
661
708
|
astroid deploy [--dry-run] [--yes] [--local] provision bindings + migrate + secrets + deploy
|
|
662
709
|
astroid ship production | preview migrate D1, then deploy or preview (Workers Builds runs this)
|
|
663
710
|
astroid provision [--dry-run] [--yes] create the resources wrangler.jsonc names by placeholder, staging included
|
|
@@ -875,76 +922,45 @@ function listStoreSecrets(wranglerBin, cwd, storeId, secretNamesFromList) {
|
|
|
875
922
|
// `astroid ship` is what Workers Builds runs, so the deploy logic lives in the
|
|
876
923
|
// repository instead of a dashboard field. An account move once rewrote a
|
|
877
924
|
// 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
|
|
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.
|
|
879
927
|
//
|
|
880
928
|
// astroid ship production the deploy/production build: migrate D1, then deploy
|
|
881
929
|
// astroid ship preview every other branch: migrate the staging D1, then
|
|
882
930
|
// `wrangler preview`, named for the branch
|
|
883
|
-
async function cmdShip(cwd, target) {
|
|
931
|
+
async function cmdShip(cwd, target, flags) {
|
|
884
932
|
if (target !== "production" && target !== "preview") {
|
|
885
933
|
fail("Usage: astroid ship production | preview");
|
|
886
934
|
}
|
|
887
935
|
const wranglerBin = resolveBin(cwd, "wrangler", "wrangler");
|
|
888
936
|
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
937
|
const wranglerPath = join(cwd, "wrangler.jsonc");
|
|
895
938
|
if (!existsSync(wranglerPath)) fail("wrangler.jsonc not found — run inside an Astroid project.");
|
|
896
939
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
+
}
|
|
901
963
|
}
|
|
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
964
|
}
|
|
949
965
|
|
|
950
966
|
async function main() {
|
|
@@ -969,7 +985,7 @@ async function main() {
|
|
|
969
985
|
await cmdDeploy(cwd, flags, rest);
|
|
970
986
|
break;
|
|
971
987
|
case "ship":
|
|
972
|
-
await cmdShip(cwd, rest[0]);
|
|
988
|
+
await cmdShip(cwd, rest[0], flags);
|
|
973
989
|
break;
|
|
974
990
|
case "provision":
|
|
975
991
|
await cmdProvision(cwd, rest);
|
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`
|
|
@@ -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;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// The post-build fix `astroid build` applies to the Worker config that
|
|
4
|
+
// `@astrojs/cloudflare` writes.
|
|
5
|
+
//
|
|
6
|
+
// The adapter writes the built config from the Wrangler version it bundles, and
|
|
7
|
+
// some of those versions include `legacy_env: true`. Current Wrangler rejects
|
|
8
|
+
// the field, so a Workers Builds deploy running a newer Wrangler fails on the
|
|
9
|
+
// build's own output. `true` was always the default, so deleting the field
|
|
10
|
+
// changes nothing else.
|
|
11
|
+
//
|
|
12
|
+
// Pure: text in, text out, so it's tested directly and the CLI only does I/O.
|
|
13
|
+
/** Where `@astrojs/cloudflare` writes the built config when there's no redirect. */
|
|
14
|
+
export const ASTROID_BUILT_WRANGLER_CONFIG = "dist/server/wrangler.json";
|
|
15
|
+
/** The redirect file Wrangler reads to find the built config. */
|
|
16
|
+
export const WRANGLER_DEPLOY_REDIRECT = ".wrangler/deploy/config.json";
|
|
17
|
+
/**
|
|
18
|
+
* The built config's path, relative to the project root, found the way
|
|
19
|
+
* Wrangler finds it: the `configPath` in `.wrangler/deploy/config.json`, which
|
|
20
|
+
* is relative to that file's own folder, else {@link ASTROID_BUILT_WRANGLER_CONFIG}.
|
|
21
|
+
* Pass the redirect's text, or `null` when there isn't one.
|
|
22
|
+
*/
|
|
23
|
+
export function builtWranglerConfigPath(redirect) {
|
|
24
|
+
if (redirect === null)
|
|
25
|
+
return ASTROID_BUILT_WRANGLER_CONFIG;
|
|
26
|
+
let configPath;
|
|
27
|
+
try {
|
|
28
|
+
configPath = JSON.parse(redirect).configPath;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return ASTROID_BUILT_WRANGLER_CONFIG;
|
|
32
|
+
}
|
|
33
|
+
if (typeof configPath !== "string" || !configPath)
|
|
34
|
+
return ASTROID_BUILT_WRANGLER_CONFIG;
|
|
35
|
+
if (configPath.startsWith("/"))
|
|
36
|
+
return configPath;
|
|
37
|
+
// Resolved against `.wrangler/deploy/`, the redirect's folder.
|
|
38
|
+
const parts = [".wrangler", "deploy"];
|
|
39
|
+
for (const segment of configPath.split("/")) {
|
|
40
|
+
if (segment === "..")
|
|
41
|
+
parts.pop();
|
|
42
|
+
else if (segment !== "." && segment !== "")
|
|
43
|
+
parts.push(segment);
|
|
44
|
+
}
|
|
45
|
+
return parts.join("/");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Remove `legacy_env` from a built Wrangler config. Returns the new text, or
|
|
49
|
+
* `null` when the field isn't there, so the caller leaves the file untouched.
|
|
50
|
+
* Keeps the file's indentation and every other key in its order. Throws when
|
|
51
|
+
* the text isn't a JSON object.
|
|
52
|
+
*/
|
|
53
|
+
export function stripLegacyEnv(text) {
|
|
54
|
+
const config = JSON.parse(text);
|
|
55
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
56
|
+
throw new TypeError("The built Wrangler config isn't a JSON object.");
|
|
57
|
+
}
|
|
58
|
+
if (!Object.hasOwn(config, "legacy_env"))
|
|
59
|
+
return null;
|
|
60
|
+
const { legacy_env: _removed, ...rest } = config;
|
|
61
|
+
const indent = /^\{\r?\n([ \t]+)"/.exec(text)?.[1] ?? "";
|
|
62
|
+
const trailing = /\r?\n$/.exec(text)?.[0] ?? "";
|
|
63
|
+
return JSON.stringify(rest, null, indent) + trailing;
|
|
64
|
+
}
|
|
@@ -5,6 +5,25 @@ export interface GeneratedFile {
|
|
|
5
5
|
path: string;
|
|
6
6
|
contents: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The paths of the regenerated trio, relative to the project root. Astroid's
|
|
10
|
+
* code, tested here, not the site's: spread it into a site's
|
|
11
|
+
* `coverage.exclude` so an Astroid upgrade that adds lines to them doesn't
|
|
12
|
+
* move the site's coverage.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* // vitest.config.ts
|
|
16
|
+
* import { ASTROID_GENERATED_FILES } from "astroidjs";
|
|
17
|
+
*
|
|
18
|
+
* export default defineConfig({
|
|
19
|
+
* test: { coverage: { exclude: [...ASTROID_GENERATED_FILES] } },
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* A site that lists them by hand would miss a generated file a later release
|
|
24
|
+
* adds; this list gains it.
|
|
25
|
+
*/
|
|
26
|
+
export declare const ASTROID_GENERATED_FILES: readonly ["src/schema.ts", "src/worker.ts", "src/middleware.ts"];
|
|
8
27
|
/**
|
|
9
28
|
* The regenerated trio—the files that are a pure function of the Astroid config
|
|
10
29
|
* and carry a "do not hand-edit" banner. `astroid generate` writes exactly these,
|
package/dist/project/generate.js
CHANGED
|
@@ -18,12 +18,36 @@ import { ASTROID_VITALS_BINDING, astroidVitalsDataset } from "../analytics/index
|
|
|
18
18
|
import { astroidCheckoutVars } from "../commerce/checkout-scaffold.js";
|
|
19
19
|
import { astroidCommerceProviders } from "../commerce/roles.js";
|
|
20
20
|
import { COMMERCE_PROVIDER_SECRETS, COMMERCE_PROVIDER_SETUP, commerceSecretNames, } from "../commerce/secrets.js";
|
|
21
|
-
import { ASTROID_QUEUE_BINDING, astroidCrons, astroidQueueNames, astroidUsesQueues, } from "../queues/messages.js";
|
|
21
|
+
import { ASTROID_QUEUE_BINDING, ASTROID_QUEUE_RETRY_DELAY, astroidCrons, astroidQueueNames, astroidUsesQueues, } from "../queues/messages.js";
|
|
22
22
|
import { ASTROID_EDIT_SESSION_CLASS, ASTROID_REALTIME_BINDING, ASTROID_REALTIME_MIGRATION_TAG, usesRealtime, } from "../realtime/scaffold.js";
|
|
23
23
|
import { ASTROID_SECRET_PLACEHOLDER } from "../secrets.js";
|
|
24
24
|
import { tenancyZone } from "../tenancy/index.js";
|
|
25
25
|
import { generateAstroidSchema } from "../schema/generate.js";
|
|
26
|
+
import { ASTROID_AI_GATEWAY_VAR } from "../worker/gateway.js";
|
|
26
27
|
import { generateAstroidMiddleware, generateAstroidWorker } from "../worker/generate.js";
|
|
28
|
+
/**
|
|
29
|
+
* The paths of the regenerated trio, relative to the project root. Astroid's
|
|
30
|
+
* code, tested here, not the site's: spread it into a site's
|
|
31
|
+
* `coverage.exclude` so an Astroid upgrade that adds lines to them doesn't
|
|
32
|
+
* move the site's coverage.
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* // vitest.config.ts
|
|
36
|
+
* import { ASTROID_GENERATED_FILES } from "astroidjs";
|
|
37
|
+
*
|
|
38
|
+
* export default defineConfig({
|
|
39
|
+
* test: { coverage: { exclude: [...ASTROID_GENERATED_FILES] } },
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* A site that lists them by hand would miss a generated file a later release
|
|
44
|
+
* adds; this list gains it.
|
|
45
|
+
*/
|
|
46
|
+
export const ASTROID_GENERATED_FILES = [
|
|
47
|
+
"src/schema.ts",
|
|
48
|
+
"src/worker.ts",
|
|
49
|
+
"src/middleware.ts",
|
|
50
|
+
];
|
|
27
51
|
/**
|
|
28
52
|
* The regenerated trio—the files that are a pure function of the Astroid config
|
|
29
53
|
* and carry a "do not hand-edit" banner. `astroid generate` writes exactly these,
|
|
@@ -31,10 +55,11 @@ import { generateAstroidMiddleware, generateAstroidWorker } from "../worker/gene
|
|
|
31
55
|
* once files (wrangler.jsonc, astro.config, auth.ts) are NOT here by design.
|
|
32
56
|
*/
|
|
33
57
|
export function generateAstroidProject(config) {
|
|
58
|
+
const [schema, worker, middleware] = ASTROID_GENERATED_FILES;
|
|
34
59
|
return [
|
|
35
|
-
{ path:
|
|
36
|
-
{ path:
|
|
37
|
-
{ path:
|
|
60
|
+
{ path: schema, contents: generateAstroidSchema(config) },
|
|
61
|
+
{ path: worker, contents: generateAstroidWorker(config) },
|
|
62
|
+
{ path: middleware, contents: generateAstroidMiddleware(config) },
|
|
38
63
|
];
|
|
39
64
|
}
|
|
40
65
|
/**
|
|
@@ -174,6 +199,10 @@ export function generateAstroidWrangler(config) {
|
|
|
174
199
|
p(` "max_batch_size": ${config.queues?.maxBatchSize ?? 10},`);
|
|
175
200
|
p(` "max_batch_timeout": ${config.queues?.maxBatchTimeout ?? 30},`);
|
|
176
201
|
p(` "max_retries": ${config.queues?.maxRetries ?? 5},`);
|
|
202
|
+
// A wait between deliveries, so a failed message doesn't hit a provider
|
|
203
|
+
// that's already failing or rate limiting again the same second. The queue
|
|
204
|
+
// owns retries (the consumer seam says so), so it owns the backoff too.
|
|
205
|
+
p(` "retry_delay": ${config.queues?.retryDelay ?? ASTROID_QUEUE_RETRY_DELAY},`);
|
|
177
206
|
p(` "dead_letter_queue": ${JSON.stringify(dlq)},`);
|
|
178
207
|
p(" },");
|
|
179
208
|
p(" ],");
|
|
@@ -233,6 +262,11 @@ export function generateAstroidWrangler(config) {
|
|
|
233
262
|
p(` "SITE_URL": ${JSON.stringify(primaryHost ? `https://${primaryHost}` : `https://${key}.workers.dev`)},`);
|
|
234
263
|
p(" // The editor allowlist / owner. Wire this into your auth seam (src/auth.ts).");
|
|
235
264
|
p(' "OWNER_EMAIL": "",');
|
|
265
|
+
p(" // AI Gateway for the editor's AI assists: request logs, latency and error");
|
|
266
|
+
p(" // rates, and caching. Empty calls Workers AI directly. Create a gateway,");
|
|
267
|
+
p(" // put its id here, and first say on the privacy page that its log holds");
|
|
268
|
+
p(" // the text editors send to the assists.");
|
|
269
|
+
p(` "${ASTROID_AI_GATEWAY_VAR}": "",`);
|
|
236
270
|
p(" // Edge caching for published pages (ADR 0004). OFF by default, and the");
|
|
237
271
|
p(" // default is the safe state: with it off every render is `no-store` and");
|
|
238
272
|
p(" // the Worker cache layer stores nothing.");
|
package/dist/project/index.d.ts
CHANGED
package/dist/project/index.js
CHANGED
|
@@ -19,6 +19,23 @@ export interface ScaffoldFile {
|
|
|
19
19
|
*/
|
|
20
20
|
marker?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* `migrations/0004_page_redirects.sql`: the table that keeps a renamed page's
|
|
24
|
+
* old URL working (louise-toolkit's `pageRedirects`). The same DDL drizzle-kit
|
|
25
|
+
* writes for it, with `IF NOT EXISTS`, since a site that already added the
|
|
26
|
+
* table by hand must not fail on it.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ASTROID_PAGE_REDIRECTS_MIGRATION: string;
|
|
29
|
+
/**
|
|
30
|
+
* `migrations/0005_media_alt_undecided.sql`: the one-time data migration for
|
|
31
|
+
* the three alt text states. Before louise-toolkit 0.35, an empty alt meant
|
|
32
|
+
* both "not written" and "cleared", so an existing `''` is ambiguous. This
|
|
33
|
+
* makes each one "not written" (NULL), so nothing silently becomes decorative,
|
|
34
|
+
* and the owner marks what is. The statement is louise-toolkit's
|
|
35
|
+
* `MEDIA_ALT_UNDECIDED_SQL("media")`, written out rather than imported so the
|
|
36
|
+
* CLI doesn't load `louise-toolkit/editor` and its drizzle-orm peer.
|
|
37
|
+
*/
|
|
38
|
+
export declare const ASTROID_MEDIA_ALT_MIGRATION: string;
|
|
22
39
|
/**
|
|
23
40
|
* Every scaffold-once file this config implies.
|
|
24
41
|
*
|
package/dist/project/scaffold.js
CHANGED
|
@@ -58,6 +58,69 @@ function generateAstroidPagesHooks() {
|
|
|
58
58
|
"",
|
|
59
59
|
].join("\n");
|
|
60
60
|
}
|
|
61
|
+
/** `src/status-checks.ts`—the site's own status checks, scaffolded once. */
|
|
62
|
+
function generateAstroidStatusChecks() {
|
|
63
|
+
return [
|
|
64
|
+
"// The site's own checks for the public status route (GET /api/louise/status),",
|
|
65
|
+
"// which an outside probe reads. The generated worker spreads `statusChecks`",
|
|
66
|
+
"// after Astroid's own `d1` and `content` checks. Scaffolded once and yours to",
|
|
67
|
+
"// edit.",
|
|
68
|
+
"//",
|
|
69
|
+
"// A check gets `env` and an abort signal, and returns true, false, or",
|
|
70
|
+
"// `{ ok, ageMs }`. Keep each one cheap: anyone can make it run. Its name is",
|
|
71
|
+
"// in the public response, so don't put anything in one you wouldn't publish.",
|
|
72
|
+
'import type { StatusCheck } from "louise-toolkit/editor";',
|
|
73
|
+
"",
|
|
74
|
+
"export const statusChecks: Record<string, StatusCheck<CloudflareEnv>> = {",
|
|
75
|
+
" // For example, fail when the daily health scan is over 36 hours old:",
|
|
76
|
+
" //",
|
|
77
|
+
' // import { ageCheck } from "louise-toolkit/editor";',
|
|
78
|
+
' // import { readHealthSummary } from "louise-toolkit/health";',
|
|
79
|
+
" //",
|
|
80
|
+
" // healthScan: ageCheck(",
|
|
81
|
+
" // async (env) => (await readHealthSummary(env.RL))?.checkedAt,",
|
|
82
|
+
" // 36 * 60 * 60 * 1000,",
|
|
83
|
+
" // ),",
|
|
84
|
+
"};",
|
|
85
|
+
"",
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* `migrations/0004_page_redirects.sql`: the table that keeps a renamed page's
|
|
90
|
+
* old URL working (louise-toolkit's `pageRedirects`). The same DDL drizzle-kit
|
|
91
|
+
* writes for it, with `IF NOT EXISTS`, since a site that already added the
|
|
92
|
+
* table by hand must not fail on it.
|
|
93
|
+
*/
|
|
94
|
+
export const ASTROID_PAGE_REDIRECTS_MIGRATION = [
|
|
95
|
+
"-- Page redirects: a renamed page's old URL answers a 301 to the new one.",
|
|
96
|
+
"-- pagesRoute and versionsRoute record `/old → /new` when a slug changes, and",
|
|
97
|
+
"-- the middleware's redirectFor serves them. Scaffolded by astroidjs.",
|
|
98
|
+
"CREATE TABLE IF NOT EXISTS `page_redirects` (",
|
|
99
|
+
"\t`from_path` text PRIMARY KEY NOT NULL,",
|
|
100
|
+
"\t`to_path` text NOT NULL,",
|
|
101
|
+
"\t`code` integer DEFAULT 301 NOT NULL,",
|
|
102
|
+
"\t`created_at` integer",
|
|
103
|
+
");",
|
|
104
|
+
"",
|
|
105
|
+
].join("\n");
|
|
106
|
+
/**
|
|
107
|
+
* `migrations/0005_media_alt_undecided.sql`: the one-time data migration for
|
|
108
|
+
* the three alt text states. Before louise-toolkit 0.35, an empty alt meant
|
|
109
|
+
* both "not written" and "cleared", so an existing `''` is ambiguous. This
|
|
110
|
+
* makes each one "not written" (NULL), so nothing silently becomes decorative,
|
|
111
|
+
* and the owner marks what is. The statement is louise-toolkit's
|
|
112
|
+
* `MEDIA_ALT_UNDECIDED_SQL("media")`, written out rather than imported so the
|
|
113
|
+
* CLI doesn't load `louise-toolkit/editor` and its drizzle-orm peer.
|
|
114
|
+
*/
|
|
115
|
+
export const ASTROID_MEDIA_ALT_MIGRATION = [
|
|
116
|
+
"-- Alt text has three states: NULL is not written yet, '' is decorative (an",
|
|
117
|
+
"-- image the owner marked for screen readers to skip), and anything else is",
|
|
118
|
+
"-- the description. An existing '' predates the decorative state, so it's",
|
|
119
|
+
'-- ambiguous; this makes each one "not written". It runs once, before the',
|
|
120
|
+
"-- health scan starts counting only NULL as missing. Scaffolded by astroidjs.",
|
|
121
|
+
'UPDATE "media" SET "alt" = NULL WHERE "alt" = \'\';',
|
|
122
|
+
"",
|
|
123
|
+
].join("\n");
|
|
61
124
|
/** `src/settings-hooks.ts`—the site's settings sanitizers, scaffolded once. */
|
|
62
125
|
function generateAstroidSettingsHooks() {
|
|
63
126
|
return [
|
|
@@ -99,6 +162,12 @@ export function generateAstroidScaffoldFiles(config) {
|
|
|
99
162
|
const catalogSql = generateCatalogMigrationSql(config);
|
|
100
163
|
if (catalogSql)
|
|
101
164
|
files.push({ path: "migrations/0003_catalog.sql", contents: catalogSql });
|
|
165
|
+
// --- louise-toolkit 0.35's two schema changes -----------------------------
|
|
166
|
+
// Numbered after the catalog's 0003, and written into an existing site by
|
|
167
|
+
// `astroid generate` because a missing scaffold file is always written. The
|
|
168
|
+
// alt update is a no-op on a fresh database. Wrangler tracks migrations by
|
|
169
|
+
// filename, so a site that already has its own 0004 keeps both.
|
|
170
|
+
files.push({ path: "migrations/0004_page_redirects.sql", contents: ASTROID_PAGE_REDIRECTS_MIGRATION }, { path: "migrations/0005_media_alt_undecided.sql", contents: ASTROID_MEDIA_ALT_MIGRATION });
|
|
102
171
|
// --- the CWV beacon -------------------------------------------------------
|
|
103
172
|
// A static file under public/, so it is same-origin and covered by
|
|
104
173
|
// `script-src 'self'`—an inline script carrying generated content could not
|
|
@@ -126,6 +195,10 @@ export function generateAstroidScaffoldFiles(config) {
|
|
|
126
195
|
if (config.pages?.hooks) {
|
|
127
196
|
files.push({ path: "src/pages-hooks.ts", contents: generateAstroidPagesHooks() });
|
|
128
197
|
}
|
|
198
|
+
// --- status: the site's own probe checks -----------------------------------
|
|
199
|
+
if (config.status?.checks) {
|
|
200
|
+
files.push({ path: "src/status-checks.ts", contents: generateAstroidStatusChecks() });
|
|
201
|
+
}
|
|
129
202
|
// --- settings: the sanitize + read seam ------------------------------------
|
|
130
203
|
// Only when asked for (settings.hooks): the generated worker and the Actions
|
|
131
204
|
// surface both import it, so it must exist whenever either does. Scaffolded
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AstroidConfig } from "../config.js";
|
|
2
|
+
/** Whether this app applies D1 migrations when it deploys. Default `true`. */
|
|
3
|
+
export declare function astroidRunsMigrations(config: AstroidConfig): boolean;
|
|
4
|
+
/** One step of a ship, in order. */
|
|
5
|
+
export type ShipStep =
|
|
6
|
+
/** Run `wrangler` with these arguments; a non-zero exit stops the ship. */
|
|
7
|
+
{
|
|
8
|
+
run: string[];
|
|
9
|
+
}
|
|
10
|
+
/** Print a line. */
|
|
11
|
+
| {
|
|
12
|
+
note: string;
|
|
13
|
+
}
|
|
14
|
+
/** Write a file, relative to the project root, before the next step. */
|
|
15
|
+
| {
|
|
16
|
+
write: {
|
|
17
|
+
path: string;
|
|
18
|
+
contents: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/** Where `ship preview` writes the config that names the staging database. */
|
|
22
|
+
export declare const ASTROID_PREVIEW_MIGRATIONS_CONFIG = ".wrangler/astroid-preview-migrations.jsonc";
|
|
23
|
+
/** Printed in place of the migrations step when `deploy.migrations` is `false`. */
|
|
24
|
+
export declare const ASTROID_SKIP_MIGRATIONS_NOTE = "(deploy.migrations is false, so this app applies no migrations: another app owns this database's schema)";
|
|
25
|
+
interface ShipContext {
|
|
26
|
+
/** The project's `wrangler.jsonc` text. */
|
|
27
|
+
wrangler: string;
|
|
28
|
+
/** The project root's absolute path. */
|
|
29
|
+
root: string;
|
|
30
|
+
/** Workers Builds' `WORKERS_CI_BRANCH`, which names the Preview. */
|
|
31
|
+
branch?: string;
|
|
32
|
+
}
|
|
33
|
+
/** The steps `astroid ship <target>` runs for a project. */
|
|
34
|
+
export declare function astroidShipPlan(target: "production" | "preview", config: AstroidConfig, { wrangler, root, branch }: ShipContext): ShipStep[];
|
|
35
|
+
/**
|
|
36
|
+
* `astroid doctor`'s check on who migrates this app's database. Returns an
|
|
37
|
+
* error when `deploy.migrations` is `false` but `wrangler.jsonc` still names a
|
|
38
|
+
* `migrations_dir` for `DB`, since that contradiction means someone expects
|
|
39
|
+
* this app to migrate. `null` when there's nothing to report.
|
|
40
|
+
*/
|
|
41
|
+
export declare function migrationsOwnershipError(config: AstroidConfig, wrangler: string): string | null;
|
|
42
|
+
export {};
|