astroidjs 0.1.2 → 0.3.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 +240 -5
- package/bin/astroid.mjs +185 -9
- package/dist/analytics/index.d.ts +37 -0
- package/dist/analytics/index.js +108 -0
- package/dist/astro/csp.d.ts +64 -0
- package/dist/astro/csp.js +173 -0
- package/dist/astro/index.d.ts +1 -0
- package/dist/astro/index.js +7 -0
- package/dist/auth/index.d.ts +27 -0
- package/dist/auth/index.js +59 -0
- package/dist/commerce/adapters.d.ts +60 -0
- package/dist/commerce/adapters.js +90 -0
- package/dist/commerce/checkout-scaffold.d.ts +42 -0
- package/dist/commerce/checkout-scaffold.js +306 -0
- package/dist/commerce/checkout.d.ts +72 -0
- package/dist/commerce/checkout.js +124 -0
- package/dist/commerce/index.d.ts +8 -0
- package/dist/commerce/index.js +9 -0
- package/dist/commerce/loader.d.ts +71 -0
- package/dist/commerce/loader.js +90 -0
- package/dist/commerce/mirror.d.ts +69 -0
- package/dist/commerce/mirror.js +214 -0
- package/dist/commerce/roles.d.ts +38 -0
- package/dist/commerce/roles.js +93 -0
- package/dist/commerce/secrets.d.ts +74 -0
- package/dist/commerce/secrets.js +129 -0
- package/dist/commerce/sync.d.ts +86 -0
- package/dist/commerce/sync.js +154 -0
- package/dist/components/sections.d.ts +577 -0
- package/dist/components/sections.js +425 -0
- package/dist/config.d.ts +239 -12
- package/dist/config.js +49 -1
- package/dist/email/index.d.ts +4 -0
- package/dist/email/index.js +5 -0
- package/dist/email/inquiry.d.ts +33 -0
- package/dist/email/inquiry.js +63 -0
- package/dist/email/send.d.ts +120 -0
- package/dist/email/send.js +196 -0
- package/dist/email/templates.d.ts +24 -0
- package/dist/email/templates.js +184 -0
- package/dist/email/theme.d.ts +24 -0
- package/dist/email/theme.js +150 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/map/index.d.ts +3 -0
- package/dist/map/index.js +4 -0
- package/dist/map/pmtiles.d.ts +92 -0
- package/dist/map/pmtiles.js +130 -0
- package/dist/map/scaffold.d.ts +29 -0
- package/dist/map/scaffold.js +212 -0
- package/dist/map/style.d.ts +58 -0
- package/dist/map/style.js +154 -0
- package/dist/portal/config.d.ts +26 -0
- package/dist/portal/config.js +56 -0
- package/dist/portal/guard.d.ts +54 -0
- package/dist/portal/guard.js +64 -0
- package/dist/portal/index.d.ts +5 -0
- package/dist/portal/index.js +6 -0
- package/dist/portal/nav.d.ts +26 -0
- package/dist/portal/nav.js +35 -0
- package/dist/portal/scaffold.d.ts +28 -0
- package/dist/portal/scaffold.js +140 -0
- package/dist/portal/session.d.ts +36 -0
- package/dist/portal/session.js +86 -0
- package/dist/portfolio/index.d.ts +1 -0
- package/dist/portfolio/index.js +4 -0
- package/dist/portfolio/scaffold.d.ts +9 -0
- package/dist/portfolio/scaffold.js +93 -0
- package/dist/project/actions.d.ts +3 -0
- package/dist/project/actions.js +121 -0
- package/dist/project/generate.d.ts +15 -0
- package/dist/project/generate.js +144 -2
- package/dist/project/index.d.ts +2 -0
- package/dist/project/index.js +2 -0
- package/dist/project/scaffold.d.ts +29 -0
- package/dist/project/scaffold.js +166 -0
- package/dist/pwa/generate.d.ts +49 -0
- package/dist/pwa/generate.js +218 -0
- package/dist/pwa/index.d.ts +1 -0
- package/dist/pwa/index.js +2 -0
- package/dist/queues/consumer.d.ts +29 -0
- package/dist/queues/consumer.js +37 -0
- package/dist/queues/index.d.ts +4 -0
- package/dist/queues/index.js +5 -0
- package/dist/queues/messages.d.ts +60 -0
- package/dist/queues/messages.js +71 -0
- package/dist/queues/scaffold.d.ts +44 -0
- package/dist/queues/scaffold.js +204 -0
- package/dist/queues/webhook.d.ts +60 -0
- package/dist/queues/webhook.js +81 -0
- package/dist/realtime/index.d.ts +1 -0
- package/dist/realtime/index.js +4 -0
- package/dist/realtime/scaffold.d.ts +30 -0
- package/dist/realtime/scaffold.js +159 -0
- package/dist/schema/collections.d.ts +41 -7
- package/dist/schema/collections.js +110 -12
- package/dist/schema/framework.js +5 -0
- package/dist/schema/generate.js +17 -1
- package/dist/secrets.d.ts +54 -0
- package/dist/secrets.js +80 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +2 -0
- package/dist/security/rate-rules.d.ts +21 -0
- package/dist/security/rate-rules.js +110 -0
- package/dist/seo/index.d.ts +3 -0
- package/dist/seo/index.js +4 -0
- package/dist/seo/resolve.d.ts +68 -0
- package/dist/seo/resolve.js +73 -0
- package/dist/seo/routes.d.ts +44 -0
- package/dist/seo/routes.js +104 -0
- package/dist/seo/structured-data.d.ts +51 -0
- package/dist/seo/structured-data.js +105 -0
- package/dist/status.d.ts +51 -0
- package/dist/status.js +113 -0
- package/dist/worker/generate.d.ts +18 -10
- package/dist/worker/generate.js +353 -42
- package/dist/worker/routes.d.ts +1 -1
- package/dist/worker/routes.js +42 -0
- package/dist/workflow/advance.d.ts +102 -0
- package/dist/workflow/advance.js +145 -0
- package/dist/workflow/config.d.ts +60 -0
- package/dist/workflow/config.js +73 -0
- package/dist/workflow/generate.d.ts +22 -0
- package/dist/workflow/generate.js +138 -0
- package/dist/workflow/index.d.ts +3 -0
- package/dist/workflow/index.js +4 -0
- package/package.json +21 -4
- package/src/components/Editable.astro +33 -9
- package/src/components/JustifiedGallery.astro +254 -0
- package/src/components/MediaSlot.astro +178 -0
- package/src/components/PortalShell.astro +80 -0
- package/src/components/RegisterSW.astro +45 -0
- package/src/components/Section.astro +101 -35
- package/src/components/Sections.astro +64 -0
- package/src/components/Seo.astro +57 -0
- package/src/components/StageBar.astro +137 -0
- package/src/components/StructuredData.astro +33 -0
- package/src/components/justify.ts +170 -0
- package/src/components/media-meta.ts +174 -0
- package/src/components/sections/AboutIntro.astro +46 -0
- package/src/components/sections/Banner.astro +31 -0
- package/src/components/sections/Contact.astro +22 -9
- package/src/components/sections/Cta.astro +33 -10
- package/src/components/sections/Faq.astro +50 -0
- package/src/components/sections/FeatureGrid.astro +40 -11
- package/src/components/sections/Gallery.astro +46 -0
- package/src/components/sections/Hero.astro +40 -12
- package/src/components/sections/LocationHours.astro +59 -0
- package/src/components/sections/Media.astro +44 -0
- package/src/components/sections/PricingTiers.astro +79 -0
- package/src/components/sections/ProductGrid.astro +73 -0
- package/src/components/sections/SplitImage.astro +61 -0
- package/src/components/sections/Steps.astro +58 -0
- package/src/components/sections/Testimonial.astro +51 -0
- package/src/components/sections.ts +452 -67
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// Every SCAFFOLD-ONCE file a config implies, in one list.
|
|
4
|
+
//
|
|
5
|
+
// This exists because the split between "generated" and "scaffolded" was drawn
|
|
6
|
+
// in the wrong place. `generateAstroidProject` returns the regenerated trio, and
|
|
7
|
+
// that trio emits STATIC IMPORTS of scaffold-once modules:
|
|
8
|
+
//
|
|
9
|
+
// src/worker.ts → import { handleQueueMessage } from "./queue.js"
|
|
10
|
+
// src/middleware.ts → import { resolvePortalUser } from "./portal-auth.js"
|
|
11
|
+
//
|
|
12
|
+
// The files behind those imports were written in exactly one place —
|
|
13
|
+
// `create-astroid`'s CLI — and nothing else could produce them. So turning a
|
|
14
|
+
// module on AFTER scaffold, by editing the one typed config the framework is
|
|
15
|
+
// built around, regenerated a trio importing files that did not exist. `astroid
|
|
16
|
+
// doctor` reported "healthy" and the project failed in Vite.
|
|
17
|
+
//
|
|
18
|
+
// Aggregating them here makes `astroid generate` able to complete a config
|
|
19
|
+
// change, `astroid doctor` able to notice one it hasn't, and `create-astroid`
|
|
20
|
+
// stop hand-listing the same nine files a third time.
|
|
21
|
+
//
|
|
22
|
+
// SCAFFOLD-ONCE means what it says: every file here is written only when ABSENT.
|
|
23
|
+
// Each one exists to be edited (what a catalog refresh means, which events
|
|
24
|
+
// matter, what a reset email says), so regenerating over it would destroy the
|
|
25
|
+
// work the seam exists to hold.
|
|
26
|
+
import { generateAstroidCheckoutRoute, generateAstroidSquareCard, } from "../commerce/checkout-scaffold.js";
|
|
27
|
+
import { generateCatalogMigrationSql } from "../commerce/mirror.js";
|
|
28
|
+
import { generateAstroidVitalsBeacon } from "../analytics/index.js";
|
|
29
|
+
import { generateAstroidActions } from "./actions.js";
|
|
30
|
+
import { cwvBeaconScript } from "louise-toolkit/analytics";
|
|
31
|
+
import { generateMapEmbedComponent, generateMapTileRoute } from "../map/scaffold.js";
|
|
32
|
+
import { generateAstroidGalleryPage } from "../portfolio/scaffold.js";
|
|
33
|
+
import { astroidPortal } from "../portal/config.js";
|
|
34
|
+
import { generateAstroidPortalAuth, generateAstroidPortalAuthRoute } from "../portal/scaffold.js";
|
|
35
|
+
import { generateAstroidEditSession } from "../realtime/scaffold.js";
|
|
36
|
+
import { generatePwaHeaders, generateServiceWorker, generateWebManifest } from "../pwa/generate.js";
|
|
37
|
+
import { astroidUsesQueues } from "../queues/messages.js";
|
|
38
|
+
import { generateAstroidQueueSeam, generateAstroidWebhookRoutes } from "../queues/scaffold.js";
|
|
39
|
+
/**
|
|
40
|
+
* Every scaffold-once file this config implies.
|
|
41
|
+
*
|
|
42
|
+
* Ordered by module so a `generate` that writes several prints them in a stable
|
|
43
|
+
* sequence. Returns `[]` for a plain marketing site with no modules — the
|
|
44
|
+
* baseline floor is entirely the regenerated trio plus the static template.
|
|
45
|
+
*/
|
|
46
|
+
export function generateAstroidScaffoldFiles(config) {
|
|
47
|
+
const files = [];
|
|
48
|
+
// --- commerce: the catalog table's migration ------------------------------
|
|
49
|
+
// Numbered 0003 so it lands after the template's 0000_content and the auth
|
|
50
|
+
// pair (0001, 0002) that `create-astroid` writes. Without it `--commerce`
|
|
51
|
+
// scaffolded a `products` table into src/schema.ts that no migration ever
|
|
52
|
+
// created, and the first sync wrote nothing while reporting success.
|
|
53
|
+
const catalogSql = generateCatalogMigrationSql(config);
|
|
54
|
+
if (catalogSql)
|
|
55
|
+
files.push({ path: "migrations/0003_catalog.sql", contents: catalogSql });
|
|
56
|
+
// --- the CWV beacon -------------------------------------------------------
|
|
57
|
+
// A static file under public/, so it is same-origin and covered by
|
|
58
|
+
// `script-src 'self'` — an inline script carrying generated content could not
|
|
59
|
+
// be hashed into the CSP and would be blocked.
|
|
60
|
+
const beacon = generateAstroidVitalsBeacon(config, cwvBeaconScript());
|
|
61
|
+
files.push({ path: beacon.path, contents: beacon.contents });
|
|
62
|
+
// --- site-owned schema tables --------------------------------------------
|
|
63
|
+
// Always: the generated src/schema.ts re-exports `./schema.site.js`, so the
|
|
64
|
+
// file must exist even when empty. A project declares tables Astroid doesn't
|
|
65
|
+
// manage here (an existing overlay predating Astroid, a redirects map, a
|
|
66
|
+
// customer address book) and they flow to drizzle-kit + the worker for free.
|
|
67
|
+
files.push({
|
|
68
|
+
path: "src/schema.site.ts",
|
|
69
|
+
contents: [
|
|
70
|
+
"// Site-owned Drizzle tables — the ones Astroid doesn't manage. Declare them",
|
|
71
|
+
"// here; the generated src/schema.ts re-exports everything from this file, so",
|
|
72
|
+
"// drizzle-kit sees them and the worker can import them. Empty by default.",
|
|
73
|
+
"//",
|
|
74
|
+
"// e.g. export const redirects = sqliteTable(\"redirects\", { … });",
|
|
75
|
+
"export {};",
|
|
76
|
+
"",
|
|
77
|
+
].join("\n"),
|
|
78
|
+
});
|
|
79
|
+
// --- the typed Astro Actions surface --------------------------------------
|
|
80
|
+
// Always: every project has editable pages, and the routes alone leave the
|
|
81
|
+
// Astro-native half of ADR 0001 unbuilt. Scaffold-once because it is meant to
|
|
82
|
+
// be added to.
|
|
83
|
+
files.push({ path: "src/actions/index.ts", contents: generateAstroidActions(config) });
|
|
84
|
+
// --- commerce: the server-authoritative payment seam ----------------------
|
|
85
|
+
// Scaffold-once: a real store adds shipping, tax, an order row, a receipt.
|
|
86
|
+
// What's fixed is the sequence that keeps a charge correct.
|
|
87
|
+
const checkoutRoute = generateAstroidCheckoutRoute(config);
|
|
88
|
+
if (checkoutRoute)
|
|
89
|
+
files.push({ path: "src/pages/api/checkout.ts", contents: checkoutRoute });
|
|
90
|
+
const squareCard = generateAstroidSquareCard(config);
|
|
91
|
+
if (squareCard)
|
|
92
|
+
files.push({ path: "src/components/SquareCard.astro", contents: squareCard });
|
|
93
|
+
// --- queues: the consumer seam + one receiver per commerce provider --------
|
|
94
|
+
// Both exist to be edited (what a refresh means; which events matter), which
|
|
95
|
+
// is why `astroid generate` must never rewrite them.
|
|
96
|
+
if (astroidUsesQueues(config)) {
|
|
97
|
+
files.push({ path: "src/queue.ts", contents: generateAstroidQueueSeam(config) });
|
|
98
|
+
// One receiver per provider — a site can run two (invoicing + storefront).
|
|
99
|
+
for (const route of generateAstroidWebhookRoutes(config)) {
|
|
100
|
+
files.push({ path: route.path, contents: route.contents });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// --- portfolio: the gallery page -----------------------------------------
|
|
104
|
+
// "Which assets appear, in what order" is the first thing a portfolio changes.
|
|
105
|
+
const gallery = generateAstroidGalleryPage(config);
|
|
106
|
+
if (gallery)
|
|
107
|
+
files.push({ path: "src/pages/work.astro", contents: gallery });
|
|
108
|
+
// --- pwa: the service worker, manifest, and its headers -------------------
|
|
109
|
+
// Static files under public/, not generated source — a service worker is not
|
|
110
|
+
// bundled, and `_headers` is shared with whatever else writes to it.
|
|
111
|
+
const sw = generateServiceWorker(config);
|
|
112
|
+
if (sw) {
|
|
113
|
+
files.push({ path: "public/sw.js", contents: sw });
|
|
114
|
+
const manifest = generateWebManifest(config);
|
|
115
|
+
if (manifest)
|
|
116
|
+
files.push({ path: "public/manifest.webmanifest", contents: manifest });
|
|
117
|
+
const headers = generatePwaHeaders(config);
|
|
118
|
+
if (headers) {
|
|
119
|
+
files.push({
|
|
120
|
+
path: "public/_headers",
|
|
121
|
+
contents: headers,
|
|
122
|
+
apply: "append-once",
|
|
123
|
+
// The service-worker path is the one token this stanza always contains
|
|
124
|
+
// and nothing else in a `_headers` file would.
|
|
125
|
+
marker: "/sw.js",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// --- map: the tile route + embed component --------------------------------
|
|
130
|
+
// Generated into the project rather than shipped in astroidjs so maplibre-gl
|
|
131
|
+
// stays a dependency of the projects that actually draw a map.
|
|
132
|
+
const tileRoute = generateMapTileRoute(config);
|
|
133
|
+
if (tileRoute)
|
|
134
|
+
files.push({ path: "src/pages/map/basemap.pmtiles.ts", contents: tileRoute });
|
|
135
|
+
const mapEmbed = generateMapEmbedComponent(config);
|
|
136
|
+
if (mapEmbed)
|
|
137
|
+
files.push({ path: "src/components/MapEmbed.astro", contents: mapEmbed });
|
|
138
|
+
// --- realtime: the per-page edit-session Durable Object -------------------
|
|
139
|
+
// Scaffold-once because it must import `cloudflare:workers` (runtime-only) and
|
|
140
|
+
// because `persist` is the seam a project tunes.
|
|
141
|
+
const editSession = generateAstroidEditSession(config);
|
|
142
|
+
if (editSession)
|
|
143
|
+
files.push({ path: "src/edit-session.ts", contents: editSession });
|
|
144
|
+
// --- portal: the second auth instance + its mounted catch-all -------------
|
|
145
|
+
// A site edits the reset email and the role a new account gets, but not the
|
|
146
|
+
// mount/cookie/table prefixes that keep the two instances isolated.
|
|
147
|
+
const portalAuth = generateAstroidPortalAuth(config);
|
|
148
|
+
if (portalAuth) {
|
|
149
|
+
files.push({ path: "src/portal-auth.ts", contents: portalAuth });
|
|
150
|
+
const route = generateAstroidPortalAuthRoute(config);
|
|
151
|
+
// Always non-null alongside portalAuth (same `astroidPortal` gate), but the
|
|
152
|
+
// types don't know that and a silent drop here is a portal that cannot
|
|
153
|
+
// authenticate — so assert it rather than `?.`-ing it away.
|
|
154
|
+
//
|
|
155
|
+
// The route file lives at the mount path, because Astro routing is
|
|
156
|
+
// file-path-based: a portal mounted at `/api/shop-auth` needs its catch-all
|
|
157
|
+
// at `src/pages/api/shop-auth/[...all].ts`, not the default `/api/portal-auth`.
|
|
158
|
+
// Derive it from the resolved basePath so a configured mount and its route
|
|
159
|
+
// can't drift.
|
|
160
|
+
const portal = astroidPortal(config);
|
|
161
|
+
if (route) {
|
|
162
|
+
files.push({ path: `src/pages${portal.basePath}/[...all].ts`, contents: route });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return files;
|
|
166
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AstroidConfig } from "../config.js";
|
|
2
|
+
/** Tuning for the PWA scaffold. Every field has a sensible derivation. */
|
|
3
|
+
export interface PwaConfig {
|
|
4
|
+
/**
|
|
5
|
+
* URL prefix the app is installed at. Default `"/"`.
|
|
6
|
+
*
|
|
7
|
+
* A narrower scope (`"/order"`) is usually right: it keeps the worker off the
|
|
8
|
+
* marketing pages entirely, which is both safer and a smaller cache.
|
|
9
|
+
*/
|
|
10
|
+
scope?: string;
|
|
11
|
+
/** Home screen name. Defaults to the brand name. */
|
|
12
|
+
shortName?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
/** `standalone` (default) hides browser chrome; `browser` opts out of it. */
|
|
15
|
+
display?: "standalone" | "minimal-ui" | "fullscreen" | "browser";
|
|
16
|
+
orientation?: "any" | "portrait" | "landscape";
|
|
17
|
+
/** Splash background. Defaults to white. */
|
|
18
|
+
backgroundColor?: string;
|
|
19
|
+
/** Theme colour. Defaults to the brand colour. */
|
|
20
|
+
themeColor?: string;
|
|
21
|
+
/** Extra paths to precache alongside the scope root. */
|
|
22
|
+
shell?: string[];
|
|
23
|
+
}
|
|
24
|
+
/** True when this project switched the PWA on. */
|
|
25
|
+
export declare const usesPwa: (config: AstroidConfig) => boolean;
|
|
26
|
+
/** Resolved PWA settings — config over derivation over default. */
|
|
27
|
+
export declare function resolvePwa(config: AstroidConfig): Required<Omit<PwaConfig, "shell">> & {
|
|
28
|
+
shell: string[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* `public/manifest.webmanifest`.
|
|
32
|
+
*
|
|
33
|
+
* Icons are declared but NOT generated — a brand's icon is not something a
|
|
34
|
+
* scaffold can invent, and emitting placeholders would produce an installable
|
|
35
|
+
* app with a grey square for a face. The generated README step says to add them.
|
|
36
|
+
*/
|
|
37
|
+
export declare function generateWebManifest(config: AstroidConfig): string | null;
|
|
38
|
+
/** `public/sw.js`. Plain JS — a service worker is not bundled. */
|
|
39
|
+
export declare function generateServiceWorker(config: AstroidConfig): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* The `public/_headers` block the PWA needs.
|
|
42
|
+
*
|
|
43
|
+
* `Service-Worker-Allowed` is emitted ONLY when the scope is broader than the
|
|
44
|
+
* script's own location — which, with `sw.js` at the root, never is. Emitting it
|
|
45
|
+
* unconditionally (as the reference does) is harmless but misleading: it implies
|
|
46
|
+
* a requirement that isn't there, and someone later moving the script will trust
|
|
47
|
+
* a header that no longer says what they need.
|
|
48
|
+
*/
|
|
49
|
+
export declare function generatePwaHeaders(config: AstroidConfig): string | null;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// The PWA scaffold — a scoped service worker, a manifest, and the headers they
|
|
4
|
+
// need.
|
|
5
|
+
//
|
|
6
|
+
// The scoping is the whole design, not a detail. A Louise site is CMS-edited:
|
|
7
|
+
// an editor signs in, flips edit mode on, and edits the live page in place. A
|
|
8
|
+
// service worker that cached HTML across the whole origin would serve that
|
|
9
|
+
// editor a stale copy of the page they are trying to change — and the bug would
|
|
10
|
+
// present as "my edits don't save", which is about as far from the cause as a
|
|
11
|
+
// report can get.
|
|
12
|
+
//
|
|
13
|
+
// So the generated worker is scoped, and inside its scope it still refuses to
|
|
14
|
+
// touch anything dynamic:
|
|
15
|
+
//
|
|
16
|
+
// • `/api/*` never cached — checkout, auth, and every Louise write
|
|
17
|
+
// • editor routes never cached — the studio must always be live
|
|
18
|
+
// • edit-mode URLs never cached — `?louise` marks a request as an editing
|
|
19
|
+
// session, and caching one poisons it for everyone
|
|
20
|
+
//
|
|
21
|
+
// Everything else is the ordinary split: navigations network-first with a
|
|
22
|
+
// cached offline shell, hashed assets cache-first because their names change
|
|
23
|
+
// when their content does.
|
|
24
|
+
/** True when this project switched the PWA on. */
|
|
25
|
+
export const usesPwa = (config) => (config.modules ?? []).includes("pwa");
|
|
26
|
+
/** Resolved PWA settings — config over derivation over default. */
|
|
27
|
+
export function resolvePwa(config) {
|
|
28
|
+
const pwa = config.pwa ?? {};
|
|
29
|
+
// Normalized to a leading slash and no trailing one (except root), because
|
|
30
|
+
// scope comparison is a string prefix test and "/order/" vs "/order" would
|
|
31
|
+
// silently exclude the scope root itself.
|
|
32
|
+
const raw = pwa.scope ?? "/";
|
|
33
|
+
const scope = raw === "/" ? "/" : `/${raw.replace(/^\/+|\/+$/g, "")}`;
|
|
34
|
+
return {
|
|
35
|
+
scope,
|
|
36
|
+
shortName: pwa.shortName ?? config.theme.name,
|
|
37
|
+
description: pwa.description ?? `${config.theme.name} — installable app.`,
|
|
38
|
+
display: pwa.display ?? "standalone",
|
|
39
|
+
orientation: pwa.orientation ?? "any",
|
|
40
|
+
backgroundColor: pwa.backgroundColor ?? "#ffffff",
|
|
41
|
+
themeColor: pwa.themeColor ?? config.theme.colors.brand,
|
|
42
|
+
shell: [scope, "/manifest.webmanifest", ...(pwa.shell ?? [])],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `public/manifest.webmanifest`.
|
|
47
|
+
*
|
|
48
|
+
* Icons are declared but NOT generated — a brand's icon is not something a
|
|
49
|
+
* scaffold can invent, and emitting placeholders would produce an installable
|
|
50
|
+
* app with a grey square for a face. The generated README step says to add them.
|
|
51
|
+
*/
|
|
52
|
+
export function generateWebManifest(config) {
|
|
53
|
+
if (!usesPwa(config))
|
|
54
|
+
return null;
|
|
55
|
+
const pwa = resolvePwa(config);
|
|
56
|
+
return `${JSON.stringify({
|
|
57
|
+
name: config.theme.name,
|
|
58
|
+
short_name: pwa.shortName,
|
|
59
|
+
description: pwa.description,
|
|
60
|
+
id: pwa.scope,
|
|
61
|
+
start_url: pwa.scope,
|
|
62
|
+
scope: pwa.scope,
|
|
63
|
+
display: pwa.display,
|
|
64
|
+
orientation: pwa.orientation,
|
|
65
|
+
background_color: pwa.backgroundColor,
|
|
66
|
+
theme_color: pwa.themeColor,
|
|
67
|
+
icons: [
|
|
68
|
+
{ src: "/icons/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any" },
|
|
69
|
+
{ src: "/icons/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any" },
|
|
70
|
+
// A maskable icon is a separate asset, not a flag on the same file: the
|
|
71
|
+
// platform crops it to its own shape, so the artwork needs padding the
|
|
72
|
+
// `any` icon shouldn't have.
|
|
73
|
+
{ src: "/icons/maskable-192.png", sizes: "192x192", type: "image/png", purpose: "maskable" },
|
|
74
|
+
{ src: "/icons/maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
|
|
75
|
+
],
|
|
76
|
+
}, null, 2)}\n`;
|
|
77
|
+
}
|
|
78
|
+
/** `public/sw.js`. Plain JS — a service worker is not bundled. */
|
|
79
|
+
export function generateServiceWorker(config) {
|
|
80
|
+
if (!usesPwa(config))
|
|
81
|
+
return null;
|
|
82
|
+
const pwa = resolvePwa(config);
|
|
83
|
+
// Cache name carries the project key so two Astroid apps on one origin (a
|
|
84
|
+
// preview deploy, say) can't read each other's entries.
|
|
85
|
+
const cacheName = `${config.key}-pwa-v1`;
|
|
86
|
+
return [
|
|
87
|
+
`// Service worker for the ${config.theme.name} PWA, scoped to ${pwa.scope}.`,
|
|
88
|
+
"//",
|
|
89
|
+
"// Generated by Astroid. Safe to edit — bump CACHE to invalidate everything",
|
|
90
|
+
"// on the next visit.",
|
|
91
|
+
"//",
|
|
92
|
+
"// What it deliberately never caches, and why:",
|
|
93
|
+
"// /api/* checkout, auth, and every Louise write — a cached POST",
|
|
94
|
+
"// response or a stale session is worse than being offline",
|
|
95
|
+
"// editor routes the studio must always be live",
|
|
96
|
+
"// ?louise URLs an edit-mode request; caching one would serve an editor a",
|
|
97
|
+
"// stale copy of the page they're editing, and the bug would",
|
|
98
|
+
"// present as 'my changes don't save'",
|
|
99
|
+
`const CACHE = ${JSON.stringify(cacheName)};`,
|
|
100
|
+
`const SCOPE = ${JSON.stringify(pwa.scope)};`,
|
|
101
|
+
`const SHELL = ${JSON.stringify([...new Set(pwa.shell)])};`,
|
|
102
|
+
"",
|
|
103
|
+
"self.addEventListener('install', (event) => {",
|
|
104
|
+
" event.waitUntil(",
|
|
105
|
+
" caches",
|
|
106
|
+
" .open(CACHE)",
|
|
107
|
+
" // Best-effort: one 404 or redirect in the shell must not fail the",
|
|
108
|
+
" // whole install and leave the app without a worker.",
|
|
109
|
+
" .then((c) => Promise.allSettled(SHELL.map((u) => c.add(u))))",
|
|
110
|
+
" .then(() => self.skipWaiting()),",
|
|
111
|
+
" );",
|
|
112
|
+
"});",
|
|
113
|
+
"",
|
|
114
|
+
"self.addEventListener('activate', (event) => {",
|
|
115
|
+
" event.waitUntil(",
|
|
116
|
+
" caches",
|
|
117
|
+
" .keys()",
|
|
118
|
+
" .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))",
|
|
119
|
+
" .then(() => self.clients.claim()),",
|
|
120
|
+
" );",
|
|
121
|
+
"});",
|
|
122
|
+
"",
|
|
123
|
+
"/** Requests that must always hit the network. */",
|
|
124
|
+
"function isDynamic(url) {",
|
|
125
|
+
" return (",
|
|
126
|
+
" url.pathname.startsWith('/api/') ||",
|
|
127
|
+
" url.pathname.startsWith('/login') ||",
|
|
128
|
+
" url.pathname.startsWith('/admin') ||",
|
|
129
|
+
" // Louise's edit mode. `has()` rather than a value check: `?louise` with",
|
|
130
|
+
" // no value is the usual form.",
|
|
131
|
+
" url.searchParams.has('louise')",
|
|
132
|
+
" );",
|
|
133
|
+
"}",
|
|
134
|
+
"",
|
|
135
|
+
"/** Hashed build output — the filename changes when the content does, so it",
|
|
136
|
+
" * can be cached forever without a staleness risk. */",
|
|
137
|
+
"function isImmutable(url) {",
|
|
138
|
+
" return url.pathname.startsWith('/_astro/') || url.pathname.startsWith('/icons/');",
|
|
139
|
+
"}",
|
|
140
|
+
"",
|
|
141
|
+
"self.addEventListener('fetch', (event) => {",
|
|
142
|
+
" const req = event.request;",
|
|
143
|
+
" // Only GET: a cached POST response would be a correctness bug, not a",
|
|
144
|
+
" // speedup.",
|
|
145
|
+
" if (req.method !== 'GET') return;",
|
|
146
|
+
"",
|
|
147
|
+
" const url = new URL(req.url);",
|
|
148
|
+
" // Cross-origin requests belong to whoever serves them.",
|
|
149
|
+
" if (url.origin !== self.location.origin) return;",
|
|
150
|
+
" if (isDynamic(url)) return;",
|
|
151
|
+
" // Outside the scope this worker has no business intercepting — the rest of",
|
|
152
|
+
" // the site is CMS-edited and must stay live.",
|
|
153
|
+
" if (SCOPE !== '/' && !url.pathname.startsWith(SCOPE)) return;",
|
|
154
|
+
"",
|
|
155
|
+
" if (req.mode === 'navigate') {",
|
|
156
|
+
" // Network-first: a page is only worth serving from cache when the network",
|
|
157
|
+
" // failed, since the content behind it changes.",
|
|
158
|
+
" event.respondWith(",
|
|
159
|
+
" fetch(req)",
|
|
160
|
+
" .then((res) => {",
|
|
161
|
+
" const copy = res.clone();",
|
|
162
|
+
" caches",
|
|
163
|
+
" .open(CACHE)",
|
|
164
|
+
" .then((c) => c.put(req, copy))",
|
|
165
|
+
" .catch(() => {});",
|
|
166
|
+
" return res;",
|
|
167
|
+
" })",
|
|
168
|
+
" .catch(() => caches.match(req).then((r) => r || caches.match(SCOPE))),",
|
|
169
|
+
" );",
|
|
170
|
+
" return;",
|
|
171
|
+
" }",
|
|
172
|
+
"",
|
|
173
|
+
" if (isImmutable(url)) {",
|
|
174
|
+
" event.respondWith(",
|
|
175
|
+
" caches.match(req).then(",
|
|
176
|
+
" (cached) =>",
|
|
177
|
+
" cached ||",
|
|
178
|
+
" fetch(req).then((res) => {",
|
|
179
|
+
" const copy = res.clone();",
|
|
180
|
+
" caches",
|
|
181
|
+
" .open(CACHE)",
|
|
182
|
+
" .then((c) => c.put(req, copy))",
|
|
183
|
+
" .catch(() => {});",
|
|
184
|
+
" return res;",
|
|
185
|
+
" }),",
|
|
186
|
+
" ),",
|
|
187
|
+
" );",
|
|
188
|
+
" }",
|
|
189
|
+
" // Everything else falls through to the browser's own handling.",
|
|
190
|
+
"});",
|
|
191
|
+
"",
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The `public/_headers` block the PWA needs.
|
|
196
|
+
*
|
|
197
|
+
* `Service-Worker-Allowed` is emitted ONLY when the scope is broader than the
|
|
198
|
+
* script's own location — which, with `sw.js` at the root, never is. Emitting it
|
|
199
|
+
* unconditionally (as the reference does) is harmless but misleading: it implies
|
|
200
|
+
* a requirement that isn't there, and someone later moving the script will trust
|
|
201
|
+
* a header that no longer says what they need.
|
|
202
|
+
*/
|
|
203
|
+
export function generatePwaHeaders(config) {
|
|
204
|
+
if (!usesPwa(config))
|
|
205
|
+
return null;
|
|
206
|
+
return [
|
|
207
|
+
"",
|
|
208
|
+
"# The service worker must revalidate on every load, or a bad worker sticks",
|
|
209
|
+
"# around until its cache entry expires — and it controls every page in scope.",
|
|
210
|
+
"/sw.js",
|
|
211
|
+
" Cache-Control: no-cache",
|
|
212
|
+
"",
|
|
213
|
+
"/manifest.webmanifest",
|
|
214
|
+
" Content-Type: application/manifest+json",
|
|
215
|
+
" Cache-Control: public, max-age=3600",
|
|
216
|
+
"",
|
|
217
|
+
].join("\n");
|
|
218
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { generatePwaHeaders, generateServiceWorker, generateWebManifest, type PwaConfig, resolvePwa, usesPwa, } from "./generate.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type AstroidQueueMessage } from "./messages.js";
|
|
2
|
+
export interface QueueHandlerOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Re-sync whatever the provider owns — the catalog mirror, a cache. Called
|
|
5
|
+
* for a periodic refresh and for webhooks that touched the catalog.
|
|
6
|
+
*
|
|
7
|
+
* Throwing marks the message for retry, which is usually right: a failed
|
|
8
|
+
* refresh means the site is serving stale data.
|
|
9
|
+
*/
|
|
10
|
+
refreshCatalog?: () => void | Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Anything else this project queues. Runs for every message, after the
|
|
13
|
+
* catalog dispatch above, so a project can add its own kinds without
|
|
14
|
+
* reimplementing the refresh logic.
|
|
15
|
+
*/
|
|
16
|
+
onMessage?: (message: AstroidQueueMessage) => void | Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build the per-message handler to hand to `processBatch`.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* async queue(batch, env) {
|
|
23
|
+
* await processBatch(batch, astroidQueueHandler({
|
|
24
|
+
* refreshCatalog: () => refreshCatalog(env),
|
|
25
|
+
* }));
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function astroidQueueHandler(options?: QueueHandlerOptions): (message: AstroidQueueMessage) => Promise<void>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// The consumer side.
|
|
4
|
+
//
|
|
5
|
+
// `processBatch` (louise-toolkit/queues) already owns per-message ack/retry, and
|
|
6
|
+
// Cloudflare Queues owns redelivery and DLQ routing. What every site then wrote
|
|
7
|
+
// on top was the same dispatch: a periodic refresh runs the re-sync, a webhook
|
|
8
|
+
// runs it only if the event actually touched the catalog, and everything else
|
|
9
|
+
// acks as a no-op.
|
|
10
|
+
//
|
|
11
|
+
// That last part matters more than it looks. Order, payment, and subscription
|
|
12
|
+
// events are read live from the provider, so there is nothing local to update —
|
|
13
|
+
// but they still arrive, in volume. A consumer that treats every event as
|
|
14
|
+
// actionable turns a busy sales day into a catalog-refresh storm.
|
|
15
|
+
import { affectsCatalog } from "./messages.js";
|
|
16
|
+
/**
|
|
17
|
+
* Build the per-message handler to hand to `processBatch`.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* async queue(batch, env) {
|
|
21
|
+
* await processBatch(batch, astroidQueueHandler({
|
|
22
|
+
* refreshCatalog: () => refreshCatalog(env),
|
|
23
|
+
* }));
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function astroidQueueHandler(options = {}) {
|
|
28
|
+
return async (message) => {
|
|
29
|
+
if (message.kind === "catalog_refresh") {
|
|
30
|
+
await options.refreshCatalog?.();
|
|
31
|
+
}
|
|
32
|
+
else if (message.kind === "webhook" && affectsCatalog(message.provider, message.type)) {
|
|
33
|
+
await options.refreshCatalog?.();
|
|
34
|
+
}
|
|
35
|
+
await options.onMessage?.(message);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { astroidQueueHandler, type QueueHandlerOptions } from "./consumer.js";
|
|
2
|
+
export { affectsCatalog, ASTROID_DEFAULT_CRON, ASTROID_HEALTH_CRON, ASTROID_QUEUE_BINDING, type AstroidQueueMessage, astroidCron, astroidCrons, astroidQueueNames, astroidUsesQueues, type CatalogRefreshMessage, type WebhookMessage, } from "./messages.js";
|
|
3
|
+
export { generateAstroidEnvBindings, generateAstroidQueueSeam, generateAstroidWebhookRoute, generateAstroidWebhookRoutes, } from "./scaffold.js";
|
|
4
|
+
export { handleWebhook, type QueueProducer, type WebhookRouteOptions, type WebhookVerifyInput, } from "./webhook.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
export { astroidQueueHandler } from "./consumer.js";
|
|
3
|
+
export { affectsCatalog, ASTROID_DEFAULT_CRON, ASTROID_HEALTH_CRON, ASTROID_QUEUE_BINDING, astroidCron, astroidCrons, astroidQueueNames, astroidUsesQueues, } from "./messages.js";
|
|
4
|
+
export { generateAstroidEnvBindings, generateAstroidQueueSeam, generateAstroidWebhookRoute, generateAstroidWebhookRoutes, } from "./scaffold.js";
|
|
5
|
+
export { handleWebhook, } from "./webhook.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AstroidConfig } from "../config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Whether this project runs a queue consumer + cron.
|
|
4
|
+
*
|
|
5
|
+
* On by default whenever commerce is configured: a commerce provider means
|
|
6
|
+
* webhooks, and a webhook processed inline is a webhook you drop the moment the
|
|
7
|
+
* provider's delivery timeout is shorter than your catalog sync.
|
|
8
|
+
*/
|
|
9
|
+
export declare function astroidUsesQueues(config: AstroidConfig): boolean;
|
|
10
|
+
/** Hourly. Frequent enough that stale data has a bounded lifetime, rare enough
|
|
11
|
+
* to be free. */
|
|
12
|
+
export declare const ASTROID_DEFAULT_CRON = "0 * * * *";
|
|
13
|
+
/** The cron expression for the safety-net re-sync, or null when disabled. */
|
|
14
|
+
export declare function astroidCron(config: AstroidConfig): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Daily, at an off-peak-ish minute. The health scan crawls the site's own pages,
|
|
17
|
+
* so it is deliberately NOT on the hourly catalog cron — hourly would be a
|
|
18
|
+
* self-inflicted crawl 24× a day to recompute counts that move slowly.
|
|
19
|
+
*/
|
|
20
|
+
export declare const ASTROID_HEALTH_CRON = "17 4 * * *";
|
|
21
|
+
/**
|
|
22
|
+
* Every cron expression the project needs, in the order they're declared in
|
|
23
|
+
* `wrangler.jsonc`.
|
|
24
|
+
*
|
|
25
|
+
* Cloudflare fires ONE `scheduled` handler for all of them and identifies which
|
|
26
|
+
* by `controller.cron`, so the generated handler dispatches on that string. That
|
|
27
|
+
* is why this list is derived in one place rather than assembled at each call
|
|
28
|
+
* site: the wrangler `triggers.crons` array and the handler's dispatch have to
|
|
29
|
+
* agree exactly, and a mismatch is a job that silently never runs.
|
|
30
|
+
*/
|
|
31
|
+
export declare function astroidCrons(config: AstroidConfig): string[];
|
|
32
|
+
/** Binding name for the project's queue producer. */
|
|
33
|
+
export declare const ASTROID_QUEUE_BINDING = "COMMERCE_QUEUE";
|
|
34
|
+
/** Queue names derived from the project key — the main queue and its DLQ. */
|
|
35
|
+
export declare function astroidQueueNames(config: AstroidConfig): {
|
|
36
|
+
queue: string;
|
|
37
|
+
dlq: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* A provider webhook, thinned to what a consumer needs. The raw `payload` is
|
|
41
|
+
* carried through deliberately: the consumer's needs change faster than the
|
|
42
|
+
* webhook contract, and a message that only kept the fields today's consumer
|
|
43
|
+
* reads can't be replayed from the DLQ once that changes.
|
|
44
|
+
*/
|
|
45
|
+
export interface WebhookMessage {
|
|
46
|
+
kind: "webhook";
|
|
47
|
+
/** Which integration sent it — `"square"`, `"stripe"`, `"fourthwall"`. */
|
|
48
|
+
provider: string;
|
|
49
|
+
/** The provider's event type, e.g. `"catalog.version.updated"`. */
|
|
50
|
+
type: string;
|
|
51
|
+
payload: unknown;
|
|
52
|
+
}
|
|
53
|
+
/** The periodic (or manually triggered) full re-sync. */
|
|
54
|
+
export interface CatalogRefreshMessage {
|
|
55
|
+
kind: "catalog_refresh";
|
|
56
|
+
}
|
|
57
|
+
/** Everything the project's queue carries. Match on `kind`. */
|
|
58
|
+
export type AstroidQueueMessage = WebhookMessage | CatalogRefreshMessage;
|
|
59
|
+
/** Whether a provider event should trigger a catalog re-sync. */
|
|
60
|
+
export declare function affectsCatalog(provider: string, type: string): boolean;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// What flows through the project's queue, and when it matters.
|
|
4
|
+
/**
|
|
5
|
+
* Whether this project runs a queue consumer + cron.
|
|
6
|
+
*
|
|
7
|
+
* On by default whenever commerce is configured: a commerce provider means
|
|
8
|
+
* webhooks, and a webhook processed inline is a webhook you drop the moment the
|
|
9
|
+
* provider's delivery timeout is shorter than your catalog sync.
|
|
10
|
+
*/
|
|
11
|
+
export function astroidUsesQueues(config) {
|
|
12
|
+
return config.queues?.enabled ?? Boolean(config.commerce);
|
|
13
|
+
}
|
|
14
|
+
/** Hourly. Frequent enough that stale data has a bounded lifetime, rare enough
|
|
15
|
+
* to be free. */
|
|
16
|
+
export const ASTROID_DEFAULT_CRON = "0 * * * *";
|
|
17
|
+
/** The cron expression for the safety-net re-sync, or null when disabled. */
|
|
18
|
+
export function astroidCron(config) {
|
|
19
|
+
if (!astroidUsesQueues(config))
|
|
20
|
+
return null;
|
|
21
|
+
const cron = config.queues?.cron;
|
|
22
|
+
if (cron === false)
|
|
23
|
+
return null;
|
|
24
|
+
return cron ?? ASTROID_DEFAULT_CRON;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Daily, at an off-peak-ish minute. The health scan crawls the site's own pages,
|
|
28
|
+
* so it is deliberately NOT on the hourly catalog cron — hourly would be a
|
|
29
|
+
* self-inflicted crawl 24× a day to recompute counts that move slowly.
|
|
30
|
+
*/
|
|
31
|
+
export const ASTROID_HEALTH_CRON = "17 4 * * *";
|
|
32
|
+
/**
|
|
33
|
+
* Every cron expression the project needs, in the order they're declared in
|
|
34
|
+
* `wrangler.jsonc`.
|
|
35
|
+
*
|
|
36
|
+
* Cloudflare fires ONE `scheduled` handler for all of them and identifies which
|
|
37
|
+
* by `controller.cron`, so the generated handler dispatches on that string. That
|
|
38
|
+
* is why this list is derived in one place rather than assembled at each call
|
|
39
|
+
* site: the wrangler `triggers.crons` array and the handler's dispatch have to
|
|
40
|
+
* agree exactly, and a mismatch is a job that silently never runs.
|
|
41
|
+
*/
|
|
42
|
+
export function astroidCrons(config) {
|
|
43
|
+
const crons = [ASTROID_HEALTH_CRON];
|
|
44
|
+
const catalog = astroidCron(config);
|
|
45
|
+
if (catalog)
|
|
46
|
+
crons.push(catalog);
|
|
47
|
+
return crons;
|
|
48
|
+
}
|
|
49
|
+
/** Binding name for the project's queue producer. */
|
|
50
|
+
export const ASTROID_QUEUE_BINDING = "COMMERCE_QUEUE";
|
|
51
|
+
/** Queue names derived from the project key — the main queue and its DLQ. */
|
|
52
|
+
export function astroidQueueNames(config) {
|
|
53
|
+
return { queue: `${config.key}-commerce`, dlq: `${config.key}-commerce-dlq` };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Event-type prefixes that invalidate a cached catalog, per provider.
|
|
57
|
+
*
|
|
58
|
+
* Prefix matching rather than an exhaustive list on purpose: providers add event
|
|
59
|
+
* types, and the failure mode of matching one too many is a redundant refresh,
|
|
60
|
+
* while missing one is a storefront serving a price that no longer exists.
|
|
61
|
+
*/
|
|
62
|
+
const CATALOG_EVENT_PREFIXES = {
|
|
63
|
+
square: ["catalog.", "inventory.", "item.", "item_variation."],
|
|
64
|
+
stripe: ["product.", "price.", "plan."],
|
|
65
|
+
fourthwall: ["product.", "variant.", "collection."],
|
|
66
|
+
};
|
|
67
|
+
/** Whether a provider event should trigger a catalog re-sync. */
|
|
68
|
+
export function affectsCatalog(provider, type) {
|
|
69
|
+
const prefixes = CATALOG_EVENT_PREFIXES[provider] ?? [];
|
|
70
|
+
return prefixes.some((prefix) => type.startsWith(prefix));
|
|
71
|
+
}
|