astroidjs 0.18.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 +19 -3
- 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 +31 -14
- package/dist/config.js +27 -7
- package/dist/portal/guard.js +1 -1
- package/dist/project/index.d.ts +1 -0
- package/dist/project/index.js +1 -0
- package/dist/project/migrations.d.ts +26 -0
- package/dist/project/migrations.js +78 -0
- package/dist/project/scaffold.d.ts +6 -0
- package/dist/project/scaffold.js +15 -4
- package/dist/schema/collections.d.ts +6 -0
- package/dist/schema/collections.js +14 -2
- package/dist/schema/framework.js +1 -1
- package/dist/workflow/advance.js +1 -1
- package/dist/workflow/config.js +1 -1
- package/package.json +1 -1
- 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
|
@@ -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") {
|
|
@@ -196,7 +212,7 @@ async function cmdDoctor(cwd, flags) {
|
|
|
196
212
|
// that names a module whose seam was never written produces a project that
|
|
197
213
|
// cannot resolve its own imports. This is precisely the state that used to
|
|
198
214
|
// report "healthy" with two warnings and exit 0.
|
|
199
|
-
for (const file of generateAstroidScaffoldFiles(config)) {
|
|
215
|
+
for (const file of await scaffoldFilesFor(cwd, generateAstroidScaffoldFiles(config))) {
|
|
200
216
|
if (file.apply === "append-once") continue; // accumulated, not owned—see below
|
|
201
217
|
if (existsSync(join(cwd, file.path))) ok(`${file.path} present`);
|
|
202
218
|
else err(`${file.path} is missing (required by your config) — run \`astroid generate\`.`);
|
|
@@ -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.
|
|
@@ -323,9 +324,9 @@ export interface TenancyConfig {
|
|
|
323
324
|
* sign-in) instead of JSON, and every data load on that host silently fails
|
|
324
325
|
* while the same code works on the apex.
|
|
325
326
|
*
|
|
326
|
-
* That is not hypothetical—it is why this default exists (found on
|
|
327
|
-
*
|
|
328
|
-
*
|
|
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).
|
|
329
330
|
*
|
|
330
331
|
* Set `[]` to rewrite everything, or add prefixes for other host-agnostic
|
|
331
332
|
* surfaces (`/_actions`, `/webhooks`). Matching is prefix-based on a path
|
|
@@ -407,8 +408,8 @@ export interface SettingsConfig {
|
|
|
407
408
|
* on top of (or, with `columns: []`, instead of) Astroid's base columns. The
|
|
408
409
|
* generated `settingsRoute` + Action accept these; the Settings panel writes
|
|
409
410
|
* them through the `settingsExtension` groups a site supplies to
|
|
410
|
-
* `mountSettings`. A site with a rich settings shape (
|
|
411
|
-
*
|
|
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.
|
|
412
413
|
*/
|
|
413
414
|
customKeys?: string[];
|
|
414
415
|
/** Extra media-library image keys beyond the base logo/favicon/OG defaults—*
|
|
@@ -456,7 +457,22 @@ export interface PagesConfig {
|
|
|
456
457
|
* clamping a title, or filling a new page's defaults.
|
|
457
458
|
*/
|
|
458
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[];
|
|
459
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[];
|
|
460
476
|
export interface StatusConfig {
|
|
461
477
|
/**
|
|
462
478
|
* Add the site's own checks to the public status route, from the
|
|
@@ -484,7 +500,7 @@ export interface DeployConfig {
|
|
|
484
500
|
export interface AstroidConfig {
|
|
485
501
|
/**
|
|
486
502
|
* Stable project slug—the worker/D1/R2 base name and default subdomain (for example,
|
|
487
|
-
* `"
|
|
503
|
+
* `"example"`). Required and non-empty; it drives the generated binding names.
|
|
488
504
|
*/
|
|
489
505
|
key: string;
|
|
490
506
|
/** Hostnames this site serves (prod + preview), for custom-domain routes. */
|
|
@@ -505,7 +521,7 @@ export interface AstroidConfig {
|
|
|
505
521
|
* A site-provided section catalog that REPLACES the built-in one for
|
|
506
522
|
* SERVER-side validation + sanitization of `pages.sections` (the generated
|
|
507
523
|
* pages route + versions route). A site with bespoke section designs—its own
|
|
508
|
-
* `.astro` components and field defs (
|
|
524
|
+
* `.astro` components and field defs (a dozen or more sections)—registers them
|
|
509
525
|
* here so writes to its custom `_type`s validate instead of 422-ing against the
|
|
510
526
|
* built-in vocabulary. The on-canvas editor already uses the site's catalog
|
|
511
527
|
* (its `mountSections` call passes it); this closes the server half so both
|
|
@@ -561,7 +577,8 @@ export interface AstroidConfig {
|
|
|
561
577
|
* Force the contact form + `inquiries` table on or off. Omit to detect from
|
|
562
578
|
* the config (a `contact` section, or a wholesale-inquiry module). Set `true`
|
|
563
579
|
* when a bespoke section captures inquiries under a name Astroid can't see
|
|
564
|
-
* (
|
|
580
|
+
* (for example, a custom `contactForm` section); set `false` to suppress it
|
|
581
|
+
* entirely.
|
|
565
582
|
*/
|
|
566
583
|
inquiries?: boolean;
|
|
567
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
|
package/dist/project/index.d.ts
CHANGED
package/dist/project/index.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ScaffoldFile } from "./scaffold.js";
|
|
2
|
+
/**
|
|
3
|
+
* The `DB` binding's `migrations_dir` from `wrangler.jsonc`, or Wrangler's
|
|
4
|
+
* default `migrations` when the binding sets none or the file is missing.
|
|
5
|
+
* Returned as written, without a trailing slash.
|
|
6
|
+
*/
|
|
7
|
+
export declare function astroidMigrationsDir(wrangler: string | null | undefined): string;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve each scaffold file's path against the site's migrations directory.
|
|
10
|
+
* Files that aren't migrations pass through unchanged. For a migration:
|
|
11
|
+
*
|
|
12
|
+
* - **Already there under any number:** the path is that file's, so a caller
|
|
13
|
+
* that skips existing files skips it. A site that copied `page_redirects`
|
|
14
|
+
* in by hand as `0009_page_redirects.sql` keeps that file and gets no second.
|
|
15
|
+
* - **Default number free and past the newest migration:** the default, so a
|
|
16
|
+
* standard project numbers exactly as before.
|
|
17
|
+
* - **Otherwise:** the next number after the newest migration, at the same
|
|
18
|
+
* width. A migration never takes a number the site already uses, and never
|
|
19
|
+
* sorts before one that already ran.
|
|
20
|
+
*
|
|
21
|
+
* `existing` is the file names in `migrationsDir`, in any order.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveAstroidScaffoldPaths(files: ScaffoldFile[], { migrationsDir, existing }: {
|
|
24
|
+
migrationsDir: string;
|
|
25
|
+
existing: string[];
|
|
26
|
+
}): ScaffoldFile[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// Where a scaffolded D1 migration lands in a site. The scaffold names each
|
|
4
|
+
// migration with a default path (`migrations/0004_page_redirects.sql`), but a
|
|
5
|
+
// site can point its `DB` binding at another directory with `migrations_dir`,
|
|
6
|
+
// and it may already number its own migrations past the default. Wrangler
|
|
7
|
+
// applies only the directory in `migrations_dir` and tracks each file by name,
|
|
8
|
+
// so a migration written anywhere else never runs, and one written onto a
|
|
9
|
+
// number the site already uses leaves two files claiming that number.
|
|
10
|
+
import { parseJsonc } from "./previews.js";
|
|
11
|
+
/** Wrangler's own default when a D1 binding sets no `migrations_dir`. */
|
|
12
|
+
const DEFAULT_MIGRATIONS_DIR = "migrations";
|
|
13
|
+
/** `0004_page_redirects.sql` → number `"0004"`, name `page_redirects`. */
|
|
14
|
+
const MIGRATION_FILE = /^(\d+)_(.+)\.sql$/;
|
|
15
|
+
/**
|
|
16
|
+
* The `DB` binding's `migrations_dir` from `wrangler.jsonc`, or Wrangler's
|
|
17
|
+
* default `migrations` when the binding sets none or the file is missing.
|
|
18
|
+
* Returned as written, without a trailing slash.
|
|
19
|
+
*/
|
|
20
|
+
export function astroidMigrationsDir(wrangler) {
|
|
21
|
+
if (!wrangler)
|
|
22
|
+
return DEFAULT_MIGRATIONS_DIR;
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = parseJsonc(wrangler);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return DEFAULT_MIGRATIONS_DIR;
|
|
29
|
+
}
|
|
30
|
+
const dir = (parsed.d1_databases ?? []).find((d) => d.binding === "DB")?.migrations_dir;
|
|
31
|
+
return dir ? dir.replace(/\/+$/, "") : DEFAULT_MIGRATIONS_DIR;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve each scaffold file's path against the site's migrations directory.
|
|
35
|
+
* Files that aren't migrations pass through unchanged. For a migration:
|
|
36
|
+
*
|
|
37
|
+
* - **Already there under any number:** the path is that file's, so a caller
|
|
38
|
+
* that skips existing files skips it. A site that copied `page_redirects`
|
|
39
|
+
* in by hand as `0009_page_redirects.sql` keeps that file and gets no second.
|
|
40
|
+
* - **Default number free and past the newest migration:** the default, so a
|
|
41
|
+
* standard project numbers exactly as before.
|
|
42
|
+
* - **Otherwise:** the next number after the newest migration, at the same
|
|
43
|
+
* width. A migration never takes a number the site already uses, and never
|
|
44
|
+
* sorts before one that already ran.
|
|
45
|
+
*
|
|
46
|
+
* `existing` is the file names in `migrationsDir`, in any order.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveAstroidScaffoldPaths(files, { migrationsDir, existing }) {
|
|
49
|
+
const taken = new Map();
|
|
50
|
+
const byName = new Map();
|
|
51
|
+
for (const file of existing) {
|
|
52
|
+
const match = MIGRATION_FILE.exec(file);
|
|
53
|
+
if (!match)
|
|
54
|
+
continue;
|
|
55
|
+
taken.set(Number(match[1]), file);
|
|
56
|
+
byName.set(match[2], file);
|
|
57
|
+
}
|
|
58
|
+
let newest = taken.size ? Math.max(...taken.keys()) : -1;
|
|
59
|
+
return files.map((file) => {
|
|
60
|
+
if (!file.migration)
|
|
61
|
+
return file;
|
|
62
|
+
const base = file.path.slice(file.path.lastIndexOf("/") + 1);
|
|
63
|
+
const match = MIGRATION_FILE.exec(base);
|
|
64
|
+
if (!match)
|
|
65
|
+
return { ...file, path: `${migrationsDir}/${base}` };
|
|
66
|
+
const [, digits, name] = match;
|
|
67
|
+
const present = byName.get(name);
|
|
68
|
+
if (present)
|
|
69
|
+
return { ...file, path: `${migrationsDir}/${present}` };
|
|
70
|
+
const wanted = Number(digits);
|
|
71
|
+
const number = !taken.has(wanted) && wanted > newest ? wanted : newest + 1;
|
|
72
|
+
const filename = `${String(number).padStart(digits.length, "0")}_${name}.sql`;
|
|
73
|
+
taken.set(number, filename);
|
|
74
|
+
byName.set(name, filename);
|
|
75
|
+
newest = Math.max(newest, number);
|
|
76
|
+
return { ...file, path: `${migrationsDir}/${filename}` };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -18,6 +18,12 @@ export interface ScaffoldFile {
|
|
|
18
18
|
* Without it a re-run would append a duplicate every time.
|
|
19
19
|
*/
|
|
20
20
|
marker?: string;
|
|
21
|
+
/**
|
|
22
|
+
* A D1 migration. `path` is its default, `migrations/NNNN_name.sql`; the CLI
|
|
23
|
+
* places it in the `DB` binding's `migrations_dir` and renumbers it past the
|
|
24
|
+
* site's own migrations with {@link resolveAstroidScaffoldPaths}.
|
|
25
|
+
*/
|
|
26
|
+
migration?: true;
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* `migrations/0004_page_redirects.sql`: the table that keeps a renamed page's
|
package/dist/project/scaffold.js
CHANGED
|
@@ -160,14 +160,25 @@ export function generateAstroidScaffoldFiles(config) {
|
|
|
160
160
|
// scaffolded a `products` table into src/schema.ts that no migration ever
|
|
161
161
|
// created, and the first sync wrote nothing while reporting success.
|
|
162
162
|
const catalogSql = generateCatalogMigrationSql(config);
|
|
163
|
-
if (catalogSql)
|
|
164
|
-
files.push({ path: "migrations/0003_catalog.sql", contents: catalogSql });
|
|
163
|
+
if (catalogSql) {
|
|
164
|
+
files.push({ path: "migrations/0003_catalog.sql", contents: catalogSql, migration: true });
|
|
165
|
+
}
|
|
165
166
|
// --- louise-toolkit 0.35's two schema changes -----------------------------
|
|
166
167
|
// Numbered after the catalog's 0003, and written into an existing site by
|
|
167
168
|
// `astroid generate` because a missing scaffold file is always written. The
|
|
168
169
|
// alt update is a no-op on a fresh database. Wrangler tracks migrations by
|
|
169
|
-
// filename
|
|
170
|
-
|
|
170
|
+
// filename. The CLI moves each into the site's `migrations_dir` and past
|
|
171
|
+
// the site's own numbers (see migrations.ts), so a site that already has its
|
|
172
|
+
// own 0004 gets the next free number instead of a second 0004.
|
|
173
|
+
files.push({
|
|
174
|
+
path: "migrations/0004_page_redirects.sql",
|
|
175
|
+
contents: ASTROID_PAGE_REDIRECTS_MIGRATION,
|
|
176
|
+
migration: true,
|
|
177
|
+
}, {
|
|
178
|
+
path: "migrations/0005_media_alt_undecided.sql",
|
|
179
|
+
contents: ASTROID_MEDIA_ALT_MIGRATION,
|
|
180
|
+
migration: true,
|
|
181
|
+
});
|
|
171
182
|
// --- the CWV beacon -------------------------------------------------------
|
|
172
183
|
// A static file under public/, so it is same-origin and covered by
|
|
173
184
|
// `script-src 'self'`—an inline script carrying generated content could not
|
|
@@ -69,6 +69,12 @@ export interface AstroidPagesHooks {
|
|
|
69
69
|
* with a 422 instead.
|
|
70
70
|
*/
|
|
71
71
|
export declare const ASTROID_RESERVED_SLUGS: readonly string[];
|
|
72
|
+
/**
|
|
73
|
+
* The reserved slugs for this config: {@link ASTROID_RESERVED_SLUGS}, plus the
|
|
74
|
+
* file routes a module scaffolds, minus any in `pages.allowSlugs`. A
|
|
75
|
+
* portfolio's gallery is `src/pages/work.astro`.
|
|
76
|
+
*/
|
|
77
|
+
export declare function astroidReservedSlugs(config: AstroidConfig): string[];
|
|
72
78
|
export declare function astroidPagesWriteHooks(config: AstroidConfig, site?: AstroidPagesHooks): {
|
|
73
79
|
sanitize: (html: string) => string;
|
|
74
80
|
transform: (data: Record<string, unknown>, ctx: AstroidPagesWriteContext) => Promise<Record<string, unknown>>;
|
|
@@ -47,7 +47,7 @@ const pageMediaBase = astroidMediaBase;
|
|
|
47
47
|
* The section catalog a `pages` write is validated + sanitized against: the
|
|
48
48
|
* site's own (`config.sectionCatalog`) when it registered bespoke sections, else
|
|
49
49
|
* Astroid's built-in vocabulary. This is what lets a site with its own section
|
|
50
|
-
* designs
|
|
50
|
+
* designs keep the same write contract as a stock Astroid site.
|
|
51
51
|
*/
|
|
52
52
|
function resolveSectionCatalog(config) {
|
|
53
53
|
return config.sectionCatalog ?? astroidSectionCatalog;
|
|
@@ -109,9 +109,21 @@ export const ASTROID_RESERVED_SLUGS = [
|
|
|
109
109
|
"_astro",
|
|
110
110
|
"api",
|
|
111
111
|
"cdn-cgi",
|
|
112
|
+
// The scaffold's own file routes, which every site has.
|
|
113
|
+
"contact",
|
|
114
|
+
"login",
|
|
112
115
|
"robots.txt",
|
|
113
116
|
"sitemap.xml",
|
|
114
117
|
];
|
|
118
|
+
/**
|
|
119
|
+
* The reserved slugs for this config: {@link ASTROID_RESERVED_SLUGS}, plus the
|
|
120
|
+
* file routes a module scaffolds, minus any in `pages.allowSlugs`. A
|
|
121
|
+
* portfolio's gallery is `src/pages/work.astro`.
|
|
122
|
+
*/
|
|
123
|
+
export function astroidReservedSlugs(config) {
|
|
124
|
+
const allowed = new Set(config.pages?.allowSlugs ?? []);
|
|
125
|
+
return [...ASTROID_RESERVED_SLUGS, ...(config.archetype === "portfolio" ? ["work"] : [])].filter((slug) => !allowed.has(slug));
|
|
126
|
+
}
|
|
115
127
|
export function astroidPagesWriteHooks(config, site = {}) {
|
|
116
128
|
return {
|
|
117
129
|
// `body` is a richField, so it goes through pagesRoute's own sanitize seam—with
|
|
@@ -127,7 +139,7 @@ export function astroidPagesWriteHooks(config, site = {}) {
|
|
|
127
139
|
await site.validate?.(data, ctx);
|
|
128
140
|
await assertAstroidPageSections(config, data, ctx.operation);
|
|
129
141
|
},
|
|
130
|
-
reservedSlugs: [...
|
|
142
|
+
reservedSlugs: [...astroidReservedSlugs(config), ...(site.reservedSlugs ?? [])],
|
|
131
143
|
};
|
|
132
144
|
}
|
|
133
145
|
export function astroidPagesCollection(config) {
|
package/dist/schema/framework.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (the worker route plan). */
|
|
11
11
|
export function capturesInquiries(config) {
|
|
12
12
|
// Explicit override wins—a site whose inquiry surface is a bespoke section
|
|
13
|
-
// (
|
|
13
|
+
// (a custom `contactForm`, say) can't be detected from the built-in
|
|
14
14
|
// vocabulary, so it says so directly.
|
|
15
15
|
if (typeof config.inquiries === "boolean")
|
|
16
16
|
return config.inquiries;
|
package/dist/workflow/advance.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// expected—`UPDATE … SET stage = ? WHERE id = ? AND stage = ?`—and treat "0 rows
|
|
11
11
|
// changed" as the conflict signal rather than checking first and hoping.
|
|
12
12
|
//
|
|
13
|
-
// ORDERING MATTERS, and the reference gets it wrong.
|
|
13
|
+
// ORDERING MATTERS, and the reference gets it wrong. Its floor route
|
|
14
14
|
// inserts the sign-off row and THEN runs the guarded update, so a double submit
|
|
15
15
|
// writes two audit rows even though only one advance lands. Here the guarded
|
|
16
16
|
// update goes first and the audit row is written only if it actually moved the
|
package/dist/workflow/config.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// `defineWorkflow`—staged, audited pipelines.
|
|
4
4
|
//
|
|
5
|
-
// The shape this generalizes is
|
|
5
|
+
// The shape this generalizes is a client site's production floor, and the
|
|
6
6
|
// framing correction in #256 is the important part: despite the name "order
|
|
7
7
|
// tracker", it is NOT queue- or Durable-Object-driven. It is a synchronous SSR
|
|
8
8
|
// + D1 state machine—an integer `stage` column advanced by sign-off rows,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
// `<StageBar>`—a pipeline's progress as a segmented bar.
|
|
3
3
|
//
|
|
4
|
-
// Generalized from
|
|
4
|
+
// Generalized from a client site's six-stage floor tracker, which hard-coded the
|
|
5
5
|
// segment count, the corner radii, the brand hex codes, and a mascot image.
|
|
6
6
|
// Here the stage list drives everything and the colours are theme tokens, so a
|
|
7
7
|
// four-stage onboarding flow and an eight-stage production line both work and
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// So the whole page is resolved in ONE bounded `IN (...)` lookup before anything
|
|
11
11
|
// renders, and the result is threaded down as `mediaMeta`. This is the pattern
|
|
12
|
-
//
|
|
12
|
+
// a client site's `Sections.astro` arrived at independently, generalized.
|
|
13
13
|
//
|
|
14
14
|
// The collection step is SCHEMA-DRIVEN: it walks the catalog looking for fields
|
|
15
15
|
// of `type: "image"` rather than hardcoding field names. That's what keeps it
|