astroidjs 0.1.1 → 0.2.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/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 +67 -0
- package/dist/commerce/mirror.js +203 -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 +174 -12
- package/dist/config.js +43 -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 +14 -0
- package/dist/index.js +14 -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 +50 -0
- package/dist/portal/guard.d.ts +48 -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 +106 -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 +140 -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 +42 -8
- package/dist/schema/collections.js +102 -8
- package/dist/schema/generate.js +10 -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 +107 -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 +325 -37
- 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 -5
- 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,106 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// `src/actions/index.ts` — the Astro-native, typed mutation surface (ADR 0001
|
|
4
|
+
// layer 2), beside the framework-agnostic `/api/louise/*` routes.
|
|
5
|
+
//
|
|
6
|
+
// Astroid generated only the route half. That is not a missing convenience: the
|
|
7
|
+
// two entrypoints write the SAME rows, and the whole reason `louise-toolkit/astro`
|
|
8
|
+
// exposes these factories is that each one shares the raw route's store path —
|
|
9
|
+
// `applyFieldSave`, `applySettingsPatch`, `applySaveDraft`. A project that wired
|
|
10
|
+
// its own Actions by hand would get a second write path, and a second write path
|
|
11
|
+
// is where validation, sanitization, and draft-merge semantics drift apart
|
|
12
|
+
// silently (#138).
|
|
13
|
+
//
|
|
14
|
+
// So this file is SCAFFOLD-ONCE and is meant to be added to — the reference site
|
|
15
|
+
// keeps its own bespoke actions right beside these — but the three below come
|
|
16
|
+
// pre-wired against the same tables and the same collection config the generated
|
|
17
|
+
// worker uses.
|
|
18
|
+
/** `src/actions/index.ts` — the typed mutation surface, scaffolded once. */
|
|
19
|
+
export function generateAstroidActions(_config) {
|
|
20
|
+
return [
|
|
21
|
+
"// The typed Astro Actions surface — ADR 0001 layer 2.",
|
|
22
|
+
"//",
|
|
23
|
+
"// Scaffolded once and yours to ADD to: put your own `defineAction`s in the",
|
|
24
|
+
"// `server` object below, alongside these.",
|
|
25
|
+
"//",
|
|
26
|
+
"// What matters about the three that ship here is that they are NOT a second",
|
|
27
|
+
"// implementation. Each factory shares the store path of its raw",
|
|
28
|
+
"// `/api/louise/*` counterpart (`applyFieldSave`, `applySettingsPatch`,",
|
|
29
|
+
"// `applySaveDraft`), so a field is validated once and written in exactly one",
|
|
30
|
+
"// place however it was called. Hand-rolling an Action that writes the same",
|
|
31
|
+
"// row is how sanitization and draft-merge semantics drift apart without",
|
|
32
|
+
"// anything failing.",
|
|
33
|
+
'import { ActionError, defineAction } from "astro:actions";',
|
|
34
|
+
'import { env } from "cloudflare:workers";',
|
|
35
|
+
"import {",
|
|
36
|
+
" louiseSaveAction,",
|
|
37
|
+
" louiseSaveDraftAction,",
|
|
38
|
+
" louiseSettingsAction,",
|
|
39
|
+
'} from "louise-toolkit/astro";',
|
|
40
|
+
"import {",
|
|
41
|
+
" ASTROID_SETTINGS_COLUMNS,",
|
|
42
|
+
" ASTROID_SETTINGS_IMAGE_KEYS,",
|
|
43
|
+
" astroidPagesCollection,",
|
|
44
|
+
'} from "astroidjs";',
|
|
45
|
+
'import astroidConfig from "../../astroid.config.js";',
|
|
46
|
+
'import { pages, pagesVersions, siteSettings } from "../schema.js";',
|
|
47
|
+
"",
|
|
48
|
+
"const pagesCollection = astroidPagesCollection(astroidConfig);",
|
|
49
|
+
"",
|
|
50
|
+
"// Astro v6+ removed `Astro.locals.runtime.env`, so the bindings are resolved",
|
|
51
|
+
"// from `cloudflare:workers` — the same env the raw routes read.",
|
|
52
|
+
"const getEnv = () => env as unknown as CloudflareEnv;",
|
|
53
|
+
"",
|
|
54
|
+
"// `getEditor` is left to its default (`locals.editor`), which the generated",
|
|
55
|
+
"// middleware sets for a signed-in editor. A falsy result answers 401, so these",
|
|
56
|
+
"// carry the same gate as the routes rather than a parallel one.",
|
|
57
|
+
"const deps = { ActionError, getEnv };",
|
|
58
|
+
"",
|
|
59
|
+
"export const server = {",
|
|
60
|
+
" louise: {",
|
|
61
|
+
" // Inline field save (title, SEO) — the live, non-versioned path.",
|
|
62
|
+
" save: defineAction(",
|
|
63
|
+
" louiseSaveAction({",
|
|
64
|
+
" ...deps,",
|
|
65
|
+
" collections: {",
|
|
66
|
+
" pages: {",
|
|
67
|
+
" table: pages,",
|
|
68
|
+
' fields: ["title", "seoTitle", "seoDescription"],',
|
|
69
|
+
" },",
|
|
70
|
+
" },",
|
|
71
|
+
" }),",
|
|
72
|
+
" ),",
|
|
73
|
+
"",
|
|
74
|
+
" // The versioned body/sections save — stages a DRAFT, exactly as",
|
|
75
|
+
" // versionsRoute does, through the same `applySaveDraft`.",
|
|
76
|
+
" saveDraft: defineAction(",
|
|
77
|
+
" louiseSaveDraftAction({",
|
|
78
|
+
" ...deps,",
|
|
79
|
+
" table: pages,",
|
|
80
|
+
" versionsTable: pagesVersions,",
|
|
81
|
+
" config: pagesCollection,",
|
|
82
|
+
" // The same KV write-buffer the route uses. Both entrypoints coalesce",
|
|
83
|
+
" // through one buffer, so an autosave burst is one D1 write however",
|
|
84
|
+
" // the client happened to call in.",
|
|
85
|
+
" bufferKv: (e) => e.DRAFTS,",
|
|
86
|
+
" }),",
|
|
87
|
+
" ),",
|
|
88
|
+
"",
|
|
89
|
+
" // The Settings-panel patch (brand, nav, contact, SEO defaults).",
|
|
90
|
+
" settings: defineAction(",
|
|
91
|
+
" louiseSettingsAction({",
|
|
92
|
+
" ...deps,",
|
|
93
|
+
" table: siteSettings,",
|
|
94
|
+
" // The SAME allowlist the generated worker enforces, imported rather",
|
|
95
|
+
" // than copied — a second literal here is a list that drifts from the",
|
|
96
|
+
" // one the routes check against, and nothing would fail when it did.",
|
|
97
|
+
" columns: ASTROID_SETTINGS_COLUMNS,",
|
|
98
|
+
" imageKeys: ASTROID_SETTINGS_IMAGE_KEYS,",
|
|
99
|
+
' mediaBase: astroidConfig.deploy?.mediaBase ?? "/media",',
|
|
100
|
+
" }),",
|
|
101
|
+
" ),",
|
|
102
|
+
" },",
|
|
103
|
+
"};",
|
|
104
|
+
"",
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
|
@@ -12,6 +12,21 @@ export interface GeneratedFile {
|
|
|
12
12
|
* once files (wrangler.jsonc, astro.config, auth.ts) are NOT here by design.
|
|
13
13
|
*/
|
|
14
14
|
export declare function generateAstroidProject(config: AstroidConfig): GeneratedFile[];
|
|
15
|
+
/**
|
|
16
|
+
* The module-secret block `create-astroid` substitutes into `.env.example`.
|
|
17
|
+
*
|
|
18
|
+
* Every name is seeded with the placeholder sentinel rather than left empty,
|
|
19
|
+
* which is the whole trick behind a scaffold that runs with no accounts: the
|
|
20
|
+
* bindings all EXIST and all read as unconfigured, so each module takes its
|
|
21
|
+
* dormant path deliberately instead of hitting an undefined-binding error. The
|
|
22
|
+
* names come from {@link commerceSecretNames}, the same declaration the runtime
|
|
23
|
+
* gate and the generated `env.d.ts` read.
|
|
24
|
+
*
|
|
25
|
+
* Empty string when the project enables no module that needs credentials — the
|
|
26
|
+
* core secrets (session, Turnstile, mail) are already in the template file, with
|
|
27
|
+
* their own prose.
|
|
28
|
+
*/
|
|
29
|
+
export declare function generateAstroidSecretsEnv(config: AstroidConfig): string;
|
|
15
30
|
/**
|
|
16
31
|
* Generate a floor `wrangler.jsonc` from the config: the Worker name + editable
|
|
17
32
|
* bindings a baseline Louise site needs (D1, R2 media, the rate-limit + autosave
|
package/dist/project/generate.js
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
// them (fills real binding ids, secrets, account). `astroid generate` must
|
|
15
15
|
// NEVER clobber them, or it would wipe provisioned ids — so they live in a
|
|
16
16
|
// separate function the regenerate path doesn't call.
|
|
17
|
+
import { ASTROID_VITALS_BINDING, astroidVitalsDataset, } from "../analytics/index.js";
|
|
18
|
+
import { astroidCheckoutVars } from "../commerce/checkout-scaffold.js";
|
|
19
|
+
import { astroidCommerceProviders } from "../commerce/roles.js";
|
|
20
|
+
import { COMMERCE_PROVIDER_SECRETS, COMMERCE_PROVIDER_SETUP, commerceSecretNames, } from "../commerce/secrets.js";
|
|
21
|
+
import { ASTROID_QUEUE_BINDING, astroidCron, astroidCrons, astroidQueueNames, astroidUsesQueues, } from "../queues/messages.js";
|
|
22
|
+
import { ASTROID_EDIT_SESSION_CLASS, ASTROID_REALTIME_BINDING, ASTROID_REALTIME_MIGRATION_TAG, usesRealtime, } from "../realtime/scaffold.js";
|
|
23
|
+
import { ASTROID_SECRET_PLACEHOLDER } from "../secrets.js";
|
|
17
24
|
import { generateAstroidSchema } from "../schema/generate.js";
|
|
18
25
|
import { generateAstroidMiddleware, generateAstroidWorker } from "../worker/generate.js";
|
|
19
26
|
/**
|
|
@@ -29,6 +36,43 @@ export function generateAstroidProject(config) {
|
|
|
29
36
|
{ path: "src/middleware.ts", contents: generateAstroidMiddleware(config) },
|
|
30
37
|
];
|
|
31
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* The module-secret block `create-astroid` substitutes into `.env.example`.
|
|
41
|
+
*
|
|
42
|
+
* Every name is seeded with the placeholder sentinel rather than left empty,
|
|
43
|
+
* which is the whole trick behind a scaffold that runs with no accounts: the
|
|
44
|
+
* bindings all EXIST and all read as unconfigured, so each module takes its
|
|
45
|
+
* dormant path deliberately instead of hitting an undefined-binding error. The
|
|
46
|
+
* names come from {@link commerceSecretNames}, the same declaration the runtime
|
|
47
|
+
* gate and the generated `env.d.ts` read.
|
|
48
|
+
*
|
|
49
|
+
* Empty string when the project enables no module that needs credentials — the
|
|
50
|
+
* core secrets (session, Turnstile, mail) are already in the template file, with
|
|
51
|
+
* their own prose.
|
|
52
|
+
*/
|
|
53
|
+
export function generateAstroidSecretsEnv(config) {
|
|
54
|
+
const providers = astroidCommerceProviders(config.commerce);
|
|
55
|
+
if (providers.length === 0)
|
|
56
|
+
return "";
|
|
57
|
+
const lines = [
|
|
58
|
+
"",
|
|
59
|
+
"# --- commerce -------------------------------------------------------------",
|
|
60
|
+
"#",
|
|
61
|
+
"# Seeded with the DUMMY_REPLACE_ME sentinel, which reads as NOT CONFIGURED.",
|
|
62
|
+
"# Commerce is dormant until every value below is real: the D1 catalog mirror",
|
|
63
|
+
"# still serves whatever it last synced, the webhook receiver answers 503 (so",
|
|
64
|
+
"# the provider retries rather than dropping events), and checkout is",
|
|
65
|
+
"# simulated. Nothing calls the provider with a placeholder credential.",
|
|
66
|
+
];
|
|
67
|
+
for (const provider of providers) {
|
|
68
|
+
const spec = COMMERCE_PROVIDER_SECRETS[provider];
|
|
69
|
+
lines.push("#", `# ${provider}: ${COMMERCE_PROVIDER_SETUP[provider]}`);
|
|
70
|
+
for (const name of [...spec.credentials, spec.webhook]) {
|
|
71
|
+
lines.push(`${name}=${ASTROID_SECRET_PLACEHOLDER}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
32
76
|
// Pinned compatibility date for the emitted Worker. A literal (Astroid's
|
|
33
77
|
// generators are pure — no `Date.now()`), bumped deliberately when the runtime
|
|
34
78
|
// baseline moves; matches the reference site's wrangler.jsonc.
|
|
@@ -73,6 +117,50 @@ export function generateAstroidWrangler(config) {
|
|
|
73
117
|
p(" // No `hosts` in your config → deploys to <name>.workers.dev. Add a");
|
|
74
118
|
p(' // "routes" block with a custom_domain pattern to serve a real domain.');
|
|
75
119
|
}
|
|
120
|
+
if (usesRealtime(config)) {
|
|
121
|
+
// The per-page live editing session (ADR 0002). Two halves, and BOTH are
|
|
122
|
+
// required — a binding with no migration is a deploy error, and the class
|
|
123
|
+
// must also be exported from the worker entry (the generated src/worker.ts
|
|
124
|
+
// re-exports it) or wrangler can't resolve `class_name`.
|
|
125
|
+
p(" // Durable Object: the per-page live editing session (realtime module).");
|
|
126
|
+
p(' "durable_objects": {');
|
|
127
|
+
p(` "bindings": [{ "name": ${JSON.stringify(ASTROID_REALTIME_BINDING)}, "class_name": ${JSON.stringify(ASTROID_EDIT_SESSION_CLASS)} }]`);
|
|
128
|
+
p(" },");
|
|
129
|
+
p(" // A DO class needs a migration tag. `new_sqlite_classes` (NOT");
|
|
130
|
+
p(" // `new_classes`) because the session keeps its authoritative state in");
|
|
131
|
+
p(" // `ctx.storage`, which is the SQLite-backed store — and the storage");
|
|
132
|
+
p(" // backend cannot be changed after the class is first deployed.");
|
|
133
|
+
p(" \"migrations\": [");
|
|
134
|
+
p(` { "tag": ${JSON.stringify(ASTROID_REALTIME_MIGRATION_TAG)}, "new_sqlite_classes": [${JSON.stringify(ASTROID_EDIT_SESSION_CLASS)}] }`);
|
|
135
|
+
p(" ],");
|
|
136
|
+
}
|
|
137
|
+
// Crons. ONE `scheduled` handler receives all of them and tells them apart by
|
|
138
|
+
// `controller.cron`, so this list and the handler's dispatch must agree
|
|
139
|
+
// exactly — both come from `astroidCrons`, which is why it exists.
|
|
140
|
+
//
|
|
141
|
+
// Daily: the site-health scan (broken links, missing alt text, SEO gaps).
|
|
142
|
+
// Hourly (commerce only): the catalog re-sync safety net, so a missed or DLQ'd
|
|
143
|
+
// webhook can only leave the site stale until the next tick.
|
|
144
|
+
p(` "triggers": { "crons": ${JSON.stringify(astroidCrons(config))} },`);
|
|
145
|
+
if (astroidUsesQueues(config)) {
|
|
146
|
+
const { queue, dlq } = astroidQueueNames(config);
|
|
147
|
+
p(" // Provider webhooks are verified at the edge, then enqueued here so the");
|
|
148
|
+
p(" // receiver can return fast. Retries + DLQ routing are Cloudflare's, not");
|
|
149
|
+
p(" // the consumer's — set them here, not in code.");
|
|
150
|
+
p(` // Create both: \`wrangler queues create ${queue}\` and \`… ${dlq}\`.`);
|
|
151
|
+
p(' "queues": {');
|
|
152
|
+
p(` "producers": [{ "queue": ${JSON.stringify(queue)}, "binding": ${JSON.stringify(ASTROID_QUEUE_BINDING)} }],`);
|
|
153
|
+
p(' "consumers": [');
|
|
154
|
+
p(" {");
|
|
155
|
+
p(` "queue": ${JSON.stringify(queue)},`);
|
|
156
|
+
p(` "max_batch_size": ${config.queues?.maxBatchSize ?? 10},`);
|
|
157
|
+
p(` "max_batch_timeout": ${config.queues?.maxBatchTimeout ?? 30},`);
|
|
158
|
+
p(` "max_retries": ${config.queues?.maxRetries ?? 5},`);
|
|
159
|
+
p(` "dead_letter_queue": ${JSON.stringify(dlq)},`);
|
|
160
|
+
p(" },");
|
|
161
|
+
p(" ],");
|
|
162
|
+
p(" },");
|
|
163
|
+
}
|
|
76
164
|
p(" // D1 holds pages / site_settings / media / inquiries (schema in src/schema.ts,");
|
|
77
165
|
p(" // migrations in ./migrations). Create it: `wrangler d1 create <name>`.");
|
|
78
166
|
p(' "d1_databases": [');
|
|
@@ -89,20 +177,74 @@ export function generateAstroidWrangler(config) {
|
|
|
89
177
|
p(" // Cloudflare Images: the media route reads upload dimensions + backs server-");
|
|
90
178
|
p(" // side re-encode. Also @astrojs/cloudflare's production image service.");
|
|
91
179
|
p(' "images": { "binding": "IMAGES" },');
|
|
92
|
-
p(" //
|
|
180
|
+
p(" // Analytics Engine: real-visitor Core Web Vitals. Free, and the ingest");
|
|
181
|
+
p(" // route accepts-and-drops without it, so it costs nothing unused. Reading");
|
|
182
|
+
p(" // the p75 back out needs CF_ACCOUNT_ID + CF_API_TOKEN (see .env.example) —");
|
|
183
|
+
p(" // until those are real the Health badge reads 'not measured yet'.");
|
|
184
|
+
p(` "analytics_engine_datasets": [{ "binding": ${JSON.stringify(ASTROID_VITALS_BINDING)}, "dataset": ${JSON.stringify(astroidVitalsDataset(config))} }],`);
|
|
185
|
+
p(" // Workers AI. Powers the editor's rewrite + SEO-suggest buttons and alt-text");
|
|
186
|
+
p(" // generation on upload — all of which SHIP IN THE EDITOR DRAWER already and,");
|
|
187
|
+
p(" // without this binding, were permanently invisible: their routes answer 503");
|
|
188
|
+
p(" // and the client hides the button. No account setup beyond the binding, and");
|
|
189
|
+
p(" // every call is editor-gated, so a visitor can never spend your AI budget.");
|
|
190
|
+
p(' "ai": { "binding": "AI" },');
|
|
191
|
+
p(" // KV: RL = the security rate limiter (it also holds the daily site-health");
|
|
192
|
+
p(" // summary under its own key — one small singleton blob, not worth a binding");
|
|
193
|
+
p(" // someone has to remember to provision); DRAFTS = the autosave write-buffer.");
|
|
93
194
|
p(" // Create each: `wrangler kv namespace create <RL|DRAFTS>`.");
|
|
94
195
|
p(' "kv_namespaces": [');
|
|
95
196
|
p(' { "binding": "RL", "id": "<run: wrangler kv namespace create RL>" },');
|
|
96
197
|
p(' { "binding": "DRAFTS", "id": "<run: wrangler kv namespace create DRAFTS>" },');
|
|
97
198
|
p(" ],");
|
|
199
|
+
// Email Sending. NOT optional decoration: `src/env.d.ts` declares EMAIL as a
|
|
200
|
+
// required member, and Better Auth's magic-link path console-logs the link in
|
|
201
|
+
// dev but calls `env.EMAIL.send(...)` unconditionally in production. Without
|
|
202
|
+
// this binding that call is a TypeError on a binding that was never created —
|
|
203
|
+
// so sign-in was impossible on every DEPLOYED site, while every local build
|
|
204
|
+
// and every CI scaffold passed. Nothing in this repo runs a deployed scaffold,
|
|
205
|
+
// which is why it survived.
|
|
206
|
+
p(" // Cloudflare Email Sending — magic-link sign-in + inquiry notifications.");
|
|
207
|
+
p(" // Sign-in DEPENDS on this: in production the magic link is emailed, not logged.");
|
|
208
|
+
p(" // Enable Email Sending for your zone, then verify the address in MAIL_FROM.");
|
|
209
|
+
p(' "send_email": [{ "name": "EMAIL" }],');
|
|
98
210
|
p(" // Public base for media URLs; same-origin keeps media self-contained. Read off");
|
|
99
211
|
p(" // the runtime env by the framework-agnostic media route, so it stays a `var`.");
|
|
100
212
|
p(' "vars": {');
|
|
101
213
|
p(` "MEDIA_URL": ${JSON.stringify(mediaBase)},`);
|
|
102
214
|
p(` "SITE_URL": ${JSON.stringify(primaryHost ? `https://${primaryHost}` : `https://${key}.workers.dev`)},`);
|
|
103
|
-
p(
|
|
215
|
+
p(" // The editor allowlist / owner. Wire this into your auth seam (src/auth.ts).");
|
|
104
216
|
p(' "OWNER_EMAIL": "",');
|
|
217
|
+
p(" // Edge caching for published pages (ADR 0004). OFF by default, and the");
|
|
218
|
+
p(" // default is the safe state: with it off every render is `no-store` and");
|
|
219
|
+
p(" // the Worker cache layer stores nothing.");
|
|
220
|
+
p(" //");
|
|
221
|
+
p(" // Turn it on for a PREVIEW deploy first and walk the activation runbook");
|
|
222
|
+
p(" // (docs/adr/0004-edge-caching.md). `caches.default` is not cleared by");
|
|
223
|
+
p(" // Cloudflare Dev Mode or Purge Everything, so a bad prod flip is hard to");
|
|
224
|
+
p(" // undo — this feature was reverted twice for exactly that.");
|
|
225
|
+
p(' "ASTROID_EDGE_CACHE": "false",');
|
|
226
|
+
for (const v of astroidCheckoutVars(config)) {
|
|
227
|
+
// Public, not secret — the app id ships to the browser to mount the card
|
|
228
|
+
// field, and the environment is a choice. Keeping them out of the secret
|
|
229
|
+
// roster also keeps them out of the dormancy gate, which asks whether we can
|
|
230
|
+
// safely CALL Square, not whether a card field can render.
|
|
231
|
+
p(` ${JSON.stringify(v.name)}: ${JSON.stringify(v.value)},`);
|
|
232
|
+
}
|
|
105
233
|
p(" },");
|
|
234
|
+
// Secrets are NOT vars: they belong in .dev.vars locally and in `wrangler
|
|
235
|
+
// secret put` / Secrets Store when deployed. Listing the names here is
|
|
236
|
+
// deliberate — this is the file someone opens when provisioning, and the list
|
|
237
|
+
// is generated from the same declaration the runtime dormancy gate reads.
|
|
238
|
+
const secretNames = commerceSecretNames(config.commerce);
|
|
239
|
+
if (secretNames.length > 0) {
|
|
240
|
+
p(" // Commerce secrets — set OUTSIDE this file (it's committed):");
|
|
241
|
+
p(" // local: .dev.vars (see .env.example, seeded with DUMMY_REPLACE_ME)");
|
|
242
|
+
p(" // deployed: `wrangler secret put <NAME>`, or a Secrets Store binding");
|
|
243
|
+
p(" // Until each is real, commerce stays dormant: the D1 mirror serves, the");
|
|
244
|
+
p(" // webhook receiver answers 503, and nothing calls the provider.");
|
|
245
|
+
for (const name of secretNames)
|
|
246
|
+
p(` // ${name}`);
|
|
247
|
+
}
|
|
106
248
|
p(' "observability": { "enabled": true },');
|
|
107
249
|
p("}");
|
|
108
250
|
p();
|
package/dist/project/index.d.ts
CHANGED
package/dist/project/index.js
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AstroidConfig } from "../config.js";
|
|
2
|
+
/**
|
|
3
|
+
* A file written once, then owned by the project.
|
|
4
|
+
*
|
|
5
|
+
* `apply` is the whole contract. `"skip"` (the default) leaves an existing file
|
|
6
|
+
* alone. `"append-once"` is for the files a project accumulates into rather than
|
|
7
|
+
* owns outright — `public/_headers` gets a stanza per module, and a second
|
|
8
|
+
* module must not erase the first one's.
|
|
9
|
+
*/
|
|
10
|
+
export interface ScaffoldFile {
|
|
11
|
+
/** Path relative to the project root, POSIX-separated. */
|
|
12
|
+
path: string;
|
|
13
|
+
contents: string;
|
|
14
|
+
/** What to do when the path already exists. Default `"skip"`. */
|
|
15
|
+
apply?: "skip" | "append-once";
|
|
16
|
+
/**
|
|
17
|
+
* For `"append-once"`: a substring that proves this stanza is already there.
|
|
18
|
+
* Without it a re-run would append a duplicate every time.
|
|
19
|
+
*/
|
|
20
|
+
marker?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Every scaffold-once file this config implies.
|
|
24
|
+
*
|
|
25
|
+
* Ordered by module so a `generate` that writes several prints them in a stable
|
|
26
|
+
* sequence. Returns `[]` for a plain marketing site with no modules — the
|
|
27
|
+
* baseline floor is entirely the regenerated trio plus the static template.
|
|
28
|
+
*/
|
|
29
|
+
export declare function generateAstroidScaffoldFiles(config: AstroidConfig): ScaffoldFile[];
|
|
@@ -0,0 +1,140 @@
|
|
|
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 { generateAstroidPortalAuth, generateAstroidPortalAuthRoute } from "../portal/scaffold.js";
|
|
34
|
+
import { generateAstroidEditSession } from "../realtime/scaffold.js";
|
|
35
|
+
import { generatePwaHeaders, generateServiceWorker, generateWebManifest } from "../pwa/generate.js";
|
|
36
|
+
import { astroidUsesQueues } from "../queues/messages.js";
|
|
37
|
+
import { generateAstroidQueueSeam, generateAstroidWebhookRoutes } from "../queues/scaffold.js";
|
|
38
|
+
/**
|
|
39
|
+
* Every scaffold-once file this config implies.
|
|
40
|
+
*
|
|
41
|
+
* Ordered by module so a `generate` that writes several prints them in a stable
|
|
42
|
+
* sequence. Returns `[]` for a plain marketing site with no modules — the
|
|
43
|
+
* baseline floor is entirely the regenerated trio plus the static template.
|
|
44
|
+
*/
|
|
45
|
+
export function generateAstroidScaffoldFiles(config) {
|
|
46
|
+
const files = [];
|
|
47
|
+
// --- commerce: the catalog table's migration ------------------------------
|
|
48
|
+
// Numbered 0003 so it lands after the template's 0000_content and the auth
|
|
49
|
+
// pair (0001, 0002) that `create-astroid` writes. Without it `--commerce`
|
|
50
|
+
// scaffolded a `products` table into src/schema.ts that no migration ever
|
|
51
|
+
// created, and the first sync wrote nothing while reporting success.
|
|
52
|
+
const catalogSql = generateCatalogMigrationSql(config);
|
|
53
|
+
if (catalogSql)
|
|
54
|
+
files.push({ path: "migrations/0003_catalog.sql", contents: catalogSql });
|
|
55
|
+
// --- the CWV beacon -------------------------------------------------------
|
|
56
|
+
// A static file under public/, so it is same-origin and covered by
|
|
57
|
+
// `script-src 'self'` — an inline script carrying generated content could not
|
|
58
|
+
// be hashed into the CSP and would be blocked.
|
|
59
|
+
const beacon = generateAstroidVitalsBeacon(config, cwvBeaconScript());
|
|
60
|
+
files.push({ path: beacon.path, contents: beacon.contents });
|
|
61
|
+
// --- the typed Astro Actions surface --------------------------------------
|
|
62
|
+
// Always: every project has editable pages, and the routes alone leave the
|
|
63
|
+
// Astro-native half of ADR 0001 unbuilt. Scaffold-once because it is meant to
|
|
64
|
+
// be added to.
|
|
65
|
+
files.push({ path: "src/actions/index.ts", contents: generateAstroidActions(config) });
|
|
66
|
+
// --- commerce: the server-authoritative payment seam ----------------------
|
|
67
|
+
// Scaffold-once: a real store adds shipping, tax, an order row, a receipt.
|
|
68
|
+
// What's fixed is the sequence that keeps a charge correct.
|
|
69
|
+
const checkoutRoute = generateAstroidCheckoutRoute(config);
|
|
70
|
+
if (checkoutRoute)
|
|
71
|
+
files.push({ path: "src/pages/api/checkout.ts", contents: checkoutRoute });
|
|
72
|
+
const squareCard = generateAstroidSquareCard(config);
|
|
73
|
+
if (squareCard)
|
|
74
|
+
files.push({ path: "src/components/SquareCard.astro", contents: squareCard });
|
|
75
|
+
// --- queues: the consumer seam + one receiver per commerce provider --------
|
|
76
|
+
// Both exist to be edited (what a refresh means; which events matter), which
|
|
77
|
+
// is why `astroid generate` must never rewrite them.
|
|
78
|
+
if (astroidUsesQueues(config)) {
|
|
79
|
+
files.push({ path: "src/queue.ts", contents: generateAstroidQueueSeam(config) });
|
|
80
|
+
// One receiver per provider — a site can run two (invoicing + storefront).
|
|
81
|
+
for (const route of generateAstroidWebhookRoutes(config)) {
|
|
82
|
+
files.push({ path: route.path, contents: route.contents });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// --- portfolio: the gallery page -----------------------------------------
|
|
86
|
+
// "Which assets appear, in what order" is the first thing a portfolio changes.
|
|
87
|
+
const gallery = generateAstroidGalleryPage(config);
|
|
88
|
+
if (gallery)
|
|
89
|
+
files.push({ path: "src/pages/work.astro", contents: gallery });
|
|
90
|
+
// --- pwa: the service worker, manifest, and its headers -------------------
|
|
91
|
+
// Static files under public/, not generated source — a service worker is not
|
|
92
|
+
// bundled, and `_headers` is shared with whatever else writes to it.
|
|
93
|
+
const sw = generateServiceWorker(config);
|
|
94
|
+
if (sw) {
|
|
95
|
+
files.push({ path: "public/sw.js", contents: sw });
|
|
96
|
+
const manifest = generateWebManifest(config);
|
|
97
|
+
if (manifest)
|
|
98
|
+
files.push({ path: "public/manifest.webmanifest", contents: manifest });
|
|
99
|
+
const headers = generatePwaHeaders(config);
|
|
100
|
+
if (headers) {
|
|
101
|
+
files.push({
|
|
102
|
+
path: "public/_headers",
|
|
103
|
+
contents: headers,
|
|
104
|
+
apply: "append-once",
|
|
105
|
+
// The service-worker path is the one token this stanza always contains
|
|
106
|
+
// and nothing else in a `_headers` file would.
|
|
107
|
+
marker: "/sw.js",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// --- map: the tile route + embed component --------------------------------
|
|
112
|
+
// Generated into the project rather than shipped in astroidjs so maplibre-gl
|
|
113
|
+
// stays a dependency of the projects that actually draw a map.
|
|
114
|
+
const tileRoute = generateMapTileRoute(config);
|
|
115
|
+
if (tileRoute)
|
|
116
|
+
files.push({ path: "src/pages/map/basemap.pmtiles.ts", contents: tileRoute });
|
|
117
|
+
const mapEmbed = generateMapEmbedComponent(config);
|
|
118
|
+
if (mapEmbed)
|
|
119
|
+
files.push({ path: "src/components/MapEmbed.astro", contents: mapEmbed });
|
|
120
|
+
// --- realtime: the per-page edit-session Durable Object -------------------
|
|
121
|
+
// Scaffold-once because it must import `cloudflare:workers` (runtime-only) and
|
|
122
|
+
// because `persist` is the seam a project tunes.
|
|
123
|
+
const editSession = generateAstroidEditSession(config);
|
|
124
|
+
if (editSession)
|
|
125
|
+
files.push({ path: "src/edit-session.ts", contents: editSession });
|
|
126
|
+
// --- portal: the second auth instance + its mounted catch-all -------------
|
|
127
|
+
// A site edits the reset email and the role a new account gets, but not the
|
|
128
|
+
// mount/cookie/table prefixes that keep the two instances isolated.
|
|
129
|
+
const portalAuth = generateAstroidPortalAuth(config);
|
|
130
|
+
if (portalAuth) {
|
|
131
|
+
files.push({ path: "src/portal-auth.ts", contents: portalAuth });
|
|
132
|
+
const route = generateAstroidPortalAuthRoute(config);
|
|
133
|
+
// Always non-null alongside portalAuth (same `astroidPortal` gate), but the
|
|
134
|
+
// types don't know that and a silent drop here is a portal that cannot
|
|
135
|
+
// authenticate — so assert it rather than `?.`-ing it away.
|
|
136
|
+
if (route)
|
|
137
|
+
files.push({ path: "src/pages/api/portal-auth/[...all].ts", contents: route });
|
|
138
|
+
}
|
|
139
|
+
return files;
|
|
140
|
+
}
|
|
@@ -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;
|