astroidjs 0.18.0 → 0.20.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 +51 -11
- 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/commerce/secrets.d.ts +7 -0
- package/dist/commerce/secrets.js +12 -3
- package/dist/config.d.ts +85 -16
- package/dist/config.js +111 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/portal/guard.js +1 -1
- package/dist/project/generate.js +96 -52
- 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 +33 -11
- package/dist/queues/index.d.ts +1 -1
- package/dist/queues/index.js +1 -1
- package/dist/queues/messages.d.ts +6 -0
- package/dist/queues/messages.js +17 -2
- package/dist/queues/scaffold.js +5 -1
- package/dist/schema/collections.d.ts +6 -0
- package/dist/schema/collections.js +14 -2
- package/dist/schema/framework.js +1 -1
- package/dist/schema/generate.js +48 -4
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/rate-rules.d.ts +9 -2
- package/dist/security/rate-rules.js +46 -22
- package/dist/seo/routes.js +3 -2
- package/dist/shape.d.ts +6 -0
- package/dist/shape.js +14 -0
- package/dist/status.js +22 -10
- package/dist/worker/generate.js +142 -10
- package/dist/workflow/advance.js +1 -1
- package/dist/workflow/config.js +1 -1
- package/package.json +3 -3
- package/src/components/Credit.astro +70 -0
- 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") {
|
|
@@ -156,6 +172,7 @@ async function cmdDoctor(cwd, flags) {
|
|
|
156
172
|
generateAstroidScaffoldFiles,
|
|
157
173
|
astroidUsesQueues,
|
|
158
174
|
astroidCrons,
|
|
175
|
+
astroidHasEditor,
|
|
159
176
|
checkWranglerPreviews,
|
|
160
177
|
astroidRunsMigrations,
|
|
161
178
|
migrationsOwnershipError,
|
|
@@ -196,7 +213,7 @@ async function cmdDoctor(cwd, flags) {
|
|
|
196
213
|
// that names a module whose seam was never written produces a project that
|
|
197
214
|
// cannot resolve its own imports. This is precisely the state that used to
|
|
198
215
|
// report "healthy" with two warnings and exit 0.
|
|
199
|
-
for (const file of generateAstroidScaffoldFiles(config)) {
|
|
216
|
+
for (const file of await scaffoldFilesFor(cwd, generateAstroidScaffoldFiles(config))) {
|
|
200
217
|
if (file.apply === "append-once") continue; // accumulated, not owned—see below
|
|
201
218
|
if (existsSync(join(cwd, file.path))) ok(`${file.path} present`);
|
|
202
219
|
else err(`${file.path} is missing (required by your config) — run \`astroid generate\`.`);
|
|
@@ -219,9 +236,15 @@ async function cmdDoctor(cwd, flags) {
|
|
|
219
236
|
//
|
|
220
237
|
// So check every binding the GENERATED code actually dereferences, not just
|
|
221
238
|
// the three the baseline happens to have.
|
|
239
|
+
// An app with no editor (`editor: false`) uses none of the editor's
|
|
240
|
+
// bindings, so it isn't held to them: no draft buffer, no media bucket, and
|
|
241
|
+
// no mail unless its portal sends password resets.
|
|
242
|
+
const editor = astroidHasEditor(config);
|
|
222
243
|
const requiredBindings = [
|
|
223
244
|
{ name: "RL", what: "KV namespace", why: "the rate limiter in src/middleware.ts" },
|
|
224
|
-
|
|
245
|
+
...(editor
|
|
246
|
+
? [{ name: "DRAFTS", what: "KV namespace", why: "the autosave draft buffer" }]
|
|
247
|
+
: []),
|
|
225
248
|
...(astroidUsesQueues(config)
|
|
226
249
|
? [
|
|
227
250
|
{
|
|
@@ -247,17 +270,24 @@ async function cmdDoctor(cwd, flags) {
|
|
|
247
270
|
// it: the magic link is console-logged in dev and EMAILED in production, so
|
|
248
271
|
// a missing binding is a site nobody can sign in to—and it fails only once
|
|
249
272
|
// deployed, which is the one place nothing in this repo exercises.
|
|
250
|
-
|
|
251
|
-
|
|
273
|
+
// An app with no editor and no portal signs nobody in, so it sends no mail.
|
|
274
|
+
const sendsMail = editor || Boolean(config.portal?.enabled);
|
|
275
|
+
if (sendsMail && /"send_email"\s*:/.test(w))
|
|
276
|
+
ok("wrangler: Email Sending `EMAIL` binding present");
|
|
277
|
+
else if (sendsMail)
|
|
252
278
|
err(
|
|
253
|
-
|
|
254
|
-
|
|
279
|
+
editor
|
|
280
|
+
? "wrangler.jsonc has no `send_email` binding, but production sign-in emails the " +
|
|
281
|
+
'magic link (it is only console-logged in dev). Add: "send_email": [{ "name": "EMAIL" }]'
|
|
282
|
+
: "wrangler.jsonc has no `send_email` binding, but the portal emails password resets " +
|
|
283
|
+
'in production (they are only console-logged in dev). Add: "send_email": [{ "name": "EMAIL" }]',
|
|
255
284
|
);
|
|
256
285
|
|
|
257
286
|
if (hasBinding("DB")) ok("wrangler: D1 `DB` binding present");
|
|
258
287
|
else err("wrangler.jsonc has no D1 `DB` binding.");
|
|
259
|
-
|
|
260
|
-
|
|
288
|
+
// An app with no editor has no media library, so no bucket to hold it.
|
|
289
|
+
if (editor && hasBinding("MEDIA")) ok("wrangler: R2 `MEDIA` binding present");
|
|
290
|
+
else if (editor) err("wrangler.jsonc has no R2 `MEDIA` binding.");
|
|
261
291
|
if (/"main"\s*:\s*"src\/worker\.ts"/.test(w)) ok("wrangler: `main` → src/worker.ts");
|
|
262
292
|
else warn("wrangler.jsonc `main` does not point at src/worker.ts.");
|
|
263
293
|
|
|
@@ -271,7 +301,17 @@ async function cmdDoctor(cwd, flags) {
|
|
|
271
301
|
// JSON.parse (comments/trailing commas), matching the binding checks above.
|
|
272
302
|
const expectedCrons = astroidCrons(config);
|
|
273
303
|
const cronsMatch = w.match(/"crons"\s*:\s*\[([^\]]*)\]/);
|
|
274
|
-
|
|
304
|
+
const declaredAny = cronsMatch && /"[^"]+"/.test(cronsMatch[1]);
|
|
305
|
+
if (expectedCrons.length === 0) {
|
|
306
|
+
// Nothing scheduled, so the generated worker has no `scheduled` handler,
|
|
307
|
+
// and a declared trigger would fail every time it fired.
|
|
308
|
+
if (declaredAny)
|
|
309
|
+
warn(
|
|
310
|
+
"wrangler.jsonc declares `triggers.crons`, but nothing in your config is scheduled, " +
|
|
311
|
+
"so the generated worker has no `scheduled` handler for them. Remove the triggers.",
|
|
312
|
+
);
|
|
313
|
+
else ok("wrangler: no crons, and nothing is scheduled");
|
|
314
|
+
} else if (!cronsMatch) {
|
|
275
315
|
err(
|
|
276
316
|
"wrangler.jsonc has no `triggers.crons`, but the generated `scheduled` handler " +
|
|
277
317
|
`dispatches on ${expectedCrons.map((c) => `"${c}"`).join(", ")}. ` +
|
|
@@ -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.
|
|
@@ -50,6 +50,13 @@ export declare function commerceProviderCredentials(provider: CommerceProvider,
|
|
|
50
50
|
* `.dev.vars` would be a bug rather than a redundancy.
|
|
51
51
|
*/
|
|
52
52
|
export declare function commerceSecretNames(commerce: CommerceConfig | undefined): string[];
|
|
53
|
+
/**
|
|
54
|
+
* The webhook signing secret a provider needs, or none when the project runs no
|
|
55
|
+
* pipeline (`commerce.pipeline: false`). With no receiver there is nothing to
|
|
56
|
+
* verify, so requiring the secret would hold checkout dormant for a value no
|
|
57
|
+
* code reads.
|
|
58
|
+
*/
|
|
59
|
+
export declare function commerceProviderWebhookSecrets(provider: CommerceProvider, commerce: CommerceConfig | undefined): readonly string[];
|
|
53
60
|
/** One provider's resolved gate. */
|
|
54
61
|
export interface ProviderStatus {
|
|
55
62
|
provider: CommerceProvider;
|
package/dist/commerce/secrets.js
CHANGED
|
@@ -101,19 +101,28 @@ export function commerceProviderCredentials(provider, commerce) {
|
|
|
101
101
|
export function commerceSecretNames(commerce) {
|
|
102
102
|
const names = astroidCommerceProviders(commerce).flatMap((provider) => [
|
|
103
103
|
...commerceProviderCredentials(provider, commerce),
|
|
104
|
-
|
|
104
|
+
...commerceProviderWebhookSecrets(provider, commerce),
|
|
105
105
|
]);
|
|
106
106
|
return [...new Set(names)];
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The webhook signing secret a provider needs, or none when the project runs no
|
|
110
|
+
* pipeline (`commerce.pipeline: false`). With no receiver there is nothing to
|
|
111
|
+
* verify, so requiring the secret would hold checkout dormant for a value no
|
|
112
|
+
* code reads.
|
|
113
|
+
*/
|
|
114
|
+
export function commerceProviderWebhookSecrets(provider, commerce) {
|
|
115
|
+
return commerce?.pipeline === false ? [] : [COMMERCE_PROVIDER_SECRETS[provider].webhook];
|
|
116
|
+
}
|
|
108
117
|
/** Read one provider's secrets off an env-shaped record. */
|
|
109
118
|
async function resolveProvider(provider, roles, env, commerce) {
|
|
110
|
-
const spec = COMMERCE_PROVIDER_SECRETS[provider];
|
|
111
119
|
const pick = (names) => Object.fromEntries(names.map((n) => [n, env[n]]));
|
|
112
120
|
const [credentials, webhook] = await Promise.all([
|
|
113
121
|
// Config-aware: a multi-location project must not be held dormant waiting
|
|
114
122
|
// for a SQUARE_LOCATION_ID it will never legitimately have.
|
|
115
123
|
resolveModuleSecrets(pick(commerceProviderCredentials(provider, commerce))),
|
|
116
|
-
|
|
124
|
+
// Empty without a pipeline, which resolves as configured: nothing to verify.
|
|
125
|
+
resolveModuleSecrets(pick(commerceProviderWebhookSecrets(provider, commerce))),
|
|
117
126
|
]);
|
|
118
127
|
return {
|
|
119
128
|
provider,
|
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.
|
|
@@ -174,6 +175,18 @@ export interface CommerceConfig {
|
|
|
174
175
|
catalog?: CatalogMirrorConfig;
|
|
175
176
|
/** Square-specific options. Only meaningful when Square fills some role. */
|
|
176
177
|
square?: SquareCommerceConfig;
|
|
178
|
+
/**
|
|
179
|
+
* Whether this project runs the commerce pipeline: the webhook receivers, the
|
|
180
|
+
* queue consumer that processes them, and the hourly catalog re-sync. Default
|
|
181
|
+
* `true`.
|
|
182
|
+
*
|
|
183
|
+
* Set `false` for a project that only takes payments while another project,
|
|
184
|
+
* or another Worker in the same repository, runs the pipeline against the
|
|
185
|
+
* same account. It keeps what a checkout needs, the provider's CSP origins,
|
|
186
|
+
* the checkout rate rule, and the checkout route, and drops the rest, along
|
|
187
|
+
* with the webhook signing secret nothing would verify.
|
|
188
|
+
*/
|
|
189
|
+
pipeline?: boolean;
|
|
177
190
|
}
|
|
178
191
|
export interface SquareCommerceConfig {
|
|
179
192
|
/**
|
|
@@ -194,8 +207,9 @@ export interface SquareCommerceConfig {
|
|
|
194
207
|
export interface QueuesConfig {
|
|
195
208
|
/**
|
|
196
209
|
* Force the queue consumer + cron on or off. Defaults to on whenever
|
|
197
|
-
* `commerce` is configured: a commerce provider means
|
|
198
|
-
* you process inline is a webhook you drop when the
|
|
210
|
+
* `commerce` is configured with its pipeline: a commerce provider means
|
|
211
|
+
* webhooks, and a webhook you process inline is a webhook you drop when the
|
|
212
|
+
* provider times out.
|
|
199
213
|
*/
|
|
200
214
|
enabled?: boolean;
|
|
201
215
|
/**
|
|
@@ -323,9 +337,9 @@ export interface TenancyConfig {
|
|
|
323
337
|
* sign-in) instead of JSON, and every data load on that host silently fails
|
|
324
338
|
* while the same code works on the apex.
|
|
325
339
|
*
|
|
326
|
-
* That is not hypothetical—it is why this default exists (found on
|
|
327
|
-
*
|
|
328
|
-
*
|
|
340
|
+
* That is not hypothetical—it is why this default exists (found on a client
|
|
341
|
+
* site's studio host, where the whole admin app loaded and then fetched
|
|
342
|
+
* nothing).
|
|
329
343
|
*
|
|
330
344
|
* Set `[]` to rewrite everything, or add prefixes for other host-agnostic
|
|
331
345
|
* surfaces (`/_actions`, `/webhooks`). Matching is prefix-based on a path
|
|
@@ -407,8 +421,8 @@ export interface SettingsConfig {
|
|
|
407
421
|
* on top of (or, with `columns: []`, instead of) Astroid's base columns. The
|
|
408
422
|
* generated `settingsRoute` + Action accept these; the Settings panel writes
|
|
409
423
|
* them through the `settingsExtension` groups a site supplies to
|
|
410
|
-
* `mountSettings`. A site with a rich settings shape (
|
|
411
|
-
*
|
|
424
|
+
* `mountSettings`. A site with a rich settings shape (footer columns, an hours
|
|
425
|
+
* table, UI strings, shop/order config) lists their top-level keys here.
|
|
412
426
|
*/
|
|
413
427
|
customKeys?: string[];
|
|
414
428
|
/** Extra media-library image keys beyond the base logo/favicon/OG defaults—*
|
|
@@ -456,7 +470,22 @@ export interface PagesConfig {
|
|
|
456
470
|
* clamping a title, or filling a new page's defaults.
|
|
457
471
|
*/
|
|
458
472
|
hooks?: boolean;
|
|
473
|
+
/**
|
|
474
|
+
* Reserved slugs this site serves as pages. `contact` and `login` are
|
|
475
|
+
* reserved because a new scaffold has file routes there, and `work` because
|
|
476
|
+
* a portfolio has its gallery there. A site without that file, whose page at
|
|
477
|
+
* the path comes from its own catch-all route, lists the slug here so the
|
|
478
|
+
* Pages route accepts it. Only those three can be allowed
|
|
479
|
+
* ({@link ASTROID_SCAFFOLD_ROUTE_SLUGS}): the platform serves the other
|
|
480
|
+
* reserved slugs, such as `api` and `sitemap.xml`, before any page.
|
|
481
|
+
*/
|
|
482
|
+
allowSlugs?: string[];
|
|
459
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* The reserved slugs that come from a scaffolded file route rather than the
|
|
486
|
+
* platform, so a site without that file can allow them with `pages.allowSlugs`.
|
|
487
|
+
*/
|
|
488
|
+
export declare const ASTROID_SCAFFOLD_ROUTE_SLUGS: readonly string[];
|
|
460
489
|
export interface StatusConfig {
|
|
461
490
|
/**
|
|
462
491
|
* Add the site's own checks to the public status route, from the
|
|
@@ -481,10 +510,32 @@ export interface DeployConfig {
|
|
|
481
510
|
*/
|
|
482
511
|
migrations?: boolean;
|
|
483
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* A small "Site by …" line the footer renders for the agency that built the
|
|
515
|
+
* site. A site fact, so it has no default: omit it and nothing renders.
|
|
516
|
+
*/
|
|
517
|
+
export interface CreditConfig {
|
|
518
|
+
/** Who built the site, for example `"Example Organization"`. */
|
|
519
|
+
name: string;
|
|
520
|
+
/** Where the credit links, as an absolute `http` or `https` URL. */
|
|
521
|
+
href: string;
|
|
522
|
+
/**
|
|
523
|
+
* An optional mark shown before the name: a root-relative path (for example
|
|
524
|
+
* `"/credit-mark.svg"`) or an `https:` URL. It's drawn as a mask filled with
|
|
525
|
+
* the text color, so one single-color SVG works on every theme. Only its
|
|
526
|
+
* shape counts; its own fill is ignored.
|
|
527
|
+
*/
|
|
528
|
+
logo?: string;
|
|
529
|
+
/** The link's `rel`, for example `"noopener"` or `"noopener nofollow"`. The
|
|
530
|
+
* site decides; omitted, the link carries none. */
|
|
531
|
+
rel?: string;
|
|
532
|
+
/** The words before the name. Default `"Site by"`. */
|
|
533
|
+
label?: string;
|
|
534
|
+
}
|
|
484
535
|
export interface AstroidConfig {
|
|
485
536
|
/**
|
|
486
537
|
* Stable project slug—the worker/D1/R2 base name and default subdomain (for example,
|
|
487
|
-
* `"
|
|
538
|
+
* `"example"`). Required and non-empty; it drives the generated binding names.
|
|
488
539
|
*/
|
|
489
540
|
key: string;
|
|
490
541
|
/** Hostnames this site serves (prod + preview), for custom-domain routes. */
|
|
@@ -497,6 +548,21 @@ export interface AstroidConfig {
|
|
|
497
548
|
tenancy?: TenancyConfig;
|
|
498
549
|
/** Starting shape; sets section/module/nav defaults the site can override. */
|
|
499
550
|
archetype: Archetype;
|
|
551
|
+
/**
|
|
552
|
+
* Whether the project has a Louise editor. Default `true`.
|
|
553
|
+
*
|
|
554
|
+
* Set `false` for an app with no pages to edit, such as an order app whose
|
|
555
|
+
* menu comes from a commerce provider and whose few settings another site's
|
|
556
|
+
* editor owns. The generated worker and middleware then carry no editor
|
|
557
|
+
* routes and no sign-in, the schema carries no content tables, and
|
|
558
|
+
* `wrangler.jsonc` binds no draft buffer, media bucket, or AI. What stays is
|
|
559
|
+
* the rate limiter, the CSP, the security headers, the public status route,
|
|
560
|
+
* and the `portal`, `pwa`, `commerce`, and `tenancy` modules.
|
|
561
|
+
*
|
|
562
|
+
* `defineAstroid` refuses every option that configures the editor
|
|
563
|
+
* alongside it, such as `sections` or `media`, rather than ignore it.
|
|
564
|
+
*/
|
|
565
|
+
editor?: boolean;
|
|
500
566
|
/** The single brand's theme (display name + color tokens + font). */
|
|
501
567
|
theme: Theme;
|
|
502
568
|
/** The editable home page, top to bottom. Omit to take the archetype default. */
|
|
@@ -505,7 +571,7 @@ export interface AstroidConfig {
|
|
|
505
571
|
* A site-provided section catalog that REPLACES the built-in one for
|
|
506
572
|
* SERVER-side validation + sanitization of `pages.sections` (the generated
|
|
507
573
|
* pages route + versions route). A site with bespoke section designs—its own
|
|
508
|
-
* `.astro` components and field defs (
|
|
574
|
+
* `.astro` components and field defs (a dozen or more sections)—registers them
|
|
509
575
|
* here so writes to its custom `_type`s validate instead of 422-ing against the
|
|
510
576
|
* built-in vocabulary. The on-canvas editor already uses the site's catalog
|
|
511
577
|
* (its `mountSections` call passes it); this closes the server half so both
|
|
@@ -561,11 +627,14 @@ export interface AstroidConfig {
|
|
|
561
627
|
* Force the contact form + `inquiries` table on or off. Omit to detect from
|
|
562
628
|
* the config (a `contact` section, or a wholesale-inquiry module). Set `true`
|
|
563
629
|
* when a bespoke section captures inquiries under a name Astroid can't see
|
|
564
|
-
* (
|
|
630
|
+
* (for example, a custom `contactForm` section); set `false` to suppress it
|
|
631
|
+
* entirely.
|
|
565
632
|
*/
|
|
566
633
|
inquiries?: boolean;
|
|
567
634
|
/** Installable-app settings. Only read when `modules` includes `"pwa"`. */
|
|
568
635
|
pwa?: PwaConfig;
|
|
636
|
+
/** The agency credit `<Credit>` renders in the footer. Omit for none. */
|
|
637
|
+
credit?: CreditConfig;
|
|
569
638
|
deploy?: DeployConfig;
|
|
570
639
|
}
|
|
571
640
|
export declare function defineAstroid(config: AstroidConfig): AstroidConfig;
|
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";
|
|
@@ -34,6 +34,7 @@ import { AstroidConfigError } from "./errors.js";
|
|
|
34
34
|
// type-only, so the cycle erases at build and nothing circular exists at runtime.
|
|
35
35
|
// It is also dependency-free, so `create-astroid`'s graph is unchanged.
|
|
36
36
|
import { ASTROID_HEALTH_CRON, astroidCron, astroidUsesQueues } from "./queues/messages.js";
|
|
37
|
+
import { astroidHasEditor } from "./shape.js";
|
|
37
38
|
/**
|
|
38
39
|
* Each archetype's default home-page sections.
|
|
39
40
|
*
|
|
@@ -55,6 +56,11 @@ export const ASTROID_ARCHETYPE_SECTIONS = {
|
|
|
55
56
|
wholesale: ["hero", "featureGrid", "aboutIntro", "contact"],
|
|
56
57
|
portfolio: ["hero", "gallery", "aboutIntro", "contact"],
|
|
57
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* The reserved slugs that come from a scaffolded file route rather than the
|
|
61
|
+
* platform, so a site without that file can allow them with `pages.allowSlugs`.
|
|
62
|
+
*/
|
|
63
|
+
export const ASTROID_SCAFFOLD_ROUTE_SLUGS = ["contact", "login", "work"];
|
|
58
64
|
/**
|
|
59
65
|
* Define an Astroid project. An identity function in the shape of Astro's
|
|
60
66
|
* `defineConfig`: it returns the config verbatim with full type-checking +
|
|
@@ -64,9 +70,9 @@ export const ASTROID_ARCHETYPE_SECTIONS = {
|
|
|
64
70
|
*
|
|
65
71
|
* ```ts
|
|
66
72
|
* export default defineAstroid({
|
|
67
|
-
* key: "
|
|
73
|
+
* key: "example",
|
|
68
74
|
* archetype: "storefront",
|
|
69
|
-
* theme: { name: "
|
|
75
|
+
* theme: { name: "Example Organization", colors: { brand: "#5b4bff" } },
|
|
70
76
|
* sections: ["hero", "banner", "productGrid", "locationHours", "contact"],
|
|
71
77
|
* commerce: { provider: "square" },
|
|
72
78
|
* deploy: { platform: "cloudflare" },
|
|
@@ -83,6 +89,13 @@ export const ASTROID_ARCHETYPE_SECTIONS = {
|
|
|
83
89
|
* the unreachable-trigger failure `config.crons` exists to prevent.
|
|
84
90
|
*/
|
|
85
91
|
function assertCrons(config) {
|
|
92
|
+
// `queues.cron` schedules the catalog re-sync, which belongs to the pipeline.
|
|
93
|
+
// Without one it would be accepted and never scheduled.
|
|
94
|
+
if (config.commerce?.pipeline === false && typeof config.queues?.cron === "string") {
|
|
95
|
+
throw new AstroidConfigError("`queues.cron` schedules the catalog re-sync, which `commerce.pipeline: false` " +
|
|
96
|
+
"leaves to the project that runs the pipeline. Remove `queues.cron`, or use " +
|
|
97
|
+
"`crons` for a job of this project's own.");
|
|
98
|
+
}
|
|
86
99
|
const crons = config.crons ?? [];
|
|
87
100
|
if (crons.length === 0)
|
|
88
101
|
return;
|
|
@@ -91,7 +104,7 @@ function assertCrons(config) {
|
|
|
91
104
|
"without it the generated handler would `send` to a binding this project never creates. " +
|
|
92
105
|
"Set `queues: { enabled: true }`, or drop the crons.");
|
|
93
106
|
}
|
|
94
|
-
const seen = new Map([[ASTROID_HEALTH_CRON, "the daily health scan"]]);
|
|
107
|
+
const seen = new Map(astroidHasEditor(config) ? [[ASTROID_HEALTH_CRON, "the daily health scan"]] : []);
|
|
95
108
|
const catalog = astroidCron(config);
|
|
96
109
|
if (catalog)
|
|
97
110
|
seen.set(catalog, "the catalog re-sync (`queues.cron`)");
|
|
@@ -109,6 +122,20 @@ function assertCrons(config) {
|
|
|
109
122
|
seen.set(expression, "another entry in `crons`");
|
|
110
123
|
}
|
|
111
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* `pages.allowSlugs` may only name a slug reserved for a scaffolded file route.
|
|
127
|
+
* Allowing `api` or `sitemap.xml` would let an editor save a page nobody can
|
|
128
|
+
* reach, which is the silent failure the reserved list exists to prevent.
|
|
129
|
+
*/
|
|
130
|
+
function assertAllowSlugs(config) {
|
|
131
|
+
for (const slug of config.pages?.allowSlugs ?? []) {
|
|
132
|
+
if (!ASTROID_SCAFFOLD_ROUTE_SLUGS.includes(slug)) {
|
|
133
|
+
throw new AstroidConfigError(`\`pages.allowSlugs\` can't allow "${slug}". Only the slugs reserved for a scaffolded ` +
|
|
134
|
+
`file route can be allowed (${ASTROID_SCAFFOLD_ROUTE_SLUGS.join(", ")}); the ` +
|
|
135
|
+
"platform serves the others before any page.");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
112
139
|
/**
|
|
113
140
|
* The tenancy misconfigurations that fail late, or not at all.
|
|
114
141
|
*
|
|
@@ -177,6 +204,79 @@ function assertMediaConfig(media) {
|
|
|
177
204
|
"would fail with an error the media route never sees.");
|
|
178
205
|
}
|
|
179
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* A credit that would render as a broken link or an empty mark.
|
|
209
|
+
*
|
|
210
|
+
* The logo is limited to a root-relative path or `https:` because it lands in a
|
|
211
|
+
* CSS `url()`: a `data:` or `javascript:` value there is at best unrenderable,
|
|
212
|
+
* and a relative one resolves against each page's path rather than the site.
|
|
213
|
+
*/
|
|
214
|
+
function assertCredit(credit) {
|
|
215
|
+
if (!credit)
|
|
216
|
+
return;
|
|
217
|
+
if (!credit.name?.trim()) {
|
|
218
|
+
throw new AstroidConfigError("`credit.name` is required: the name the footer credits");
|
|
219
|
+
}
|
|
220
|
+
let href;
|
|
221
|
+
try {
|
|
222
|
+
href = new URL(credit.href);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Reported below with the value that failed.
|
|
226
|
+
}
|
|
227
|
+
if (!href || (href.protocol !== "https:" && href.protocol !== "http:")) {
|
|
228
|
+
throw new AstroidConfigError(`\`credit.href\` must be an absolute http or https URL, such as "https://example.com", ` +
|
|
229
|
+
`but it's "${credit.href}"`);
|
|
230
|
+
}
|
|
231
|
+
const logo = credit.logo;
|
|
232
|
+
if (logo !== undefined &&
|
|
233
|
+
!(logo.startsWith("/") && !logo.startsWith("//")) &&
|
|
234
|
+
!logo.startsWith("https://")) {
|
|
235
|
+
throw new AstroidConfigError(`\`credit.logo\` must be a root-relative path such as "/credit-mark.svg" or an ` +
|
|
236
|
+
`https URL, but it's "${logo}"`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The options an app with no editor can't honor. Each configures the editor, a
|
|
241
|
+
* table it edits, or a surface only an editor reviews, so accepting one would
|
|
242
|
+
* be accepting a setting nothing reads.
|
|
243
|
+
*/
|
|
244
|
+
const EDITOR_ONLY_OPTIONS = [
|
|
245
|
+
["sections", "the editable home page"],
|
|
246
|
+
["sectionCatalog", "the page editor's sections"],
|
|
247
|
+
["blockCatalog", "the page editor's blocks"],
|
|
248
|
+
["media", "the media library"],
|
|
249
|
+
["pages", "the editable pages"],
|
|
250
|
+
["settings", "the Settings panel"],
|
|
251
|
+
];
|
|
252
|
+
/** Modules that only work with an editor, and why. */
|
|
253
|
+
const EDITOR_ONLY_MODULES = {
|
|
254
|
+
realtime: "it syncs editors editing one page",
|
|
255
|
+
wholesaleInquiry: "its inquiries are reviewed in the editor",
|
|
256
|
+
};
|
|
257
|
+
function assertEditorFree(config) {
|
|
258
|
+
if (astroidHasEditor(config))
|
|
259
|
+
return;
|
|
260
|
+
const without = "An app with `editor: false` has no editor";
|
|
261
|
+
for (const [key, what] of EDITOR_ONLY_OPTIONS) {
|
|
262
|
+
if (config[key] !== undefined) {
|
|
263
|
+
throw new AstroidConfigError(`${without}, so \`${key}\` (${what}) would do nothing. Remove it, or drop \`editor: false\`.`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (config.inquiries === true) {
|
|
267
|
+
throw new AstroidConfigError(`${without} to review inquiries in, so \`inquiries: true\` would collect messages ` +
|
|
268
|
+
"nobody reads. Remove it, or drop `editor: false`.");
|
|
269
|
+
}
|
|
270
|
+
for (const module of [...(config.modules ?? []), ...(config.portal?.features ?? [])]) {
|
|
271
|
+
const why = EDITOR_ONLY_MODULES[module];
|
|
272
|
+
if (why) {
|
|
273
|
+
throw new AstroidConfigError(`${without}, so the \`${module}\` module can't work: ${why}. Remove it, or drop \`editor: false\`.`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (config.deploy?.mediaBase !== undefined) {
|
|
277
|
+
throw new AstroidConfigError(`${without} and no media library, so \`deploy.mediaBase\` would do nothing. Remove it.`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
180
280
|
export function defineAstroid(config) {
|
|
181
281
|
if (!config.key || config.key.trim().length === 0) {
|
|
182
282
|
throw new AstroidConfigError("Astroid config requires a non-empty `key` (it names the generated worker/D1/R2 bindings)");
|
|
@@ -211,6 +311,9 @@ export function defineAstroid(config) {
|
|
|
211
311
|
// an answer. Fail loudly, at config load, naming the workaround.
|
|
212
312
|
assertCrons(config);
|
|
213
313
|
assertTenancy(config);
|
|
314
|
+
assertAllowSlugs(config);
|
|
315
|
+
assertEditorFree(config);
|
|
316
|
+
assertCredit(config.credit);
|
|
214
317
|
if (config.portal?.gated) {
|
|
215
318
|
throw new AstroidConfigError("`portal.gated` is not implemented: it is accepted but wires no guard, so the site " +
|
|
216
319
|
"would be fully public while appearing gated. Remove it, and gate the whole site by " +
|