astroidjs 0.12.1 → 0.14.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.
Files changed (136) hide show
  1. package/README.md +42 -42
  2. package/bin/astroid.mjs +35 -29
  3. package/dist/analytics/index.d.ts +3 -3
  4. package/dist/analytics/index.js +11 -11
  5. package/dist/astro/csp.d.ts +5 -5
  6. package/dist/astro/csp.js +6 -6
  7. package/dist/astro/index.js +1 -1
  8. package/dist/auth/index.d.ts +2 -2
  9. package/dist/auth/index.js +5 -5
  10. package/dist/commerce/adapters.d.ts +6 -6
  11. package/dist/commerce/adapters.js +9 -9
  12. package/dist/commerce/checkout-scaffold.d.ts +5 -5
  13. package/dist/commerce/checkout-scaffold.js +27 -27
  14. package/dist/commerce/checkout.d.ts +12 -12
  15. package/dist/commerce/checkout.js +7 -7
  16. package/dist/commerce/loader.d.ts +2 -2
  17. package/dist/commerce/loader.js +3 -3
  18. package/dist/commerce/mirror.d.ts +4 -4
  19. package/dist/commerce/mirror.js +12 -12
  20. package/dist/commerce/roles.d.ts +10 -10
  21. package/dist/commerce/roles.js +13 -13
  22. package/dist/commerce/secrets.d.ts +9 -9
  23. package/dist/commerce/secrets.js +9 -9
  24. package/dist/commerce/sync.d.ts +7 -7
  25. package/dist/commerce/sync.js +5 -5
  26. package/dist/components/sections.d.ts +9 -9
  27. package/dist/components/sections.js +12 -12
  28. package/dist/config.d.ts +89 -62
  29. package/dist/config.js +18 -18
  30. package/dist/email/inquiry.d.ts +2 -2
  31. package/dist/email/inquiry.js +1 -1
  32. package/dist/email/send.d.ts +4 -4
  33. package/dist/email/send.js +7 -7
  34. package/dist/email/templates.js +3 -3
  35. package/dist/email/theme.d.ts +1 -1
  36. package/dist/email/theme.js +4 -4
  37. package/dist/errors.d.ts +1 -1
  38. package/dist/errors.js +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/map/pmtiles.d.ts +5 -5
  41. package/dist/map/pmtiles.js +5 -5
  42. package/dist/map/scaffold.d.ts +2 -2
  43. package/dist/map/scaffold.js +8 -8
  44. package/dist/map/style.d.ts +4 -4
  45. package/dist/map/style.js +1 -1
  46. package/dist/portal/config.d.ts +3 -3
  47. package/dist/portal/config.js +11 -10
  48. package/dist/portal/guard.d.ts +4 -4
  49. package/dist/portal/guard.js +4 -4
  50. package/dist/portal/nav.js +2 -2
  51. package/dist/portal/scaffold.d.ts +4 -4
  52. package/dist/portal/scaffold.js +13 -13
  53. package/dist/portal/session.d.ts +11 -5
  54. package/dist/portal/session.js +10 -5
  55. package/dist/portfolio/scaffold.d.ts +1 -1
  56. package/dist/portfolio/scaffold.js +8 -8
  57. package/dist/project/actions.d.ts +1 -1
  58. package/dist/project/actions.js +18 -12
  59. package/dist/project/generate.d.ts +4 -4
  60. package/dist/project/generate.js +26 -26
  61. package/dist/project/index.d.ts +1 -0
  62. package/dist/project/index.js +2 -1
  63. package/dist/project/scaffold.d.ts +2 -2
  64. package/dist/project/scaffold.js +69 -13
  65. package/dist/project/seed.d.ts +22 -0
  66. package/dist/project/seed.js +214 -0
  67. package/dist/pwa/generate.d.ts +11 -11
  68. package/dist/pwa/generate.js +19 -19
  69. package/dist/queues/consumer.d.ts +3 -3
  70. package/dist/queues/consumer.js +2 -2
  71. package/dist/queues/messages.d.ts +4 -4
  72. package/dist/queues/messages.js +2 -2
  73. package/dist/queues/scaffold.d.ts +4 -4
  74. package/dist/queues/scaffold.js +17 -17
  75. package/dist/queues/webhook.d.ts +5 -5
  76. package/dist/queues/webhook.js +3 -3
  77. package/dist/realtime/scaffold.d.ts +4 -4
  78. package/dist/realtime/scaffold.js +12 -12
  79. package/dist/schema/collections.d.ts +42 -11
  80. package/dist/schema/collections.js +43 -42
  81. package/dist/schema/framework.d.ts +1 -1
  82. package/dist/schema/framework.js +2 -2
  83. package/dist/schema/generate.js +6 -6
  84. package/dist/schema/index.js +1 -1
  85. package/dist/secrets.d.ts +6 -6
  86. package/dist/secrets.js +6 -6
  87. package/dist/security/csp-origins.d.ts +1 -1
  88. package/dist/security/csp-origins.js +3 -3
  89. package/dist/security/rate-rules.d.ts +2 -2
  90. package/dist/security/rate-rules.js +8 -8
  91. package/dist/seo/resolve.d.ts +5 -5
  92. package/dist/seo/resolve.js +2 -2
  93. package/dist/seo/routes.d.ts +5 -5
  94. package/dist/seo/routes.js +3 -3
  95. package/dist/seo/structured-data.d.ts +6 -6
  96. package/dist/seo/structured-data.js +7 -7
  97. package/dist/status.d.ts +5 -5
  98. package/dist/status.js +7 -7
  99. package/dist/tenancy/index.d.ts +3 -3
  100. package/dist/tenancy/index.js +11 -11
  101. package/dist/worker/generate.d.ts +2 -2
  102. package/dist/worker/generate.js +91 -57
  103. package/dist/worker/index.js +1 -1
  104. package/dist/worker/routes.js +11 -11
  105. package/dist/workflow/advance.d.ts +3 -3
  106. package/dist/workflow/advance.js +6 -6
  107. package/dist/workflow/config.d.ts +4 -4
  108. package/dist/workflow/config.js +4 -4
  109. package/dist/workflow/generate.d.ts +2 -2
  110. package/dist/workflow/generate.js +11 -11
  111. package/package.json +3 -4
  112. package/src/components/Collection.tsx +5 -5
  113. package/src/components/Editable.astro +9 -9
  114. package/src/components/JustifiedGallery.astro +8 -8
  115. package/src/components/MediaSlot.astro +12 -12
  116. package/src/components/PortalShell.astro +4 -4
  117. package/src/components/RegisterSW.astro +3 -3
  118. package/src/components/Section.astro +8 -8
  119. package/src/components/Sections.astro +6 -6
  120. package/src/components/Seo.astro +3 -3
  121. package/src/components/StageBar.astro +3 -3
  122. package/src/components/StructuredData.astro +2 -2
  123. package/src/components/justify.ts +9 -9
  124. package/src/components/media-meta.ts +10 -10
  125. package/src/components/sections/AboutIntro.astro +1 -1
  126. package/src/components/sections/Contact.astro +1 -1
  127. package/src/components/sections/Cta.astro +1 -1
  128. package/src/components/sections/Faq.astro +1 -1
  129. package/src/components/sections/FeatureGrid.astro +2 -2
  130. package/src/components/sections/Hero.astro +1 -1
  131. package/src/components/sections/PricingTiers.astro +1 -1
  132. package/src/components/sections/ProductGrid.astro +1 -1
  133. package/src/components/sections/SplitImage.astro +1 -1
  134. package/src/components/sections/Steps.astro +1 -1
  135. package/src/components/sections/Testimonial.astro +1 -1
  136. package/src/components/sections.ts +17 -17
@@ -3,15 +3,15 @@
3
3
  // The portal's SCAFFOLD-ONCE pieces: the second Better Auth instance, and the
4
4
  // `App.Locals` / `CloudflareEnv` additions that come with it.
5
5
  //
6
- // The auth instance is scaffolded rather than generated because a site edits it
7
- // — the reset email, the role a new account gets, extra user columns. What
6
+ // The auth instance is scaffolded rather than generated because a site edits it—the
7
+ // reset email, the role a new account gets, extra user columns. What
8
8
  // Astroid fixes are the three things that must not drift: the mount, the cookie
9
9
  // prefix, and the table prefix. Get any of those wrong and the two instances
10
10
  // fight over one origin's cookies, which fails intermittently and looks like a
11
11
  // session bug rather than a configuration one.
12
12
  import { astroidPortal } from "./config.js";
13
13
  /**
14
- * `src/portal-auth.ts` — the portal Better Auth instance and its session
14
+ * `src/portal-auth.ts`—the portal Better Auth instance and its session
15
15
  * resolver.
16
16
  *
17
17
  * Returns null when the project has no portal.
@@ -21,13 +21,13 @@ export function generateAstroidPortalAuth(config) {
21
21
  if (!portal)
22
22
  return null;
23
23
  return [
24
- "// The PORTAL auth instance — customers/members, separate from the editor.",
24
+ "// The PORTAL auth instance—customers/members, separate from the editor.",
25
25
  "//",
26
26
  "// Scaffolded once; yours to edit (the reset email, extra user columns, what",
27
27
  "// role a new account gets). Three things should NOT change: the basePath,",
28
28
  "// the cookiePrefix, and the tablePrefix. The studio instance keeps Better",
29
29
  "// Auth's defaults because the Louise editor client hardcodes them, so this",
30
- "// one moves — and if the two ever share a cookie prefix, signing into one",
30
+ "// one moves—and if the two ever share a cookie prefix, signing into one",
31
31
  "// silently signs you out of the other.",
32
32
  'import { astroidMailTheme, magicLinkEmail, passwordResetEmail, resolveMailer, sendTransactional } from "astroidjs";',
33
33
  'import { env } from "cloudflare:workers";',
@@ -41,7 +41,7 @@ export function generateAstroidPortalAuth(config) {
41
41
  " return getLouiseAuth(env, new URL(request.url).origin, {",
42
42
  " rpName: astroidConfig.theme.name,",
43
43
  " mailFrom: { email: env.MAIL_FROM, name: astroidConfig.theme.name },",
44
- " // The portal never sends magic links — it's email + password — but the",
44
+ " // The portal never sends magic links—it's email + password—but the",
45
45
  " // toolkit's config asks for a renderer, so give it the real one.",
46
46
  " renderMagicLinkEmail: ({ url, toEmail }) => magicLinkEmail(MAIL_THEME, { url, toEmail }),",
47
47
  ` basePath: ${JSON.stringify(portal.basePath)},`,
@@ -51,14 +51,14 @@ export function generateAstroidPortalAuth(config) {
51
51
  " minPasswordLength: 8,",
52
52
  portal.signUp
53
53
  ? " // Public sign-up is ON for this project."
54
- : " // Accounts are provisioned by staff — no public sign-up.",
54
+ : " // Accounts are provisioned by staff—no public sign-up.",
55
55
  ` disableSignUp: ${!portal.signUp},`,
56
56
  " sendResetPassword: async ({ user, url }) => {",
57
57
  " // Through `resolveMailer`, NOT a hand-built options object: it is the",
58
58
  " // only thing that applies the DUMMY_REPLACE_ME sentinel check. Built by",
59
59
  " // hand, a fresh deploy with a real EMAIL binding but a placeholder",
60
60
  " // MAIL_FROM read as configured and called the Email API with an envelope",
61
- ' // sender of literally "DUMMY_REPLACE_ME" — rejected upstream, swallowed',
61
+ ' // sender of literally "DUMMY_REPLACE_ME"—rejected upstream, swallowed',
62
62
  " // here, and reported to the user as a reset email that was sent.",
63
63
  " const mailer = await resolveMailer(env);",
64
64
  " await sendTransactional(mailer, [",
@@ -66,7 +66,7 @@ export function generateAstroidPortalAuth(config) {
66
66
  " ]);",
67
67
  " },",
68
68
  " },",
69
- " // The portal has its own users — never the editor allowlist.",
69
+ " // The portal has its own users—never the editor allowlist.",
70
70
  " resolveAdmins: () => [],",
71
71
  " });",
72
72
  "}",
@@ -97,14 +97,14 @@ export function generateAstroidPortalAuth(config) {
97
97
  ].join("\n");
98
98
  }
99
99
  /**
100
- * `src/pages/api/portal-auth/[...all].ts` — the portal's Better Auth catch-all,
100
+ * `src/pages/api/portal-auth/[...all].ts`—the portal's Better Auth catch-all,
101
101
  * mounted at its own basePath so it never collides with the studio's
102
102
  * `/api/auth`.
103
103
  *
104
104
  * Lives here rather than as a literal in `create-astroid` for the same reason
105
105
  * the archetype sections moved (#277): the scaffolder is plain JS, so a drifted
106
106
  * import path there is invisible until a user's build fails. It is also the half
107
- * `generateAstroidPortalAuth` is useless without — `src/portal-auth.ts` exports
107
+ * `generateAstroidPortalAuth` is useless without—`src/portal-auth.ts` exports
108
108
  * `handlePortalAuth`, and nothing calls it unless this route exists.
109
109
  *
110
110
  * Returns null when the project has no portal.
@@ -126,14 +126,14 @@ export function generateAstroidPortalAuthRoute(config) {
126
126
  }
127
127
  /**
128
128
  * The `App.Locals` member the portal adds, as a block `create-astroid`
129
- * substitutes into `src/env.d.ts`. Empty without a portal — a project that
129
+ * substitutes into `src/env.d.ts`. Empty without a portal—a project that
130
130
  * types `portalUser` it never sets is inviting a null-check nobody needs.
131
131
  */
132
132
  export function generateAstroidPortalLocals(config) {
133
133
  if (!astroidPortal(config))
134
134
  return "";
135
135
  return [
136
- " /** The signed-in PORTAL user (customers/members) — distinct from",
136
+ " /** The signed-in PORTAL user (customers/members)—distinct from",
137
137
  " * `editor`, which is the studio session. Null when signed out. */",
138
138
  ' portalUser: import("astroidjs").PortalUser | null;',
139
139
  ].join("\n");
@@ -1,14 +1,20 @@
1
1
  import type { PortalUser } from "./guard.js";
2
- /** Resolves the portal user for a request, or null when signed out. */
3
- export type PortalSessionResolver = (request: Request) => Promise<PortalUser | null>;
2
+ /** Resolves the portal user for a request, or null when signed out. A site's
3
+ * resolver can return its own richer user type; see {@link resolvePortalSession}. */
4
+ export type PortalSessionResolver<U extends PortalUser = PortalUser> = (request: Request) => Promise<U | null>;
4
5
  /**
5
6
  * Resolve the portal session at most once per request.
6
7
  *
7
8
  * Shares the *promise*, not the result, so two callers racing during the same
8
9
  * request both await one lookup rather than starting a second.
10
+ *
11
+ * Generic over the site's user type, so the result keeps whatever the site's
12
+ * resolver returns (a customer ID, display initials) instead of narrowing to
13
+ * `PortalUser`. Every caller in a request passes the same resolver, so the
14
+ * shared promise always holds that type.
9
15
  */
10
- export declare function resolvePortalSession(request: Request, resolve: PortalSessionResolver): Promise<PortalUser | null>;
11
- /** JSON response helper — the shape every portal API route returns. */
16
+ export declare function resolvePortalSession<U extends PortalUser = PortalUser>(request: Request, resolve: PortalSessionResolver<U>): Promise<U | null>;
17
+ /** JSON response helper—the shape every portal API route returns. */
12
18
  export declare function json(body: unknown, status?: number, headers?: Record<string, string>): Response;
13
19
  /** True when the request came from this same origin. */
14
20
  export declare function isSameOrigin(request: Request): boolean;
@@ -20,7 +26,7 @@ export type CustomerGuardResult = {
20
26
  response: Response;
21
27
  };
22
28
  /**
23
- * Guard a portal API handler: a signed-in user, and — on mutations — a
29
+ * Guard a portal API handler: a signed-in user, and—on mutations—a
24
30
  * same-origin request.
25
31
  *
26
32
  * ```ts
@@ -5,12 +5,12 @@
5
5
  // The middleware resolves it (to gate routes) and so does whatever handler runs
6
6
  // next (to know who's asking). Both hitting the session store is a wasted D1
7
7
  // round-trip on every authenticated request, so the in-flight promise is shared
8
- // per request via a `WeakMap` — keyed on the `Request`, which means entries
8
+ // per request via a `WeakMap`—keyed on the `Request`, which means entries
9
9
  // disappear with the request rather than needing eviction.
10
10
  //
11
11
  // `requireCustomer` then adds the check a session alone doesn't give you:
12
12
  // same-origin on mutations. A cookie is attached by the browser to any request
13
- // to this origin, including one a third-party page triggered — so a session
13
+ // to this origin, including one a third-party page triggered—so a session
14
14
  // proves identity, and the origin check proves intent.
15
15
  const inFlight = new WeakMap();
16
16
  /**
@@ -18,6 +18,11 @@ const inFlight = new WeakMap();
18
18
  *
19
19
  * Shares the *promise*, not the result, so two callers racing during the same
20
20
  * request both await one lookup rather than starting a second.
21
+ *
22
+ * Generic over the site's user type, so the result keeps whatever the site's
23
+ * resolver returns (a customer ID, display initials) instead of narrowing to
24
+ * `PortalUser`. Every caller in a request passes the same resolver, so the
25
+ * shared promise always holds that type.
21
26
  */
22
27
  export function resolvePortalSession(request, resolve) {
23
28
  const existing = inFlight.get(request);
@@ -29,7 +34,7 @@ export function resolvePortalSession(request, resolve) {
29
34
  inFlight.set(request, promise);
30
35
  return promise;
31
36
  }
32
- /** JSON response helper — the shape every portal API route returns. */
37
+ /** JSON response helper—the shape every portal API route returns. */
33
38
  export function json(body, status = 200, headers = {}) {
34
39
  return new Response(JSON.stringify(body), {
35
40
  status,
@@ -46,7 +51,7 @@ export function isSameOrigin(request) {
46
51
  return origin === target;
47
52
  // No Origin header: browsers always send one on cross-origin mutations, so
48
53
  // its absence means a same-origin or non-browser caller. Fall back to Referer
49
- // when present, and allow otherwise — being stricter would break legitimate
54
+ // when present, and allow otherwise—being stricter would break legitimate
50
55
  // server-to-server callers without stopping a real CSRF, which always carries
51
56
  // an Origin.
52
57
  const referer = request.headers.get("referer");
@@ -61,7 +66,7 @@ export function isSameOrigin(request) {
61
66
  return true;
62
67
  }
63
68
  /**
64
- * Guard a portal API handler: a signed-in user, and — on mutations — a
69
+ * Guard a portal API handler: a signed-in user, and—on mutations—a
65
70
  * same-origin request.
66
71
  *
67
72
  * ```ts
@@ -1,6 +1,6 @@
1
1
  import type { AstroidConfig } from "../config.js";
2
2
  /**
3
- * `src/pages/work.astro` — the portfolio gallery. Null for any other archetype.
3
+ * `src/pages/work.astro`—the portfolio gallery. Null for any other archetype.
4
4
  *
5
5
  * Intrinsic `width`/`height` are carried through deliberately: they feed the
6
6
  * pre-decode layout, so a library with dimensions recorded lays out correctly on
@@ -3,17 +3,17 @@
3
3
  // The `portfolio` archetype's scaffold-once page: a justified gallery over the
4
4
  // media library.
5
5
  //
6
- // Scaffold-once, not regenerated, for the usual reason — this is the first file
6
+ // Scaffold-once, not regenerated, for the usual reason—this is the first file
7
7
  // a portfolio site edits (which assets appear, in what order, whether tiles link
8
8
  // to a detail page), so `astroid generate` must never rewrite it.
9
9
  //
10
10
  // It exists because the primitives alone don't finish the job. `<MediaSlot>` and
11
11
  // `<JustifiedGallery>` are archetype-agnostic, but the wiring between them and
12
- // the media registry — the public URL shape, filtering to images, carrying
12
+ // the media registry—the public URL shape, filtering to images, carrying
13
13
  // alt/caption and intrinsic dimensions through so the first paint isn't a guess
14
- // — is identical every time, and is exactly what the consuming sites hand-wrote.
14
+ //—is identical every time, and is exactly what the consuming sites hand-wrote.
15
15
  /**
16
- * `src/pages/work.astro` — the portfolio gallery. Null for any other archetype.
16
+ * `src/pages/work.astro`—the portfolio gallery. Null for any other archetype.
17
17
  *
18
18
  * Intrinsic `width`/`height` are carried through deliberately: they feed the
19
19
  * pre-decode layout, so a library with dimensions recorded lays out correctly on
@@ -25,7 +25,7 @@ export function generateAstroidGalleryPage(config) {
25
25
  const mediaBase = config.deploy?.mediaBase ?? "/media";
26
26
  return [
27
27
  "---",
28
- "// The work gallery — a justified grid over the media library.",
28
+ "// The work gallery—a justified grid over the media library.",
29
29
  "//",
30
30
  "// Scaffolded once; yours to edit. The layout primitive is general",
31
31
  "// (astroidjs/components/JustifiedGallery.astro); what lives here is this",
@@ -33,8 +33,8 @@ export function generateAstroidGalleryPage(config) {
33
33
  "//",
34
34
  "// Rows carry `alt`/`caption` from the media registry, so an editor fixes alt",
35
35
  "// text once in the library and every gallery showing that asset picks it up.",
36
- "// Assets missing width/height still render — the client corrects the layout",
37
- "// once the image decodes — but they cost a visible reflow, so it's worth",
36
+ "// Assets missing width/height still render—the client corrects the layout",
37
+ "// once the image decodes—but they cost a visible reflow, so it's worth",
38
38
  "// backfilling dimensions on older uploads.",
39
39
  'import JustifiedGallery from "astroidjs/components/JustifiedGallery.astro";',
40
40
  'import type { GalleryItem } from "astroidjs/components/justify";',
@@ -59,7 +59,7 @@ export function generateAstroidGalleryPage(config) {
59
59
  " ).all<MediaRow>();",
60
60
  " rows = result.results ?? [];",
61
61
  "} catch {",
62
- " // No DB binding yet (pre-provision) — render the empty state.",
62
+ " // No DB binding yet (pre-provision)—render the empty state.",
63
63
  "}",
64
64
  "",
65
65
  "const items: GalleryItem[] = rows.map((row) => ({",
@@ -1,3 +1,3 @@
1
1
  import type { AstroidConfig } from "../config.js";
2
- /** `src/actions/index.ts` — the typed mutation surface, scaffolded once. */
2
+ /** `src/actions/index.ts`—the typed mutation surface, scaffolded once. */
3
3
  export declare function generateAstroidActions(config: AstroidConfig): string;
@@ -1,21 +1,21 @@
1
1
  // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
2
  //
3
- // `src/actions/index.ts` — the Astro-native, typed mutation surface (ADR 0001
3
+ // `src/actions/index.ts`—the Astro-native, typed mutation surface (ADR 0001
4
4
  // layer 2), beside the framework-agnostic `/api/louise/*` routes.
5
5
  //
6
6
  // Astroid generated only the route half. That is not a missing convenience: the
7
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
8
+ // exposes these factories is that each one shares the raw route's store
9
+ // path—`applyFieldSave`, `applySettingsPatch`, `applySaveDraft`. A project that wired
10
10
  // its own Actions by hand would get a second write path, and a second write path
11
11
  // is where validation, sanitization, and draft-merge semantics drift apart
12
12
  // silently (#138).
13
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
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
16
  // pre-wired against the same tables and the same collection config the generated
17
17
  // worker uses.
18
- /** `src/actions/index.ts` — the typed mutation surface, scaffolded once. */
18
+ /** `src/actions/index.ts`—the typed mutation surface, scaffolded once. */
19
19
  export function generateAstroidActions(config) {
20
20
  const customKeys = config.settings?.customKeys ?? [];
21
21
  const extraImageKeys = config.settings?.imageKeys ?? [];
@@ -29,12 +29,15 @@ export function generateAstroidActions(config) {
29
29
  ? ` columns: ${JSON.stringify(columnsOverride)},`
30
30
  : " columns: ASTROID_SETTINGS_COLUMNS,",
31
31
  ...(customKeys.length ? [` customKeys: ${JSON.stringify(customKeys)},`] : []),
32
+ // The same sanitizers the generated settingsRoute spreads in. An Action
33
+ // writes no GET, so `read` has nothing to do here.
34
+ ...(config.settings?.hooks ? [" sanitize: settingsHooks.sanitize,"] : []),
32
35
  extraImageKeys.length
33
36
  ? ` imageKeys: [...ASTROID_SETTINGS_IMAGE_KEYS, ...${JSON.stringify(extraImageKeys)}],`
34
37
  : " imageKeys: ASTROID_SETTINGS_IMAGE_KEYS,",
35
38
  ];
36
39
  return [
37
- "// The typed Astro Actions surface — ADR 0001 layer 2.",
40
+ "// The typed Astro Actions surface—ADR 0001 layer 2.",
38
41
  "//",
39
42
  "// Scaffolded once and yours to ADD to: put your own `defineAction`s in the",
40
43
  "// `server` object below, alongside these.",
@@ -54,17 +57,20 @@ export function generateAstroidActions(config) {
54
57
  " louiseSettingsAction,",
55
58
  '} from "@louise-toolkit/astro";',
56
59
  "import {",
57
- " ASTROID_SETTINGS_COLUMNS,",
60
+ // Only when the Action uses it: a site that overrides the columns gets them
61
+ // as a literal, and an unused import is a lint error in its own file.
62
+ ...(columnsOverride ? [] : [" ASTROID_SETTINGS_COLUMNS,"]),
58
63
  " ASTROID_SETTINGS_IMAGE_KEYS,",
59
64
  " astroidPagesCollection,",
60
65
  '} from "astroidjs";',
61
66
  'import astroidConfig from "../../astroid.config.js";',
62
67
  'import { pages, pagesVersions, siteSettings } from "../schema.js";',
68
+ ...(config.settings?.hooks ? ['import { settingsHooks } from "../settings-hooks.js";'] : []),
63
69
  "",
64
70
  "const pagesCollection = astroidPagesCollection(astroidConfig);",
65
71
  "",
66
72
  "// Astro v6+ removed `Astro.locals.runtime.env`, so the bindings are resolved",
67
- "// from `cloudflare:workers` — the same env the raw routes read.",
73
+ "// from `cloudflare:workers`—the same env the raw routes read.",
68
74
  "const getEnv = () => env as unknown as CloudflareEnv;",
69
75
  "",
70
76
  "// `getEditor` is left to its default (`locals.editor`), which the generated",
@@ -74,7 +80,7 @@ export function generateAstroidActions(config) {
74
80
  "",
75
81
  "export const server = {",
76
82
  " louise: {",
77
- " // Inline field save (title, SEO) — the live, non-versioned path.",
83
+ " // Inline field save (title, SEO)—the live, non-versioned path.",
78
84
  " save: defineAction(",
79
85
  " louiseSaveAction({",
80
86
  " ...deps,",
@@ -87,7 +93,7 @@ export function generateAstroidActions(config) {
87
93
  " }),",
88
94
  " ),",
89
95
  "",
90
- " // The versioned body/sections save — stages a DRAFT, exactly as",
96
+ " // The versioned body/sections save—stages a DRAFT, exactly as",
91
97
  " // versionsRoute does, through the same `applySaveDraft`.",
92
98
  " saveDraft: defineAction(",
93
99
  " louiseSaveDraftAction({",
@@ -108,7 +114,7 @@ export function generateAstroidActions(config) {
108
114
  " ...deps,",
109
115
  " table: siteSettings,",
110
116
  " // The SAME allowlist the generated worker enforces, imported rather",
111
- " // than copied — a second literal here is a list that drifts from the",
117
+ " // than copied—a second literal here is a list that drifts from the",
112
118
  " // one the routes check against, and nothing would fail when it did.",
113
119
  ...settingsExtra,
114
120
  ' mediaBase: astroidConfig.deploy?.mediaBase ?? "/media",',
@@ -1,12 +1,12 @@
1
1
  import type { AstroidConfig } from "../config.js";
2
2
  /** A generated file: a project-root-relative POSIX path + its full contents. */
3
3
  export interface GeneratedFile {
4
- /** Path relative to the project root, POSIX-separated (e.g. `"src/worker.ts"`). */
4
+ /** Path relative to the project root, POSIX-separated (for example, `"src/worker.ts"`). */
5
5
  path: string;
6
6
  contents: string;
7
7
  }
8
8
  /**
9
- * The regenerated trio — the files that are a pure function of the Astroid config
9
+ * The regenerated trio—the files that are a pure function of the Astroid config
10
10
  * and carry a "do not hand-edit" banner. `astroid generate` writes exactly these,
11
11
  * and `astroid doctor` regenerates them in-memory to diff against disk. Scaffold-
12
12
  * once files (wrangler.jsonc, astro.config, auth.ts) are NOT here by design.
@@ -22,7 +22,7 @@ export declare function generateAstroidProject(config: AstroidConfig): Generated
22
22
  * names come from {@link commerceSecretNames}, the same declaration the runtime
23
23
  * gate and the generated `env.d.ts` read.
24
24
  *
25
- * Empty string when the project enables no module that needs credentials — the
25
+ * Empty string when the project enables no module that needs credentials—the
26
26
  * core secrets (session, Turnstile, mail) are already in the template file, with
27
27
  * their own prose.
28
28
  */
@@ -31,7 +31,7 @@ export declare function generateAstroidSecretsEnv(config: AstroidConfig): string
31
31
  * Generate a floor `wrangler.jsonc` from the config: the Worker name + editable
32
32
  * bindings a baseline Louise site needs (D1, R2 media, the rate-limit + autosave
33
33
  * KV, Cloudflare Images), custom-domain routes from `hosts`, and the `vars` the
34
- * media route + editor read. Binding ids are placeholders — real ids are filled by
34
+ * media route + editor read. Binding ids are placeholders—real ids are filled by
35
35
  * `wrangler … create` (or, later, `astroid deploy`); `astroid doctor` flags any
36
36
  * still-unresolved placeholder.
37
37
  *
@@ -1,18 +1,18 @@
1
1
  // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
2
  //
3
- // Project generation — the config → files layer the `astroid` CLI writes.
3
+ // Project generation—the config → files layer the `astroid` CLI writes.
4
4
  //
5
5
  // Two tiers of generated file, deliberately kept apart:
6
6
  //
7
- // 1. The REGENERATED trio (`generateAstroidProject`) — `src/schema.ts`,
7
+ // 1. The REGENERATED trio (`generateAstroidProject`)—`src/schema.ts`,
8
8
  // `src/worker.ts`, `src/middleware.ts`. Pure functions of the config, marked
9
9
  // "do not hand-edit". `astroid generate` (and `dev`/`build`) rewrite these on
10
10
  // every run, and `astroid doctor` diffs them to catch drift.
11
11
  //
12
- // 2. The SCAFFOLD-ONCE files (`generateAstroidWrangler`, …) — `wrangler.jsonc`
12
+ // 2. The SCAFFOLD-ONCE files (`generateAstroidWrangler`, …)—`wrangler.jsonc`
13
13
  // and friends. `create-astroid` writes them once; the developer then owns
14
14
  // them (fills real binding ids, secrets, account). `astroid generate` must
15
- // NEVER clobber them, or it would wipe provisioned ids — so they live in a
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
17
  import { ASTROID_VITALS_BINDING, astroidVitalsDataset } from "../analytics/index.js";
18
18
  import { astroidCheckoutVars } from "../commerce/checkout-scaffold.js";
@@ -25,7 +25,7 @@ import { tenancyZone } from "../tenancy/index.js";
25
25
  import { generateAstroidSchema } from "../schema/generate.js";
26
26
  import { generateAstroidMiddleware, generateAstroidWorker } from "../worker/generate.js";
27
27
  /**
28
- * The regenerated trio — the files that are a pure function of the Astroid config
28
+ * The regenerated trio—the files that are a pure function of the Astroid config
29
29
  * and carry a "do not hand-edit" banner. `astroid generate` writes exactly these,
30
30
  * and `astroid doctor` regenerates them in-memory to diff against disk. Scaffold-
31
31
  * once files (wrangler.jsonc, astro.config, auth.ts) are NOT here by design.
@@ -47,7 +47,7 @@ export function generateAstroidProject(config) {
47
47
  * names come from {@link commerceSecretNames}, the same declaration the runtime
48
48
  * gate and the generated `env.d.ts` read.
49
49
  *
50
- * Empty string when the project enables no module that needs credentials — the
50
+ * Empty string when the project enables no module that needs credentials—the
51
51
  * core secrets (session, Turnstile, mail) are already in the template file, with
52
52
  * their own prose.
53
53
  */
@@ -75,14 +75,14 @@ export function generateAstroidSecretsEnv(config) {
75
75
  return lines.join("\n");
76
76
  }
77
77
  // Pinned compatibility date for the emitted Worker. A literal (Astroid's
78
- // generators are pure — no `Date.now()`), bumped deliberately when the runtime
78
+ // generators are pure—no `Date.now()`), bumped deliberately when the runtime
79
79
  // baseline moves; matches the reference site's wrangler.jsonc.
80
80
  const COMPATIBILITY_DATE = "2026-06-20";
81
81
  /**
82
82
  * Generate a floor `wrangler.jsonc` from the config: the Worker name + editable
83
83
  * bindings a baseline Louise site needs (D1, R2 media, the rate-limit + autosave
84
84
  * KV, Cloudflare Images), custom-domain routes from `hosts`, and the `vars` the
85
- * media route + editor read. Binding ids are placeholders — real ids are filled by
85
+ * media route + editor read. Binding ids are placeholders—real ids are filled by
86
86
  * `wrangler … create` (or, later, `astroid deploy`); `astroid doctor` flags any
87
87
  * still-unresolved placeholder.
88
88
  *
@@ -98,7 +98,7 @@ export function generateAstroidWrangler(config) {
98
98
  const p = (s = "") => lines.push(s);
99
99
  p("{");
100
100
  p(' "$schema": "node_modules/wrangler/config-schema.json",');
101
- p(" // The Cloudflare Worker name — also the default *.workers.dev subdomain.");
101
+ p(" // The Cloudflare Worker name—also the default *.workers.dev subdomain.");
102
102
  p(` "name": ${JSON.stringify(key)},`);
103
103
  p(" // Pin your account so deploys don't prompt (or set CLOUDFLARE_ACCOUNT_ID).");
104
104
  p(' // "account_id": "<your-cloudflare-account-id>",');
@@ -108,7 +108,7 @@ export function generateAstroidWrangler(config) {
108
108
  p(" // global_fetch_strictly_public: a fetch to this zone goes through Cloudflare's");
109
109
  p(" // front door like any Internet request, past the WAF and bot rules (ADR 0012).");
110
110
  p(" // If the daily health scan starts reporting broken links that aren't, a zone");
111
- p(" // rule is challenging its self-crawl — allow it rather than drop the flag.");
111
+ p(" // rule is challenging its self-crawl—allow it rather than drop the flag.");
112
112
  p(' "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],');
113
113
  p(" // @astrojs/cloudflare builds this entry and wires the static assets under dist/.");
114
114
  p(' "main": "src/worker.ts",');
@@ -120,11 +120,11 @@ export function generateAstroidWrangler(config) {
120
120
  p(` { "pattern": ${JSON.stringify(host)}, "custom_domain": true },`);
121
121
  }
122
122
  if (tenancy) {
123
- // A wildcard is a ZONE route, never a custom domain — Cloudflare rejects
123
+ // A wildcard is a ZONE route, never a custom domain—Cloudflare rejects
124
124
  // `custom_domain: true` on a pattern containing `*`, which is precisely
125
125
  // why `hosts` cannot express this.
126
126
  p(" // Wildcard tenant hosts (`tenancy.hostPattern`). A zone route, NOT a");
127
- p(" // custom_domain — Cloudflare refuses a wildcard custom domain. The");
127
+ p(" // custom_domain—Cloudflare refuses a wildcard custom domain. The");
128
128
  p(" // apex is routed separately above: `*.example.com` does NOT match");
129
129
  p(" // `example.com`, so without a `hosts` entry the apex would 404.");
130
130
  p(` { "pattern": ${JSON.stringify(`${tenancy.hostPattern}/*`)}, "zone_name": ${JSON.stringify(tenancyZone(tenancy))} },`);
@@ -137,7 +137,7 @@ export function generateAstroidWrangler(config) {
137
137
  }
138
138
  if (usesRealtime(config)) {
139
139
  // The per-page live editing session (ADR 0002). Two halves, and BOTH are
140
- // required — a binding with no migration is a deploy error, and the class
140
+ // required—a binding with no migration is a deploy error, and the class
141
141
  // must also be exported from the worker entry (the generated src/worker.ts
142
142
  // re-exports it) or wrangler can't resolve `class_name`.
143
143
  p(" // Durable Object: the per-page live editing session (realtime module).");
@@ -146,7 +146,7 @@ export function generateAstroidWrangler(config) {
146
146
  p(" },");
147
147
  p(" // A DO class needs a migration tag. `new_sqlite_classes` (NOT");
148
148
  p(" // `new_classes`) because the session keeps its authoritative state in");
149
- p(" // `ctx.storage`, which is the SQLite-backed store — and the storage");
149
+ p(" // `ctx.storage`, which is the SQLite-backed store—and the storage");
150
150
  p(" // backend cannot be changed after the class is first deployed.");
151
151
  p(' "migrations": [');
152
152
  p(` { "tag": ${JSON.stringify(ASTROID_REALTIME_MIGRATION_TAG)}, "new_sqlite_classes": [${JSON.stringify(ASTROID_EDIT_SESSION_CLASS)}] }`);
@@ -154,7 +154,7 @@ export function generateAstroidWrangler(config) {
154
154
  }
155
155
  // Crons. ONE `scheduled` handler receives all of them and tells them apart by
156
156
  // `controller.cron`, so this list and the handler's dispatch must agree
157
- // exactly — both come from `astroidCrons`, which is why it exists.
157
+ // exactly—both come from `astroidCrons`, which is why it exists.
158
158
  //
159
159
  // Daily: the site-health scan (broken links, missing alt text, SEO gaps).
160
160
  // Hourly (commerce only): the catalog re-sync safety net, so a missed or DLQ'd
@@ -164,7 +164,7 @@ export function generateAstroidWrangler(config) {
164
164
  const { queue, dlq } = astroidQueueNames(config);
165
165
  p(" // Provider webhooks are verified at the edge, then enqueued here so the");
166
166
  p(" // receiver can return fast. Retries + DLQ routing are Cloudflare's, not");
167
- p(" // the consumer's — set them here, not in code.");
167
+ p(" // the consumer's—set them here, not in code.");
168
168
  p(` // Create both: \`wrangler queues create ${queue}\` and \`… ${dlq}\`.`);
169
169
  p(' "queues": {');
170
170
  p(` "producers": [{ "queue": ${JSON.stringify(queue)}, "binding": ${JSON.stringify(ASTROID_QUEUE_BINDING)} }],`);
@@ -197,17 +197,17 @@ export function generateAstroidWrangler(config) {
197
197
  p(' "images": { "binding": "IMAGES" },');
198
198
  p(" // Analytics Engine: real-visitor Core Web Vitals. Free, and the ingest");
199
199
  p(" // route accepts-and-drops without it, so it costs nothing unused. Reading");
200
- p(" // the p75 back out needs CF_ACCOUNT_ID + CF_API_TOKEN (see .env.example) —");
200
+ p(" // the p75 back out needs CF_ACCOUNT_ID + CF_API_TOKEN (see .env.example)—");
201
201
  p(" // until those are real the Health badge reads 'not measured yet'.");
202
202
  p(` "analytics_engine_datasets": [{ "binding": ${JSON.stringify(ASTROID_VITALS_BINDING)}, "dataset": ${JSON.stringify(astroidVitalsDataset(config))} }],`);
203
203
  p(" // Workers AI. Powers the editor's rewrite + SEO-suggest buttons and alt-text");
204
- p(" // generation on upload — all of which SHIP IN THE EDITOR DRAWER already and,");
204
+ p(" // generation on upload—all of which SHIP IN THE EDITOR DRAWER already and,");
205
205
  p(" // without this binding, were permanently invisible: their routes answer 503");
206
206
  p(" // and the client hides the button. No account setup beyond the binding, and");
207
207
  p(" // every call is editor-gated, so a visitor can never spend your AI budget.");
208
208
  p(' "ai": { "binding": "AI" },');
209
209
  p(" // KV: RL = the security rate limiter (it also holds the daily site-health");
210
- p(" // summary under its own key — one small singleton blob, not worth a binding");
210
+ p(" // summary under its own key—one small singleton blob, not worth a binding");
211
211
  p(" // someone has to remember to provision); DRAFTS = the autosave write-buffer.");
212
212
  p(" // Create each: `wrangler kv namespace create <RL|DRAFTS>`.");
213
213
  p(' "kv_namespaces": [');
@@ -217,11 +217,11 @@ export function generateAstroidWrangler(config) {
217
217
  // Email Sending. NOT optional decoration: `src/env.d.ts` declares EMAIL as a
218
218
  // required member, and Better Auth's magic-link path console-logs the link in
219
219
  // dev but calls `env.EMAIL.send(...)` unconditionally in production. Without
220
- // this binding that call is a TypeError on a binding that was never created —
221
- // so sign-in was impossible on every DEPLOYED site, while every local build
220
+ // this binding that call is a TypeError on a binding that was never created—so
221
+ // sign-in was impossible on every DEPLOYED site, while every local build
222
222
  // and every CI scaffold passed. Nothing in this repo runs a deployed scaffold,
223
223
  // which is why it survived.
224
- p(" // Cloudflare Email Sending — magic-link sign-in + inquiry notifications.");
224
+ p(" // Cloudflare Email Sending—magic-link sign-in + inquiry notifications.");
225
225
  p(" // Sign-in DEPENDS on this: in production the magic link is emailed, not logged.");
226
226
  p(" // Enable Email Sending for your zone, then verify the address in MAIL_FROM.");
227
227
  p(' "send_email": [{ "name": "EMAIL" }],');
@@ -239,10 +239,10 @@ export function generateAstroidWrangler(config) {
239
239
  p(" // Turn it on for a PREVIEW deploy first and walk the activation runbook");
240
240
  p(" // (docs/adr/0004-edge-caching.md). `caches.default` is not cleared by");
241
241
  p(" // Cloudflare Dev Mode or Purge Everything, so a bad prod flip is hard to");
242
- p(" // undo — this feature was reverted twice for exactly that.");
242
+ p(" // undo—this feature was reverted twice for exactly that.");
243
243
  p(' "ASTROID_EDGE_CACHE": "false",');
244
244
  for (const v of astroidCheckoutVars(config)) {
245
- // Public, not secret — the app id ships to the browser to mount the card
245
+ // Public, not secret—the app id ships to the browser to mount the card
246
246
  // field, and the environment is a choice. Keeping them out of the secret
247
247
  // roster also keeps them out of the dormancy gate, which asks whether we can
248
248
  // safely CALL Square, not whether a card field can render.
@@ -251,11 +251,11 @@ export function generateAstroidWrangler(config) {
251
251
  p(" },");
252
252
  // Secrets are NOT vars: they belong in .dev.vars locally and in `wrangler
253
253
  // secret put` / Secrets Store when deployed. Listing the names here is
254
- // deliberate — this is the file someone opens when provisioning, and the list
254
+ // deliberate—this is the file someone opens when provisioning, and the list
255
255
  // is generated from the same declaration the runtime dormancy gate reads.
256
256
  const secretNames = commerceSecretNames(config.commerce);
257
257
  if (secretNames.length > 0) {
258
- p(" // Commerce secrets — set OUTSIDE this file (it's committed):");
258
+ p(" // Commerce secrets—set OUTSIDE this file (it's committed):");
259
259
  p(" // local: .dev.vars (see .env.example, seeded with DUMMY_REPLACE_ME)");
260
260
  p(" // deployed: `wrangler secret put <NAME>`, or a Secrets Store binding");
261
261
  p(" // Until each is real, commerce stays dormant: the D1 mirror serves, the");
@@ -1,3 +1,4 @@
1
1
  export * from "./generate.js";
2
2
  export * from "./actions.js";
3
3
  export * from "./scaffold.js";
4
+ export * from "./seed.js";
@@ -1,7 +1,8 @@
1
1
  // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
2
  //
3
- // Project generation — config → the files the `astroid` CLI writes (the
3
+ // Project generation—config → the files the `astroid` CLI writes (the
4
4
  // regenerated schema/worker/middleware trio + the scaffold-once wrangler.jsonc).
5
5
  export * from "./generate.js";
6
6
  export * from "./actions.js";
7
7
  export * from "./scaffold.js";
8
+ export * from "./seed.js";
@@ -4,7 +4,7 @@ import type { AstroidConfig } from "../config.js";
4
4
  *
5
5
  * `apply` is the whole contract. `"skip"` (the default) leaves an existing file
6
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
7
+ * owns outright—`public/_headers` gets a stanza per module, and a second
8
8
  * module must not erase the first one's.
9
9
  */
10
10
  export interface ScaffoldFile {
@@ -23,7 +23,7 @@ export interface ScaffoldFile {
23
23
  * Every scaffold-once file this config implies.
24
24
  *
25
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
26
+ * sequence. Returns `[]` for a plain marketing site with no modules—the
27
27
  * baseline floor is entirely the regenerated trio plus the static template.
28
28
  */
29
29
  export declare function generateAstroidScaffoldFiles(config: AstroidConfig): ScaffoldFile[];