@bettercms-ai/codegen 0.5.0 → 0.7.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 CHANGED
@@ -83,10 +83,10 @@ import { bcms } from "./bettercms.bindings.generated";
83
83
  </article>
84
84
  ```
85
85
 
86
- The attributes are emitted **only** when the site is built with `BCMS_ANNOTATE` set (preview
87
- builds); a normal production build ships zero extra attributes (`bcmsField` returns `{}`). Same
88
- generated file for both no separate mode. Bindings follow the editor's one-level path grammar
89
- (`field`, `field[i]`, `field[i].sub`); non-repeatable zones and deeper nesting aren't
86
+ The attributes are emitted on **every** build two inert `data-*` attributes, the same way
87
+ Storyblok emits `data-blok-c` and Sanity emits `data-sanity` so a site is editable no matter
88
+ which pipeline built it. No build flag, no separate mode. Bindings follow the editor's one-level
89
+ path grammar (`field`, `field[i]`, `field[i].sub`); non-repeatable zones and deeper nesting aren't
90
90
  index-addressable yet, so they're omitted rather than emitted as paths that can't bind.
91
91
 
92
92
  ## Library API
package/dist/cli.js CHANGED
@@ -47,12 +47,126 @@ var PREAMBLE = `/**
47
47
  *
48
48
  * The \`{ format, value }\` contract is unchanged; \`html\` is additive.
49
49
  */
50
+ /**
51
+ * One Portable Text block, or a block object (an image, an embed, a placed component).
52
+ *
53
+ * Inlined rather than imported: this preamble is emitted INTO your repo and is deliberately
54
+ * dependency-free. To render structure, \`npm i @portabletext/react\` and pass
55
+ * \`portableText(field)\` to it; to render without adding anything, keep using \`rich()\`.
56
+ */
57
+ export type PortableTextBlock = {
58
+ readonly _type: string;
59
+ readonly _key: string;
60
+ readonly style?: string;
61
+ readonly listItem?: string;
62
+ readonly level?: number;
63
+ readonly markDefs?: readonly { readonly _type: string; readonly _key: string; readonly [k: string]: unknown }[];
64
+ readonly children?: readonly { readonly _type: string; readonly _key: string; readonly text?: string; readonly marks?: readonly string[] }[];
65
+ readonly [k: string]: unknown;
66
+ };
67
+
50
68
  export type RichText = {
69
+ /**
70
+ * The storage format. Deliberately \`string\` and NOT a literal union: a project mid-backfill
71
+ * holds both \`"lexical-\u2026"\` and \`"portable-text-1"\` values, and narrowing this would give a
72
+ * type error to anyone regenerating types against it.
73
+ */
51
74
  readonly format: string;
75
+ /** Portable Text blocks when \`format\` is \`"portable-text-1"\`; editor state otherwise. */
52
76
  readonly value: unknown;
77
+ /**
78
+ * Server-rendered, sanitized HTML. ALWAYS present, in every format, forever \u2014 sites built
79
+ * before Portable Text existed read this directly and cannot be rebuilt.
80
+ */
53
81
  readonly html?: string;
82
+ /**
83
+ * @deprecated Superseded by Portable Text \u2014 read \`portableText(field)\` instead. Retained so
84
+ * entries written before the migration keep type-checking; nothing mints it any more.
85
+ *
86
+ * Structured blocks. Present on Body (\`document\`) fields ONLY, and optional even there \u2014
87
+ * it is derived at write time, so an entry saved before this existed carries none until its
88
+ * next save, and there is no backfill. Branch on its absence; \`html\` is always there.
89
+ *
90
+ * \`id\` is stable within ONE document, never a global key \u2014 two entries both have a "0.0".
91
+ * A cross-document anchor is (entryId, fieldKey, id).
92
+ */
93
+ readonly doc?: {
94
+ readonly version: 1;
95
+ readonly blocks: readonly { readonly id: string; readonly type: string; readonly [k: string]: unknown }[];
96
+ };
54
97
  };
55
98
 
99
+ /**
100
+ * A field that may arrive as EITHER shape.
101
+ *
102
+ * Switching a field between \`text\` and \`richtext\` in the CMS switches what Delivery
103
+ * returns for it \u2014 a bare string becomes \`{ format, value, html }\`. Type author-editable
104
+ * text with this and read it through \`plain()\`/\`rich()\` below, and that switch stops being
105
+ * a site-breaking change. Interpolating the value directly renders \`[object Object]\`.
106
+ */
107
+ export type TextOrRich = string | RichText | null | undefined;
108
+
109
+ /** True when the value is a rich-text envelope rather than a bare string. */
110
+ export function isRichText(value: unknown): value is RichText {
111
+ return (
112
+ typeof value === "object" && value !== null && !Array.isArray(value) &&
113
+ ("html" in value || "format" in value)
114
+ );
115
+ }
116
+
117
+ /** The marker on a Portable Text envelope. */
118
+ export const PORTABLE_TEXT_FORMAT = "portable-text-1";
119
+
120
+ /**
121
+ * Portable Text blocks when the field stores them, otherwise null.
122
+ *
123
+ * ADDITIVE. \`rich()\` and \`plain()\` keep working exactly as before on every format, so
124
+ * nothing you have already shipped needs to change. Use this only if you want to render the
125
+ * structure yourself \u2014 for example with \`@portabletext/react\`.
126
+ */
127
+ export function portableText(value: TextOrRich): readonly PortableTextBlock[] | null {
128
+ if (!isRichText(value)) return null;
129
+ return value.format === PORTABLE_TEXT_FORMAT && Array.isArray(value.value)
130
+ ? (value.value as readonly PortableTextBlock[])
131
+ : null;
132
+ }
133
+
134
+ /** Plain text for attribute contexts \u2014 \`<title>\`, meta description, JSON-LD, \`alt\`. */
135
+ export function plain(value: TextOrRich): string {
136
+ if (typeof value === "string") return value;
137
+ if (!isRichText(value) || typeof value.html !== "string") return "";
138
+ return decodeEntities(value.html.replace(/<[^>]+>/g, "")).trim();
139
+ }
140
+
141
+ /**
142
+ * Renderable HTML, for \`set:html\` / \`dangerouslySetInnerHTML\`. Rich text keeps its inline
143
+ * marks (the server sanitizes \`html\` on write); a bare string is escaped, so a plain field
144
+ * can never inject markup. A LONE wrapping block is unwrapped \u2014 a field switched from
145
+ * \`text\` stores \`<p>\u2026</p>\`, and \`<h1><p>\u2026</p></h1>\` is invalid HTML (the parser closes the
146
+ * heading early, dropping the text out of it). Real block structure is left alone.
147
+ */
148
+ export function rich(value: TextOrRich, fallback = ""): string {
149
+ const html = (
150
+ typeof value === "string" ? escapeHtml(value) : isRichText(value) ? (value.html ?? "") : ""
151
+ ).trim();
152
+ return html ? unwrapLoneBlock(html) : escapeHtml(fallback);
153
+ }
154
+
155
+ function unwrapLoneBlock(html: string): string {
156
+ const m = html.match(/^<(p|div|h[1-6])(?:\\s[^>]*)?>([\\s\\S]*)<\\/\\1>$/i);
157
+ return m && !new RegExp(\`</\${m[1]}>\`, "i").test(m[2]) ? m[2] : html;
158
+ }
159
+
160
+ function escapeHtml(s: string): string {
161
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
162
+ }
163
+
164
+ function decodeEntities(s: string): string {
165
+ return s
166
+ .replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"')
167
+ .replace(/&#0?39;/g, "'").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&");
168
+ }
169
+
56
170
  /**
57
171
  * Image / media field value as stored and returned verbatim by the Delivery API
58
172
  * (server-normalized on write to the canonical shape). \`url\` is always present; an
@@ -68,6 +182,45 @@ export interface BetterCMSImage {
68
182
  readonly height?: number;
69
183
  }
70
184
 
185
+ /**
186
+ * A component slot's value, as stored and delivered.
187
+ *
188
+ * \`componentId\` points at a component definition; \`overrides\` are the author's values,
189
+ * keyed by the component's declared prop keys.
190
+ *
191
+ * \`resolved\` is the SNAPSHOT: at publish time the component is resolved (its block tree
192
+ * with the overrides applied) and frozen onto the published value. That is why editing a
193
+ * component does not silently rewrite entries that were already published \u2014 a published
194
+ * entry carries what it was published with until it is published again.
195
+ *
196
+ * Read \`resolved\` when it is there; it is absent on draft-perspective reads, where you
197
+ * should resolve \`componentId\` yourself against the components endpoint.
198
+ */
199
+ export interface BetterCMSComponentRef {
200
+ readonly componentId: string;
201
+ readonly overrides?: Readonly<Record<string, unknown>>;
202
+ readonly resolved?: readonly unknown[];
203
+ }
204
+
205
+ /**
206
+ * One block in a section zone, as stored and delivered.
207
+ *
208
+ * This is the SAME shape a page's \`blockJson\` holds \u2014 a section zone is a composable page
209
+ * region, so it delivers page blocks, not a shape of its own. Deliberately structural rather
210
+ * than a discriminated union over every block type: the container blocks
211
+ * (\`columns\`, \`section\`, \`slider\`, \`tabs\`) nest \`BetterCMSBlock\` inside \`props\`,
212
+ * and the block vocabulary is server-side and versioned independently of any generated SDK.
213
+ * Narrow on \`type\` at the call site.
214
+ *
215
+ * \`style\` carries the block's design tokens when the author set any.
216
+ */
217
+ export interface BetterCMSBlock {
218
+ readonly type: string;
219
+ readonly id: string;
220
+ readonly props?: Readonly<Record<string, unknown>>;
221
+ readonly style?: Readonly<Record<string, unknown>>;
222
+ }
223
+
71
224
  /**
72
225
  * Delivery envelope around a model's typed \`data\`. \`getEntry\`/\`listEntries\` in the
73
226
  * Next adapter return this shape, with \`fields\` typed by the model.
@@ -78,6 +231,54 @@ export interface BetterCMSEntry<TFields> {
78
231
  readonly fields: TFields;
79
232
  readonly updatedAt: string;
80
233
  }
234
+
235
+ /** Fully resolved published or preview Layout returned beside a delivered page. */
236
+ export interface BetterCMSLayout {
237
+ readonly version: 1;
238
+ readonly nodes: ReadonlyArray<
239
+ | { readonly kind: "page-content"; readonly id: "page-content" }
240
+ | {
241
+ readonly kind: "section";
242
+ readonly id: string;
243
+ readonly slug: string;
244
+ readonly name: string;
245
+ readonly source: "inherit" | "override-content" | "customize-structure" | "page-only" | "detached";
246
+ }
247
+ >;
248
+ readonly sections: Readonly<Record<string, {
249
+ readonly id: string;
250
+ readonly slug: string;
251
+ readonly name: string;
252
+ /** Headless Section values, including values represented by direct field items. */
253
+ readonly fields: Readonly<Record<string, unknown>>;
254
+ readonly items: ReadonlyArray<
255
+ | { readonly id: string; readonly kind: "field"; readonly fieldId: string; readonly value: unknown }
256
+ | {
257
+ readonly id: string;
258
+ readonly kind: "component";
259
+ readonly componentId: string;
260
+ readonly variantGroupId?: string;
261
+ readonly canonicalInputs: readonly BetterCMSLayoutInput[];
262
+ readonly bindings: ReadonlyArray<{ readonly inputId: string; readonly fieldId: string }>;
263
+ readonly canonicalValues: Readonly<Record<string, unknown>>;
264
+ readonly resolvedProps: Readonly<Record<string, unknown>>;
265
+ readonly blocks: readonly unknown[];
266
+ }
267
+ >;
268
+ }>>;
269
+ }
270
+
271
+ /** Recursive canonical input description exposed with each delivered Component item. */
272
+ export interface BetterCMSLayoutInput {
273
+ readonly id: string;
274
+ readonly slug: string;
275
+ readonly label: string;
276
+ readonly type: string;
277
+ readonly required?: boolean;
278
+ readonly defaultValue?: unknown;
279
+ readonly config?: Readonly<Record<string, unknown>>;
280
+ readonly fields?: readonly BetterCMSLayoutInput[];
281
+ }
81
282
  `;
82
283
  function pascalCase(slug) {
83
284
  const parts = slug.split(/[-_\s]+/).filter(Boolean);
@@ -97,6 +298,10 @@ function scalarType(field) {
97
298
  case "text":
98
299
  return "string";
99
300
  case "richtext":
301
+ // A document field stores the SAME {format, value, html} envelope as richtext — the
302
+ // difference is the editor and the placement, not the wire shape. So it maps to the same
303
+ // generated type, and the `doc` rendition lands on `RichText` itself rather than here.
304
+ case "document":
100
305
  return "RichText";
101
306
  case "image":
102
307
  return "BetterCMSImage";
@@ -115,7 +320,12 @@ function scalarType(field) {
115
320
  case "reference":
116
321
  return "string";
117
322
  // referenced entry id
323
+ // Both spellings are live (see ContentModelFieldType). Template- and Webflow-seeded
324
+ // models carry the camelCase one; until it was handled here it fell through to the
325
+ // exhaustiveness default, so generated types for every template-created collection
326
+ // typed this field as `unknown` instead of `string[]`.
118
327
  case "multi-reference":
328
+ case "multiReference":
119
329
  return "string[]";
120
330
  // referenced entry ids
121
331
  case "array": {
@@ -123,6 +333,35 @@ function scalarType(field) {
123
333
  const inner = itemType === "number" ? "number" : "string";
124
334
  return `${inner}[]`;
125
335
  }
336
+ // ── Builder scalars ────────────────────────────────────────────────────────
337
+ // All string-shaped on the wire; each is value-validated on write (see
338
+ // src/lib/content/reference-validation.ts), so the generated type is the
339
+ // narrowest thing that is actually true of the stored value.
340
+ case "longtext":
341
+ case "slug":
342
+ case "email":
343
+ case "phone":
344
+ case "link":
345
+ case "color":
346
+ return "string";
347
+ case "json":
348
+ return "unknown";
349
+ case "component-ref":
350
+ return "BetterCMSComponentRef";
351
+ case "modular":
352
+ return "ReadonlyArray<{ readonly __id: string; readonly __type: string; readonly data: Record<string, unknown> }>";
353
+ case "sections": {
354
+ const config = field.config;
355
+ const unresolvedLegacy = config?.mode === "authored-v2" && Object.prototype.hasOwnProperty.call(config, "allowedSections") && config.legacyResolved !== true;
356
+ if (config?.mode === "authored-v2" && !unresolvedLegacy) {
357
+ return "ReadonlyArray<{ readonly __id: string; readonly __section: string; readonly __type: string; readonly data: Record<string, unknown> }>";
358
+ }
359
+ return "ReadonlyArray<BetterCMSBlock>";
360
+ }
361
+ case "location":
362
+ return "{ readonly lat: number; readonly lng: number; readonly label?: string }";
363
+ case "file":
364
+ return "BetterCMSImage";
126
365
  default: {
127
366
  const _exhaustive = t;
128
367
  return "unknown";
@@ -227,6 +466,8 @@ function bindingKind(t) {
227
466
  case "select":
228
467
  case "array":
229
468
  return t;
469
+ case "document":
470
+ return "richtext";
230
471
  // reference / multi-reference / date / datetime → plain text in the editor v1.
231
472
  default:
232
473
  return "text";
@@ -277,27 +518,12 @@ function fieldsToBindings(fields, indent) {
277
518
  return fields.map((field) => fieldBinding(field, "", indent)).join("\n");
278
519
  }
279
520
  var PREAMBLE2 = `/**
280
- * True when this site is built for Live Preview annotation. Set \`BCMS_ANNOTATE=1\`
281
- * in the preview build only; unset (the default) ships zero binding attributes.
282
- * Read defensively so the module is safe in any runtime (browser, Node, edge).
283
- */
284
- const BCMS_ANNOTATE: boolean = (() => {
285
- try {
286
- const v = (globalThis as { process?: { env?: Record<string, string | undefined> } })
287
- .process?.env?.BCMS_ANNOTATE;
288
- return v != null && v !== "" && v !== "0" && v !== "false";
289
- } catch {
290
- return false;
291
- }
292
- })();
293
-
294
- /**
295
521
  * Binding attributes for a CMS-bound element. Spread onto the element that renders a
296
- * field: \`<h1 {...bcmsField("title", "text")}>\`. Returns \`{}\` unless BCMS_ANNOTATE
297
- * is set, so production markup is untouched.
522
+ * field: \`<h1 {...bcmsField("title", "text")}>\`. Always emitted, on every build \u2014
523
+ * two inert \`data-*\` attributes are what makes the site editable in Live Preview.
298
524
  */
299
525
  export function bcmsField(path: string, kind: string): Record<string, string> {
300
- return BCMS_ANNOTATE ? { "data-bcms-field": path, "data-bcms-kind": kind } : {};
526
+ return { "data-bcms-field": path, "data-bcms-kind": kind };
301
527
  }
302
528
  `;
303
529
  function generateBindings(models, opts = {}) {
@@ -308,7 +534,7 @@ function generateBindings(models, opts = {}) {
308
534
  const header = `// \u26A0\uFE0F AUTO-GENERATED by @bettercms-ai/codegen v${version} \u2014 DO NOT EDIT.
309
535
  // Live Preview field bindings. Regenerate with: npx @bettercms-ai/codegen --bindings-out <path>
310
536
  // Spread these onto the elements that render your content; they emit
311
- // data-bcms-field/data-bcms-kind only when the site is built with BCMS_ANNOTATE set.
537
+ // data-bcms-field/data-bcms-kind on every build.
312
538
  ${opts.bannerComment ? `// ${opts.bannerComment}
313
539
  ` : ""}`;
314
540
  const entries = sorted.map((model) => {
@@ -344,6 +570,12 @@ export type RichTextValue = {
344
570
  readonly format: string;
345
571
  readonly value: unknown;
346
572
  readonly html?: string;
573
+ /**
574
+ * Structured blocks \u2014 Body (\`document\`) fields only, and optional even there: derived at
575
+ * write time, so entries saved before it existed carry none until re-saved. No backfill.
576
+ * Block ids are stable within one document only, never a global key.
577
+ */
578
+ readonly doc?: { readonly version: 1; readonly blocks: readonly { readonly id: string; readonly type: string; readonly [k: string]: unknown }[] };
347
579
  };
348
580
 
349
581
  /** Normalized image/media value from the Delivery API. */
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/fetch-models.ts","../src/generate.ts","../src/bindings.ts","../src/components.ts"],"sourcesContent":["/**\n * `bettercms-codegen` — fetch a project's content models and write a typed `.ts` file.\n *\n * Designed for two call sites:\n * 1. A developer in their repo: npx @bettercms-ai/codegen --out src/bettercms.generated.ts\n * 2. The build-time GitHub Action: same command, key from a repo secret.\n *\n * Auth + endpoint come from flags or env (BETTERCMS_API_KEY, BETTERCMS_API_URL).\n */\n\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { fetchModels } from \"./fetch-models.js\";\nimport { generateTypes } from \"./generate.js\";\nimport { generateBindings } from \"./bindings.js\";\nimport { generateComponents } from \"./components.js\";\n\nconst VERSION = \"0.2.0\";\nconst DEFAULT_API_URL = \"https://api.bettercms.ai/api/v1\";\nconst DEFAULT_OUT = \"bettercms.generated.ts\";\n\ninterface CliArgs {\n apiUrl: string;\n apiKey: string | undefined;\n out: string;\n /** When set, also emit the Live Preview bindings module to this path. */\n bindingsOut: string | undefined;\n /** When set, also emit the typed React render components (.tsx) to this path. */\n componentsOut: string | undefined;\n help: boolean;\n}\n\nfunction parseArgs(argv: string[]): CliArgs {\n const args: CliArgs = {\n apiUrl: process.env.BETTERCMS_API_URL ?? DEFAULT_API_URL,\n apiKey: process.env.BETTERCMS_API_KEY,\n out: DEFAULT_OUT,\n bindingsOut: undefined,\n componentsOut: undefined,\n help: false,\n };\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n const next = () => argv[++i];\n switch (arg) {\n case \"--api-url\":\n args.apiUrl = next() ?? args.apiUrl;\n break;\n case \"--key\":\n case \"--api-key\":\n args.apiKey = next();\n break;\n case \"--out\":\n case \"-o\":\n args.out = next() ?? args.out;\n break;\n case \"--bindings-out\":\n args.bindingsOut = next();\n break;\n case \"--components-out\":\n args.componentsOut = next();\n break;\n case \"--help\":\n case \"-h\":\n args.help = true;\n break;\n }\n }\n return args;\n}\n\nconst HELP = `bettercms-codegen v${VERSION} — generate TypeScript types from your BetterCMS schema\n\nUsage:\n npx @bettercms-ai/codegen [options]\n\nOptions:\n -o, --out <path> Output file (default: ${DEFAULT_OUT})\n --bindings-out <path> Also emit the Live Preview bindings module to <path>\n --components-out <path> Also emit typed <RichText>/<Image> React components (.tsx) to <path>\n --api-url <url> Management API base (default: ${DEFAULT_API_URL})\n --key <key> Management API key (or set BETTERCMS_API_KEY)\n -h, --help Show this help\n\nEnv:\n BETTERCMS_API_KEY Management-scoped key (content:manage)\n BETTERCMS_API_URL Override the API base\n`;\n\nasync function main(): Promise<void> {\n const args = parseArgs(process.argv.slice(2));\n\n if (args.help) {\n process.stdout.write(HELP);\n return;\n }\n if (!args.apiKey) {\n process.stderr.write(\n \"error: no API key. Pass --key <key> or set BETTERCMS_API_KEY.\\n\",\n );\n process.exit(1);\n }\n\n const models = await fetchModels({ apiUrl: args.apiUrl, apiKey: args.apiKey });\n\n const outPath = resolve(process.cwd(), args.out);\n await mkdir(dirname(outPath), { recursive: true });\n await writeFile(outPath, generateTypes(models, { version: VERSION }), \"utf8\");\n\n const plural = models.length === 1 ? \"\" : \"s\";\n process.stdout.write(\n `✓ Generated ${models.length} model type${plural} → ${args.out}\\n`,\n );\n\n if (args.bindingsOut) {\n const bindingsPath = resolve(process.cwd(), args.bindingsOut);\n await mkdir(dirname(bindingsPath), { recursive: true });\n await writeFile(bindingsPath, generateBindings(models, { version: VERSION }), \"utf8\");\n process.stdout.write(`✓ Generated Live Preview bindings → ${args.bindingsOut}\\n`);\n }\n\n if (args.componentsOut) {\n const componentsPath = resolve(process.cwd(), args.componentsOut);\n await mkdir(dirname(componentsPath), { recursive: true });\n await writeFile(componentsPath, generateComponents({ version: VERSION }), \"utf8\");\n process.stdout.write(`✓ Generated render components → ${args.componentsOut}\\n`);\n }\n}\n\nmain().catch((err: unknown) => {\n process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(1);\n});\n","/**\n * Fetches content models from the BetterCMS Management API so the CLI can generate\n * types against a live project. Kept dependency-free (plain fetch) so the generated\n * artifact and this fetcher can run anywhere — a GitHub Action, a postinstall, a script.\n */\n\nimport type { GeneratableModel } from \"./generate.js\";\n\nexport interface FetchModelsOptions {\n /** Management API base, e.g. \"https://api.bettercms.ai/api/v1\". */\n apiUrl: string;\n /** A management-scoped key (content:manage) or device-minted token. */\n apiKey: string;\n /** Optional fetch override (testing / custom runtime). */\n fetchImpl?: typeof fetch;\n}\n\ninterface ManagedModelRow {\n slug: string;\n name?: string;\n description?: string | null;\n fields: GeneratableModel[\"fields\"];\n}\n\n/**\n * GET /management/content/models — returns the project's models (the key is\n * project-scoped server-side, so this is exactly the schema for this site).\n */\nexport async function fetchModels(\n opts: FetchModelsOptions,\n): Promise<GeneratableModel[]> {\n const doFetch = opts.fetchImpl ?? globalThis.fetch;\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const url = `${base}/management/content/models`;\n\n let res: Response;\n try {\n res = await doFetch(url, {\n // No Content-Type: this is a bodyless GET; the header is incorrect here and\n // strict edge runtimes/proxies may reject it.\n headers: { Authorization: `Bearer ${opts.apiKey}`, Accept: \"application/json\" },\n });\n } catch (err) {\n throw new Error(\n `Could not reach the BetterCMS Management API at ${url}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n\n if (!res.ok) {\n const hint =\n res.status === 401 || res.status === 403\n ? \" — check your management API key (it must have the content:manage scope).\"\n : \"\";\n throw new Error(`Management API returned ${res.status} ${res.statusText}${hint}`);\n }\n\n const body = (await res.json()) as { data?: ManagedModelRow[] };\n const rows = body.data ?? [];\n return rows.map((r) => ({\n slug: r.slug,\n name: r.name,\n description: r.description ?? null,\n fields: r.fields ?? [],\n }));\n}\n","/**\n * @bettercms-ai/codegen — schema → TypeScript generator (the single source of truth).\n *\n * Both the dashboard schema builder and the MCP `create_model`/`add_field` tools write\n * the SAME `content_models.fields` (an array of `ContentModelField`). This generator maps\n * that one array into TypeScript. Because there is exactly one schema representation, the\n * generated types can never drift from the editor or the agent — they are the same source.\n *\n * Pure + deterministic: same models in → identical string out (stable ordering, no clock,\n * no I/O). That makes it trivially testable and safe to commit + diff in a customer repo.\n */\n\nimport type { ContentModelField, ContentModelFieldType } from \"@bettercms-ai/types\";\n\n/** Minimal model shape the generator needs — a subset of the Management API model row. */\nexport interface GeneratableModel {\n /** Machine-safe slug, e.g. \"blog\" or \"case-study\". Used for the schema-map key. */\n slug: string;\n /** Human name, used only for the JSDoc header. */\n name?: string;\n description?: string | null;\n fields: ContentModelField[];\n}\n\nexport interface GenerateOptions {\n /** Generator version stamped into the header (defaults to the package version). */\n version?: string;\n /** Override the banner timestamp source — omitted by default so output is deterministic. */\n bannerComment?: string;\n}\n\n/** Helper types emitted once at the top of every generated file (self-contained, zero-dep). */\nconst PREAMBLE = `/**\n * Rich-text field value returned by the Delivery API.\n *\n * - \\`format\\`/\\`value\\`: the portable, editor-agnostic payload (Lexical EditorState) —\n * render it with your editor's serializer for full fidelity.\n * - \\`html\\`: server-rendered, sanitized HTML (computed render-on-write). Present on\n * Delivery reads; the simplest path for non-React consumers — safe to inject directly\n * (e.g. \\`dangerouslySetInnerHTML\\`). Optional: legacy/un-normalized values may omit it.\n *\n * The \\`{ format, value }\\` contract is unchanged; \\`html\\` is additive.\n */\nexport type RichText = {\n readonly format: string;\n readonly value: unknown;\n readonly html?: string;\n};\n\n/**\n * Image / media field value as stored and returned verbatim by the Delivery API\n * (server-normalized on write to the canonical shape). \\`url\\` is always present; an\n * unresolved/external value may carry only \\`url\\`. \\`altText\\` is the accessibility text\n * for \\`<img alt>\\`.\n */\nexport interface BetterCMSImage {\n readonly id?: string;\n readonly url: string;\n readonly name?: string;\n readonly altText?: string | null;\n readonly width?: number;\n readonly height?: number;\n}\n\n/**\n * Delivery envelope around a model's typed \\`data\\`. \\`getEntry\\`/\\`listEntries\\` in the\n * Next adapter return this shape, with \\`fields\\` typed by the model.\n */\nexport interface BetterCMSEntry<TFields> {\n readonly slug: string;\n readonly status: \"draft\" | \"published\";\n readonly fields: TFields;\n readonly updatedAt: string;\n}\n`;\n\n/** PascalCase an identifier from a slug: \"case-study\" → \"CaseStudy\". */\nfunction pascalCase(slug: string): string {\n const parts = slug.split(/[-_\\s]+/).filter(Boolean);\n const pascal = parts\n .map((p) => p.charAt(0).toUpperCase() + p.slice(1))\n .join(\"\");\n // Guard against an identifier that starts with a digit (invalid TS type name).\n return /^[0-9]/.test(pascal) ? `Model${pascal}` : pascal || \"Model\";\n}\n\n/**\n * Make a string safe to embed inside a `/** ... *​/` JSDoc comment. A field label\n * (free-text, author/agent-controlled) could contain `*​/` — which closes the comment\n * early and injects the remainder as code — or a newline, which breaks the single-line\n * comment. Both are neutralized here. Without this, hostile content produces non-\n * compiling (or worse, code-injected) output.\n */\nfunction escapeJsDoc(text: string): string {\n return text.replace(/\\*\\//g, \"* /\").replace(/[\\r\\n]+/g, \" \").trim();\n}\n\nconst VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * Render a field key as a TS property name. Field keys are author/agent-controlled and\n * not guaranteed to be valid identifiers (e.g. \"my-field\", \"1title\", \"\"), so anything\n * that isn't a bare identifier is emitted as a quoted property name — always valid TS.\n */\nfunction propName(key: string): string {\n return VALID_IDENT.test(key) ? key : JSON.stringify(key);\n}\n\n/** A scalar/primitive field maps to a TS type expression (no nesting). */\nfunction scalarType(field: ContentModelField): string {\n const t: ContentModelFieldType = field.type;\n switch (t) {\n case \"text\":\n return \"string\";\n case \"richtext\":\n return \"RichText\";\n case \"image\":\n return \"BetterCMSImage\";\n case \"boolean\":\n return \"boolean\";\n case \"number\":\n return \"number\";\n case \"date\":\n case \"datetime\":\n return \"string\"; // ISO 8601\n case \"select\": {\n const opts = field.options?.filter((o) => typeof o === \"string\") ?? [];\n return opts.length > 0\n ? opts.map((o) => JSON.stringify(o)).join(\" | \")\n : \"string\";\n }\n case \"reference\":\n return \"string\"; // referenced entry id\n case \"multi-reference\":\n return \"string[]\"; // referenced entry ids\n case \"array\": {\n // Zoned arrays (config.zones) are expanded by fieldsToBody before reaching here;\n // this branch handles only the primitive list form (config.itemType).\n const itemType = field.config?.itemType ?? \"text\";\n const inner =\n itemType === \"number\" ? \"number\" : \"string\"; // text | date → string\n return `${inner}[]`;\n }\n default: {\n // Exhaustiveness guard: if a new field type is added to the union and not\n // mapped here, this line becomes a compile error in the codegen build.\n const _exhaustive: never = t;\n return \"unknown\";\n }\n }\n}\n\n/**\n * Render the TS type for a zoned `array` field: an object with optional\n * `nonRepeatable` (a fixed block) and/or `repeatable` (a list of blocks). Recurses\n * through zone fields, so a zone field that is itself a zoned `array` nests naturally.\n */\nfunction arrayZoneType(field: ContentModelField, indent: string): string {\n const zones = field.config?.zones;\n const parts: string[] = [];\n if (zones?.nonRepeatable?.length) {\n const nested = fieldsToBody(zones.nonRepeatable, indent + \" \");\n parts.push(`${indent} readonly nonRepeatable?: {\\n${nested}\\n${indent} };`);\n }\n if (zones?.repeatable?.fields?.length) {\n const nested = fieldsToBody(zones.repeatable.fields, indent + \" \");\n parts.push(`${indent} readonly repeatable?: Array<{\\n${nested}\\n${indent} }>;`);\n }\n if (parts.length === 0) return \"Record<string, unknown>\"; // zoned array with no fields yet\n return `{\\n${parts.join(\"\\n\")}\\n${indent}}`;\n}\n\n/** Render the body of an object type from a field list, recursing into zones. */\nfunction fieldsToBody(fields: ContentModelField[], indent: string): string {\n const lines: string[] = [];\n for (const field of fields) {\n const optional = field.required ? \"\" : \"?\";\n let typeExpr: string;\n\n if (field.type === \"array\" && field.config?.zones) {\n typeExpr = arrayZoneType(field, indent);\n } else {\n typeExpr = scalarType(field);\n }\n\n const safeLabel = field.label ? escapeJsDoc(field.label) : \"\";\n if (safeLabel && safeLabel !== field.key) {\n lines.push(`${indent}/** ${safeLabel} */`);\n }\n lines.push(`${indent}readonly ${propName(field.key)}${optional}: ${typeExpr};`);\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Generate a complete `.ts` module from a set of content models.\n * Deterministic: models are sorted by slug; field order is preserved as authored.\n */\nexport function generateTypes(\n models: GeneratableModel[],\n opts: GenerateOptions = {},\n): string {\n const version = opts.version ?? \"0.1.0\";\n // Code-unit sort (NOT localeCompare): locale/ICU-independent so the generated\n // file is byte-identical on every machine — committed output diffs cleanly.\n const sorted = [...models].sort((a, b) =>\n a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0,\n );\n\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Regenerate with: npx @bettercms-ai/codegen\n// Source of truth: your BetterCMS content models (the same schema the dashboard\n// builder and the MCP tools write). Re-run codegen after any schema change.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const interfaces: string[] = [];\n const mapEntries: string[] = [];\n // Different slugs can PascalCase to the same base name (e.g. \"case-study\" and\n // \"case_study\" → \"CaseStudy\"). Emitting two identical interfaces would silently\n // declaration-merge into one wrong type, so disambiguate with a numeric suffix.\n const usedNames = new Set<string>();\n\n for (const model of sorted) {\n const base = `${pascalCase(model.slug)}Fields`;\n let typeName = base;\n for (let n = 2; usedNames.has(typeName); n++) typeName = `${base}_${n}`;\n usedNames.add(typeName);\n\n const name = model.name ? escapeJsDoc(model.name) : \"\";\n const desc = model.description ? escapeJsDoc(model.description) : \"\";\n const doc = name\n ? `/**\\n * ${name}${desc ? ` — ${desc}` : \"\"}\\n * Model slug: \\`${model.slug}\\`\\n */\\n`\n : \"\";\n const body = model.fields.length\n ? fieldsToBody(model.fields, \" \")\n : \" // (no fields defined yet)\";\n interfaces.push(`${doc}export interface ${typeName} {\\n${body}\\n}`);\n mapEntries.push(` readonly ${JSON.stringify(model.slug)}: ${typeName};`);\n }\n\n const schemaMap = `/**\n * Registry mapping each model slug to its typed fields. The Next adapter uses this to\n * type \\`getEntry(\"blog\", ...)\\` by slug — autocomplete and exhaustiveness for free.\n */\nexport interface BetterCMSSchema {\n${mapEntries.join(\"\\n\") || \" // (no models defined yet)\"}\n}\n\n/** Union of all model slugs. */\nexport type BetterCMSModelSlug = keyof BetterCMSSchema;`;\n\n return [header, PREAMBLE, interfaces.join(\"\\n\\n\"), schemaMap, \"\"].join(\"\\n\");\n}\n","/**\n * @bettercms-ai/codegen — schema → Live Preview binding helper generator.\n *\n * Companion to {@link generateTypes}. Where that emits the *types*, this emits a\n * tiny, schema-derived runtime that stamps `data-bcms-field` / `data-bcms-kind`\n * attributes onto the elements a site author binds to CMS content. Those\n * attributes are what the dashboard's Live Preview editor reads to turn the real,\n * running site into an editable canvas (the parent maps `data-bcms-field` → its\n * internal `data-node-id` on frame load).\n *\n * Why a helper and not auto-injection: BetterCMS never renders the customer's DOM\n * — the site does. So binding is opt-in per element via a spread:\n *\n * import { bcms } from \"./bettercms.bindings.generated\";\n *\n * <h1 {...bcms.blog.title}>{entry.fields.title}</h1> // scalar\n * <li {...bcms.blog.tags.value(i)}>{tag}</li> // primitive-array item\n * <article {...bcms.blog.features.$(i)}> // array item root\n * <h3 {...bcms.blog.features.label(i)}>{f.label}</h3> // array item sub-field\n * </article>\n *\n * The attributes only appear when the site is built with `BCMS_ANNOTATE` set\n * (preview builds); a normal production build ships zero extra attributes, because\n * `bcmsField` returns `{}`. Same generated file, both builds — no separate mode.\n *\n * Pure + deterministic, exactly like the type generator: same models in → identical\n * string out (slug-sorted, field order preserved, no clock, no I/O). Field keys are\n * author/agent-controlled, so every embedded key is emitted as an escaped string\n * literal (never interpolated into code) — hostile input can't break the output.\n *\n * Grammar — mirrors what the editor's `fieldPathToNodeId` resolves:\n * `title` · `hero.heroTitle` · `hero.primaryCta.label` (group leaves, any depth)\n * `features[0]` · `features[0].label` · `intro.facts[0].label` (repeaters, one index)\n * Group (non-repeatable) zones recurse into nested binding objects; a repeater is an\n * object with `$(i)` (item root) + one accessor per scalar sub-field. Arrays nested\n * inside a repeater item (a second index) are still beyond what the editor can\n * address, so they are intentionally omitted rather than emitted as dead paths.\n */\n\nimport type { ContentModelField, ContentModelFieldType } from \"@bettercms-ai/types\";\nimport type { GeneratableModel, GenerateOptions } from \"./generate.js\";\n\nconst VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * Render a field key as an object property name. Keys aren't guaranteed to be valid\n * identifiers (e.g. \"my-field\", \"1title\"), so anything that isn't a bare identifier\n * is quoted — always valid TS. (Mirrors the same helper in `generate.ts`.)\n */\nfunction propName(key: string): string {\n return VALID_IDENT.test(key) ? key : JSON.stringify(key);\n}\n\n/**\n * The kind label written to `data-bcms-kind`, mapped to the editor's closed field-type\n * set (matches the dashboard adapter's `toEditorFieldType`): API-only types that have\n * no on-canvas control collapse to \"text\". Informational today — the editor derives the\n * authoritative kind from the loaded model — but kept truthful for debugging/forward use.\n */\nfunction bindingKind(t: ContentModelFieldType): string {\n switch (t) {\n case \"text\":\n case \"richtext\":\n case \"image\":\n case \"boolean\":\n case \"number\":\n case \"select\":\n case \"array\":\n return t;\n // reference / multi-reference / date / datetime → plain text in the editor v1.\n default:\n return \"text\";\n }\n}\n\n/**\n * Build a runtime path expression for an array element: a string literal split around\n * the index so it concatenates at call time. Both halves are JSON-escaped, so an\n * author-controlled key can never inject code. e.g. (\"features[\", \"].label\") →\n * `\"features[\" + i + \"].label\"`.\n */\nfunction indexedPath(prefix: string, suffix: string): string {\n return `${JSON.stringify(prefix)} + i + ${JSON.stringify(suffix)}`;\n}\n\n/** Render a repeater binding object: `$(i)` item root + one accessor per scalar\n * sub-field. `path` is the repeater's full (possibly dotted) field path. */\nfunction repeaterBinding(\n itemFields: ContentModelField[],\n path: string,\n indent: string,\n): string {\n const lines: string[] = [\n `${indent} $: (i: number) => bcmsField(${indexedPath(`${path}[`, \"]\")}, \"array\"),`,\n ];\n for (const sub of itemFields) {\n // A sub-field that is itself an array would need a second index the editor\n // can't address yet — skip it rather than emit a path that won't bind.\n if (sub.type === \"array\") continue;\n lines.push(\n `${indent} ${propName(sub.key)}: (i: number) => bcmsField(${indexedPath(`${path}[`, `].${sub.key}`)}, ${JSON.stringify(bindingKind(sub.type))}),`,\n );\n }\n return `{\\n${lines.join(\"\\n\")}\\n${indent}}`;\n}\n\n/** Render the binding for one field at `path`, recursing into group zones. */\nfunction fieldBinding(\n field: ContentModelField,\n prefix: string,\n indent: string,\n): string {\n const path = prefix ? `${prefix}.${field.key}` : field.key;\n const name = propName(field.key);\n\n if (field.type !== \"array\") {\n return `${indent}${name}: bcmsField(${JSON.stringify(path)}, ${JSON.stringify(bindingKind(field.type))}),`;\n }\n\n const zones = field.config?.zones;\n // Group (non-repeatable) → a nested object of dotted-path leaf bindings.\n if (zones?.nonRepeatable?.length) {\n const body = zones.nonRepeatable\n .map((child) => fieldBinding(child, path, `${indent} `))\n .join(\"\\n\");\n return `${indent}${name}: {\\n${body}\\n${indent}},`;\n }\n // Repeater → `$(i)` + scalar sub-field accessors.\n if (zones?.repeatable?.fields?.length) {\n return `${indent}${name}: ${repeaterBinding(zones.repeatable.fields, path, indent)},`;\n }\n // Primitive list (`config.itemType` or bare) → `$(i)` + synthetic `value(i)`.\n const lines = [\n `${indent} $: (i: number) => bcmsField(${indexedPath(`${path}[`, \"]\")}, \"array\"),`,\n `${indent} value: (i: number) => bcmsField(${indexedPath(`${path}[`, \"].value\")}, \"text\"),`,\n ];\n return `${indent}${name}: {\\n${lines.join(\"\\n\")}\\n${indent}},`;\n}\n\n/** Render the binding entries for one model's fields (field order preserved). */\nfunction fieldsToBindings(fields: ContentModelField[], indent: string): string {\n return fields.map((field) => fieldBinding(field, \"\", indent)).join(\"\\n\");\n}\n\n/** The self-contained runtime emitted once at the top of every bindings file. */\nconst PREAMBLE = `/**\n * True when this site is built for Live Preview annotation. Set \\`BCMS_ANNOTATE=1\\`\n * in the preview build only; unset (the default) ships zero binding attributes.\n * Read defensively so the module is safe in any runtime (browser, Node, edge).\n */\nconst BCMS_ANNOTATE: boolean = (() => {\n try {\n const v = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env?.BCMS_ANNOTATE;\n return v != null && v !== \"\" && v !== \"0\" && v !== \"false\";\n } catch {\n return false;\n }\n})();\n\n/**\n * Binding attributes for a CMS-bound element. Spread onto the element that renders a\n * field: \\`<h1 {...bcmsField(\"title\", \"text\")}>\\`. Returns \\`{}\\` unless BCMS_ANNOTATE\n * is set, so production markup is untouched.\n */\nexport function bcmsField(path: string, kind: string): Record<string, string> {\n return BCMS_ANNOTATE ? { \"data-bcms-field\": path, \"data-bcms-kind\": kind } : {};\n}\n`;\n\n/**\n * Generate the Live Preview bindings module from a set of content models.\n * Deterministic: models are sorted by slug; field order is preserved as authored.\n */\nexport function generateBindings(\n models: GeneratableModel[],\n opts: GenerateOptions = {},\n): string {\n const version = opts.version ?? \"0.1.0\";\n // Code-unit sort (NOT localeCompare) so output is byte-identical on every machine.\n const sorted = [...models].sort((a, b) =>\n a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0,\n );\n\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Live Preview field bindings. Regenerate with: npx @bettercms-ai/codegen --bindings-out <path>\n// Spread these onto the elements that render your content; they emit\n// data-bcms-field/data-bcms-kind only when the site is built with BCMS_ANNOTATE set.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const entries = sorted.map((model) => {\n const body = model.fields.length\n ? `\\n${fieldsToBindings(model.fields, \" \")}\\n `\n : \"\";\n return ` ${JSON.stringify(model.slug)}: {${body}},`;\n });\n\n const bcms = `/**\n * Field bindings keyed by model slug. Spread a binding onto the element that renders\n * that field. Arrays expose \\`$(i)\\` for the item element and one accessor per\n * (one-level) sub-field; primitive arrays expose \\`value(i)\\` for the item's scalar.\n */\nexport const bcms = {\n${entries.join(\"\\n\") || \" // (no models defined yet)\"}\n} as const;`;\n\n return [header, PREAMBLE, bcms, \"\"].join(\"\\n\");\n}\n","/**\n * @bettercms-ai/codegen — schema → typed React render components generator.\n *\n * Companion to {@link generateTypes} (types) and {@link generateBindings} (Live\n * Preview attributes). This emits a small, self-contained `.tsx` module with two\n * components that render the canonical Delivery field shapes CORRECTLY, so authors\n * never hand-roll the rendering that produces the classic bugs:\n *\n * - <RichText> renders the server-sanitized `html` via `dangerouslySetInnerHTML`,\n * instead of interpolating the value as a JSX child (which React escapes, so the\n * page shows literal `<p>…</p>` tags — the #6 escaped-richtext bug).\n * - <Image> reads the normalized image object's `.url`/`.altText`, instead of\n * treating the object as a string.\n *\n * The emitted module is intentionally generic (not per-model) and dependency-free\n * beyond React, so it is a drop-in: point codegen at a path and import the two\n * components. It is deterministic (no clock, no I/O) like the sibling generators.\n *\n * Security: `html` is the Delivery API's server-rendered, DOMPurify-sanitized output\n * (see the RichText type docs). `<RichText>` injects exactly that field. If a caller\n * passes HTML from another, untrusted source they must sanitize it themselves.\n */\n\nimport type { GenerateOptions } from \"./generate.js\";\n\n/**\n * Generate the `bettercms.components.tsx` module: typed `<RichText>` and `<Image>`\n * components for the canonical Delivery field shapes. Deterministic — same options\n * in, identical string out.\n */\nexport function generateComponents(opts: GenerateOptions = {}): string {\n const version = opts.version ?? \"0.1.0\";\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Regenerate with: npx @bettercms-ai/codegen --components-out <path>\n// Typed render components for BetterCMS field shapes. Use these instead of\n// hand-rendering richtext/image values — they render the canonical shapes correctly.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const body = `import * as React from \"react\";\n\n/** Rich-text value from the Delivery API. \\`html\\` is server-rendered + sanitized. */\nexport type RichTextValue = {\n readonly format: string;\n readonly value: unknown;\n readonly html?: string;\n};\n\n/** Normalized image/media value from the Delivery API. */\nexport interface BetterCMSImageValue {\n readonly url: string;\n readonly altText?: string | null;\n readonly width?: number;\n readonly height?: number;\n}\n\ntype RichTextProps = {\n /** The richtext field value (\\`entry.fields.someRichText\\`). */\n field?: RichTextValue | null;\n /** Element/component to render as. Default: \\`\"div\"\\`. */\n as?: React.ElementType;\n} & Omit<React.HTMLAttributes<HTMLElement>, \"dangerouslySetInnerHTML\" | \"children\">;\n\n/**\n * Render a richtext field as HTML. Uses the server-sanitized \\`html\\` via\n * \\`dangerouslySetInnerHTML\\` — NEVER interpolate a richtext value as a JSX child\n * (React escapes it, so the page shows literal tags). Renders nothing when unset.\n */\nexport function RichText({ field, as: Tag = \"div\", ...rest }: RichTextProps) {\n if (!field || !field.html) return null;\n return <Tag {...rest} dangerouslySetInnerHTML={{ __html: field.html }} />;\n}\n\ntype ImageProps = {\n /** The image field value (\\`entry.fields.someImage\\`). */\n field?: BetterCMSImageValue | null;\n /** Alt text override; defaults to the field's \\`altText\\`, then \\`\"\"\\`. */\n alt?: string;\n} & Omit<React.ImgHTMLAttributes<HTMLImageElement>, \"src\">;\n\n/**\n * Render an image field as an \\`<img>\\` from its normalized \\`.url\\`/\\`.altText\\`.\n * Renders nothing when unset. Pass \\`alt\\` to override the stored alt text.\n */\nexport function Image({ field, alt, ...rest }: ImageProps) {\n if (!field || !field.url) return null;\n return (\n <img\n src={field.url}\n alt={alt ?? field.altText ?? \"\"}\n width={field.width}\n height={field.height}\n {...rest}\n />\n );\n}\n`;\n\n return [header, body].join(\"\\n\");\n}\n"],"mappings":"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","names":["VALID_IDENT","propName","PREAMBLE"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/fetch-models.ts","../src/generate.ts","../src/bindings.ts","../src/components.ts"],"sourcesContent":["/**\n * `bettercms-codegen` — fetch a project's content models and write a typed `.ts` file.\n *\n * Designed for two call sites:\n * 1. A developer in their repo: npx @bettercms-ai/codegen --out src/bettercms.generated.ts\n * 2. The build-time GitHub Action: same command, key from a repo secret.\n *\n * Auth + endpoint come from flags or env (BETTERCMS_API_KEY, BETTERCMS_API_URL).\n */\n\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { fetchModels } from \"./fetch-models.js\";\nimport { generateTypes } from \"./generate.js\";\nimport { generateBindings } from \"./bindings.js\";\nimport { generateComponents } from \"./components.js\";\n\nconst VERSION = \"0.2.0\";\nconst DEFAULT_API_URL = \"https://api.bettercms.ai/api/v1\";\nconst DEFAULT_OUT = \"bettercms.generated.ts\";\n\ninterface CliArgs {\n apiUrl: string;\n apiKey: string | undefined;\n out: string;\n /** When set, also emit the Live Preview bindings module to this path. */\n bindingsOut: string | undefined;\n /** When set, also emit the typed React render components (.tsx) to this path. */\n componentsOut: string | undefined;\n help: boolean;\n}\n\nfunction parseArgs(argv: string[]): CliArgs {\n const args: CliArgs = {\n apiUrl: process.env.BETTERCMS_API_URL ?? DEFAULT_API_URL,\n apiKey: process.env.BETTERCMS_API_KEY,\n out: DEFAULT_OUT,\n bindingsOut: undefined,\n componentsOut: undefined,\n help: false,\n };\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n const next = () => argv[++i];\n switch (arg) {\n case \"--api-url\":\n args.apiUrl = next() ?? args.apiUrl;\n break;\n case \"--key\":\n case \"--api-key\":\n args.apiKey = next();\n break;\n case \"--out\":\n case \"-o\":\n args.out = next() ?? args.out;\n break;\n case \"--bindings-out\":\n args.bindingsOut = next();\n break;\n case \"--components-out\":\n args.componentsOut = next();\n break;\n case \"--help\":\n case \"-h\":\n args.help = true;\n break;\n }\n }\n return args;\n}\n\nconst HELP = `bettercms-codegen v${VERSION} — generate TypeScript types from your BetterCMS schema\n\nUsage:\n npx @bettercms-ai/codegen [options]\n\nOptions:\n -o, --out <path> Output file (default: ${DEFAULT_OUT})\n --bindings-out <path> Also emit the Live Preview bindings module to <path>\n --components-out <path> Also emit typed <RichText>/<Image> React components (.tsx) to <path>\n --api-url <url> Management API base (default: ${DEFAULT_API_URL})\n --key <key> Management API key (or set BETTERCMS_API_KEY)\n -h, --help Show this help\n\nEnv:\n BETTERCMS_API_KEY Management-scoped key (content:manage)\n BETTERCMS_API_URL Override the API base\n`;\n\nasync function main(): Promise<void> {\n const args = parseArgs(process.argv.slice(2));\n\n if (args.help) {\n process.stdout.write(HELP);\n return;\n }\n if (!args.apiKey) {\n process.stderr.write(\n \"error: no API key. Pass --key <key> or set BETTERCMS_API_KEY.\\n\",\n );\n process.exit(1);\n }\n\n const models = await fetchModels({ apiUrl: args.apiUrl, apiKey: args.apiKey });\n\n const outPath = resolve(process.cwd(), args.out);\n await mkdir(dirname(outPath), { recursive: true });\n await writeFile(outPath, generateTypes(models, { version: VERSION }), \"utf8\");\n\n const plural = models.length === 1 ? \"\" : \"s\";\n process.stdout.write(\n `✓ Generated ${models.length} model type${plural} → ${args.out}\\n`,\n );\n\n if (args.bindingsOut) {\n const bindingsPath = resolve(process.cwd(), args.bindingsOut);\n await mkdir(dirname(bindingsPath), { recursive: true });\n await writeFile(bindingsPath, generateBindings(models, { version: VERSION }), \"utf8\");\n process.stdout.write(`✓ Generated Live Preview bindings → ${args.bindingsOut}\\n`);\n }\n\n if (args.componentsOut) {\n const componentsPath = resolve(process.cwd(), args.componentsOut);\n await mkdir(dirname(componentsPath), { recursive: true });\n await writeFile(componentsPath, generateComponents({ version: VERSION }), \"utf8\");\n process.stdout.write(`✓ Generated render components → ${args.componentsOut}\\n`);\n }\n}\n\nmain().catch((err: unknown) => {\n process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(1);\n});\n","/**\n * Fetches content models from the BetterCMS Management API so the CLI can generate\n * types against a live project. Kept dependency-free (plain fetch) so the generated\n * artifact and this fetcher can run anywhere — a GitHub Action, a postinstall, a script.\n */\n\nimport type { GeneratableModel } from \"./generate.js\";\n\nexport interface FetchModelsOptions {\n /** Management API base, e.g. \"https://api.bettercms.ai/api/v1\". */\n apiUrl: string;\n /** A management-scoped key (content:manage) or device-minted token. */\n apiKey: string;\n /** Optional fetch override (testing / custom runtime). */\n fetchImpl?: typeof fetch;\n}\n\ninterface ManagedModelRow {\n slug: string;\n name?: string;\n description?: string | null;\n fields: GeneratableModel[\"fields\"];\n}\n\n/**\n * GET /management/content/models — returns the project's models (the key is\n * project-scoped server-side, so this is exactly the schema for this site).\n */\nexport async function fetchModels(\n opts: FetchModelsOptions,\n): Promise<GeneratableModel[]> {\n const doFetch = opts.fetchImpl ?? globalThis.fetch;\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const url = `${base}/management/content/models`;\n\n let res: Response;\n try {\n res = await doFetch(url, {\n // No Content-Type: this is a bodyless GET; the header is incorrect here and\n // strict edge runtimes/proxies may reject it.\n headers: { Authorization: `Bearer ${opts.apiKey}`, Accept: \"application/json\" },\n });\n } catch (err) {\n throw new Error(\n `Could not reach the BetterCMS Management API at ${url}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n\n if (!res.ok) {\n const hint =\n res.status === 401 || res.status === 403\n ? \" — check your management API key (it must have the content:manage scope).\"\n : \"\";\n throw new Error(`Management API returned ${res.status} ${res.statusText}${hint}`);\n }\n\n const body = (await res.json()) as { data?: ManagedModelRow[] };\n const rows = body.data ?? [];\n return rows.map((r) => ({\n slug: r.slug,\n name: r.name,\n description: r.description ?? null,\n fields: r.fields ?? [],\n }));\n}\n","/**\n * @bettercms-ai/codegen — schema → TypeScript generator (the single source of truth).\n *\n * Both the dashboard schema builder and the MCP `create_model`/`add_field` tools write\n * the SAME `content_models.fields` (an array of `ContentModelField`). This generator maps\n * that one array into TypeScript. Because there is exactly one schema representation, the\n * generated types can never drift from the editor or the agent — they are the same source.\n *\n * Pure + deterministic: same models in → identical string out (stable ordering, no clock,\n * no I/O). That makes it trivially testable and safe to commit + diff in a customer repo.\n */\n\nimport type { ContentModelField, ContentModelFieldType } from \"@bettercms-ai/types\";\n\n/** Minimal model shape the generator needs — a subset of the Management API model row. */\nexport interface GeneratableModel {\n /** Machine-safe slug, e.g. \"blog\" or \"case-study\". Used for the schema-map key. */\n slug: string;\n /** Human name, used only for the JSDoc header. */\n name?: string;\n description?: string | null;\n fields: ContentModelField[];\n}\n\nexport interface GenerateOptions {\n /** Generator version stamped into the header (defaults to the package version). */\n version?: string;\n /** Override the banner timestamp source — omitted by default so output is deterministic. */\n bannerComment?: string;\n}\n\n/** Helper types emitted once at the top of every generated file (self-contained, zero-dep). */\nconst PREAMBLE = `/**\n * Rich-text field value returned by the Delivery API.\n *\n * - \\`format\\`/\\`value\\`: the portable, editor-agnostic payload (Lexical EditorState) —\n * render it with your editor's serializer for full fidelity.\n * - \\`html\\`: server-rendered, sanitized HTML (computed render-on-write). Present on\n * Delivery reads; the simplest path for non-React consumers — safe to inject directly\n * (e.g. \\`dangerouslySetInnerHTML\\`). Optional: legacy/un-normalized values may omit it.\n *\n * The \\`{ format, value }\\` contract is unchanged; \\`html\\` is additive.\n */\n/**\n * One Portable Text block, or a block object (an image, an embed, a placed component).\n *\n * Inlined rather than imported: this preamble is emitted INTO your repo and is deliberately\n * dependency-free. To render structure, \\`npm i @portabletext/react\\` and pass\n * \\`portableText(field)\\` to it; to render without adding anything, keep using \\`rich()\\`.\n */\nexport type PortableTextBlock = {\n readonly _type: string;\n readonly _key: string;\n readonly style?: string;\n readonly listItem?: string;\n readonly level?: number;\n readonly markDefs?: readonly { readonly _type: string; readonly _key: string; readonly [k: string]: unknown }[];\n readonly children?: readonly { readonly _type: string; readonly _key: string; readonly text?: string; readonly marks?: readonly string[] }[];\n readonly [k: string]: unknown;\n};\n\nexport type RichText = {\n /**\n * The storage format. Deliberately \\`string\\` and NOT a literal union: a project mid-backfill\n * holds both \\`\"lexical-…\"\\` and \\`\"portable-text-1\"\\` values, and narrowing this would give a\n * type error to anyone regenerating types against it.\n */\n readonly format: string;\n /** Portable Text blocks when \\`format\\` is \\`\"portable-text-1\"\\`; editor state otherwise. */\n readonly value: unknown;\n /**\n * Server-rendered, sanitized HTML. ALWAYS present, in every format, forever — sites built\n * before Portable Text existed read this directly and cannot be rebuilt.\n */\n readonly html?: string;\n /**\n * @deprecated Superseded by Portable Text — read \\`portableText(field)\\` instead. Retained so\n * entries written before the migration keep type-checking; nothing mints it any more.\n *\n * Structured blocks. Present on Body (\\`document\\`) fields ONLY, and optional even there —\n * it is derived at write time, so an entry saved before this existed carries none until its\n * next save, and there is no backfill. Branch on its absence; \\`html\\` is always there.\n *\n * \\`id\\` is stable within ONE document, never a global key — two entries both have a \"0.0\".\n * A cross-document anchor is (entryId, fieldKey, id).\n */\n readonly doc?: {\n readonly version: 1;\n readonly blocks: readonly { readonly id: string; readonly type: string; readonly [k: string]: unknown }[];\n };\n};\n\n/**\n * A field that may arrive as EITHER shape.\n *\n * Switching a field between \\`text\\` and \\`richtext\\` in the CMS switches what Delivery\n * returns for it — a bare string becomes \\`{ format, value, html }\\`. Type author-editable\n * text with this and read it through \\`plain()\\`/\\`rich()\\` below, and that switch stops being\n * a site-breaking change. Interpolating the value directly renders \\`[object Object]\\`.\n */\nexport type TextOrRich = string | RichText | null | undefined;\n\n/** True when the value is a rich-text envelope rather than a bare string. */\nexport function isRichText(value: unknown): value is RichText {\n return (\n typeof value === \"object\" && value !== null && !Array.isArray(value) &&\n (\"html\" in value || \"format\" in value)\n );\n}\n\n/** The marker on a Portable Text envelope. */\nexport const PORTABLE_TEXT_FORMAT = \"portable-text-1\";\n\n/**\n * Portable Text blocks when the field stores them, otherwise null.\n *\n * ADDITIVE. \\`rich()\\` and \\`plain()\\` keep working exactly as before on every format, so\n * nothing you have already shipped needs to change. Use this only if you want to render the\n * structure yourself — for example with \\`@portabletext/react\\`.\n */\nexport function portableText(value: TextOrRich): readonly PortableTextBlock[] | null {\n if (!isRichText(value)) return null;\n return value.format === PORTABLE_TEXT_FORMAT && Array.isArray(value.value)\n ? (value.value as readonly PortableTextBlock[])\n : null;\n}\n\n/** Plain text for attribute contexts — \\`<title>\\`, meta description, JSON-LD, \\`alt\\`. */\nexport function plain(value: TextOrRich): string {\n if (typeof value === \"string\") return value;\n if (!isRichText(value) || typeof value.html !== \"string\") return \"\";\n return decodeEntities(value.html.replace(/<[^>]+>/g, \"\")).trim();\n}\n\n/**\n * Renderable HTML, for \\`set:html\\` / \\`dangerouslySetInnerHTML\\`. Rich text keeps its inline\n * marks (the server sanitizes \\`html\\` on write); a bare string is escaped, so a plain field\n * can never inject markup. A LONE wrapping block is unwrapped — a field switched from\n * \\`text\\` stores \\`<p>…</p>\\`, and \\`<h1><p>…</p></h1>\\` is invalid HTML (the parser closes the\n * heading early, dropping the text out of it). Real block structure is left alone.\n */\nexport function rich(value: TextOrRich, fallback = \"\"): string {\n const html = (\n typeof value === \"string\" ? escapeHtml(value) : isRichText(value) ? (value.html ?? \"\") : \"\"\n ).trim();\n return html ? unwrapLoneBlock(html) : escapeHtml(fallback);\n}\n\nfunction unwrapLoneBlock(html: string): string {\n const m = html.match(/^<(p|div|h[1-6])(?:\\\\s[^>]*)?>([\\\\s\\\\S]*)<\\\\/\\\\1>$/i);\n return m && !new RegExp(\\`</\\${m[1]}>\\`, \"i\").test(m[2]) ? m[2] : html;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nfunction decodeEntities(s: string): string {\n return s\n .replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\").replace(/&quot;/g, '\"')\n .replace(/&#0?39;/g, \"'\").replace(/&nbsp;/g, \" \").replace(/&amp;/g, \"&\");\n}\n\n/**\n * Image / media field value as stored and returned verbatim by the Delivery API\n * (server-normalized on write to the canonical shape). \\`url\\` is always present; an\n * unresolved/external value may carry only \\`url\\`. \\`altText\\` is the accessibility text\n * for \\`<img alt>\\`.\n */\nexport interface BetterCMSImage {\n readonly id?: string;\n readonly url: string;\n readonly name?: string;\n readonly altText?: string | null;\n readonly width?: number;\n readonly height?: number;\n}\n\n/**\n * A component slot's value, as stored and delivered.\n *\n * \\`componentId\\` points at a component definition; \\`overrides\\` are the author's values,\n * keyed by the component's declared prop keys.\n *\n * \\`resolved\\` is the SNAPSHOT: at publish time the component is resolved (its block tree\n * with the overrides applied) and frozen onto the published value. That is why editing a\n * component does not silently rewrite entries that were already published — a published\n * entry carries what it was published with until it is published again.\n *\n * Read \\`resolved\\` when it is there; it is absent on draft-perspective reads, where you\n * should resolve \\`componentId\\` yourself against the components endpoint.\n */\nexport interface BetterCMSComponentRef {\n readonly componentId: string;\n readonly overrides?: Readonly<Record<string, unknown>>;\n readonly resolved?: readonly unknown[];\n}\n\n/**\n * One block in a section zone, as stored and delivered.\n *\n * This is the SAME shape a page's \\`blockJson\\` holds — a section zone is a composable page\n * region, so it delivers page blocks, not a shape of its own. Deliberately structural rather\n * than a discriminated union over every block type: the container blocks\n * (\\`columns\\`, \\`section\\`, \\`slider\\`, \\`tabs\\`) nest \\`BetterCMSBlock\\` inside \\`props\\`,\n * and the block vocabulary is server-side and versioned independently of any generated SDK.\n * Narrow on \\`type\\` at the call site.\n *\n * \\`style\\` carries the block's design tokens when the author set any.\n */\nexport interface BetterCMSBlock {\n readonly type: string;\n readonly id: string;\n readonly props?: Readonly<Record<string, unknown>>;\n readonly style?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Delivery envelope around a model's typed \\`data\\`. \\`getEntry\\`/\\`listEntries\\` in the\n * Next adapter return this shape, with \\`fields\\` typed by the model.\n */\nexport interface BetterCMSEntry<TFields> {\n readonly slug: string;\n readonly status: \"draft\" | \"published\";\n readonly fields: TFields;\n readonly updatedAt: string;\n}\n\n/** Fully resolved published or preview Layout returned beside a delivered page. */\nexport interface BetterCMSLayout {\n readonly version: 1;\n readonly nodes: ReadonlyArray<\n | { readonly kind: \"page-content\"; readonly id: \"page-content\" }\n | {\n readonly kind: \"section\";\n readonly id: string;\n readonly slug: string;\n readonly name: string;\n readonly source: \"inherit\" | \"override-content\" | \"customize-structure\" | \"page-only\" | \"detached\";\n }\n >;\n readonly sections: Readonly<Record<string, {\n readonly id: string;\n readonly slug: string;\n readonly name: string;\n /** Headless Section values, including values represented by direct field items. */\n readonly fields: Readonly<Record<string, unknown>>;\n readonly items: ReadonlyArray<\n | { readonly id: string; readonly kind: \"field\"; readonly fieldId: string; readonly value: unknown }\n | {\n readonly id: string;\n readonly kind: \"component\";\n readonly componentId: string;\n readonly variantGroupId?: string;\n readonly canonicalInputs: readonly BetterCMSLayoutInput[];\n readonly bindings: ReadonlyArray<{ readonly inputId: string; readonly fieldId: string }>;\n readonly canonicalValues: Readonly<Record<string, unknown>>;\n readonly resolvedProps: Readonly<Record<string, unknown>>;\n readonly blocks: readonly unknown[];\n }\n >;\n }>>;\n}\n\n/** Recursive canonical input description exposed with each delivered Component item. */\nexport interface BetterCMSLayoutInput {\n readonly id: string;\n readonly slug: string;\n readonly label: string;\n readonly type: string;\n readonly required?: boolean;\n readonly defaultValue?: unknown;\n readonly config?: Readonly<Record<string, unknown>>;\n readonly fields?: readonly BetterCMSLayoutInput[];\n}\n`;\n\n/** PascalCase an identifier from a slug: \"case-study\" → \"CaseStudy\". */\nfunction pascalCase(slug: string): string {\n const parts = slug.split(/[-_\\s]+/).filter(Boolean);\n const pascal = parts\n .map((p) => p.charAt(0).toUpperCase() + p.slice(1))\n .join(\"\");\n // Guard against an identifier that starts with a digit (invalid TS type name).\n return /^[0-9]/.test(pascal) ? `Model${pascal}` : pascal || \"Model\";\n}\n\n/**\n * Make a string safe to embed inside a `/** ... *​/` JSDoc comment. A field label\n * (free-text, author/agent-controlled) could contain `*​/` — which closes the comment\n * early and injects the remainder as code — or a newline, which breaks the single-line\n * comment. Both are neutralized here. Without this, hostile content produces non-\n * compiling (or worse, code-injected) output.\n */\nfunction escapeJsDoc(text: string): string {\n return text.replace(/\\*\\//g, \"* /\").replace(/[\\r\\n]+/g, \" \").trim();\n}\n\nconst VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * Render a field key as a TS property name. Field keys are author/agent-controlled and\n * not guaranteed to be valid identifiers (e.g. \"my-field\", \"1title\", \"\"), so anything\n * that isn't a bare identifier is emitted as a quoted property name — always valid TS.\n */\nfunction propName(key: string): string {\n return VALID_IDENT.test(key) ? key : JSON.stringify(key);\n}\n\n/** A scalar/primitive field maps to a TS type expression (no nesting). */\nfunction scalarType(field: ContentModelField): string {\n const t: ContentModelFieldType = field.type;\n switch (t) {\n case \"text\":\n return \"string\";\n case \"richtext\":\n // A document field stores the SAME {format, value, html} envelope as richtext — the\n // difference is the editor and the placement, not the wire shape. So it maps to the same\n // generated type, and the `doc` rendition lands on `RichText` itself rather than here.\n case \"document\":\n return \"RichText\";\n case \"image\":\n return \"BetterCMSImage\";\n case \"boolean\":\n return \"boolean\";\n case \"number\":\n return \"number\";\n case \"date\":\n case \"datetime\":\n return \"string\"; // ISO 8601\n case \"select\": {\n const opts = field.options?.filter((o) => typeof o === \"string\") ?? [];\n return opts.length > 0\n ? opts.map((o) => JSON.stringify(o)).join(\" | \")\n : \"string\";\n }\n case \"reference\":\n return \"string\"; // referenced entry id\n // Both spellings are live (see ContentModelFieldType). Template- and Webflow-seeded\n // models carry the camelCase one; until it was handled here it fell through to the\n // exhaustiveness default, so generated types for every template-created collection\n // typed this field as `unknown` instead of `string[]`.\n case \"multi-reference\":\n case \"multiReference\":\n return \"string[]\"; // referenced entry ids\n case \"array\": {\n // Zoned arrays (config.zones) are expanded by fieldsToBody before reaching here;\n // this branch handles only the primitive list form (config.itemType).\n const itemType = field.config?.itemType ?? \"text\";\n const inner =\n itemType === \"number\" ? \"number\" : \"string\"; // text | date → string\n return `${inner}[]`;\n }\n // ── Builder scalars ────────────────────────────────────────────────────────\n // All string-shaped on the wire; each is value-validated on write (see\n // src/lib/content/reference-validation.ts), so the generated type is the\n // narrowest thing that is actually true of the stored value.\n case \"longtext\":\n case \"slug\":\n case \"email\":\n case \"phone\":\n case \"link\":\n case \"color\":\n return \"string\";\n case \"json\":\n // Arbitrary author-supplied JSON — object or parseable string. `unknown`\n // forces the consumer to narrow, which is correct: we genuinely don't know.\n return \"unknown\";\n case \"component-ref\":\n // The delivered value is the reference plus, on published reads, its frozen tree.\n return \"BetterCMSComponentRef\";\n case \"modular\":\n // ponytail: `data` stays an open record. A per-block discriminated union would need\n // every allowed block model resolved at generate time (they are separate rows, and\n // this function only sees the field), and consumers narrow on __type anyway. Emitting\n // the union later is additive — it only makes an existing `unknown` more specific.\n return \"ReadonlyArray<{ readonly __id: string; readonly __type: string; readonly data: Record<string, unknown> }>\";\n case \"sections\": {\n const config = field.config as {\n mode?: unknown;\n allowedSections?: unknown;\n legacyResolved?: unknown;\n } | null | undefined;\n // Creating the durable authored Zone intentionally preserves `allowedSections` until\n // migration is explicitly confirmed. During that window the writer, validator and\n // renderer still use the legacy BetterCMSBlock[] value, so codegen must tell the same\n // truth instead of exposing the authored instance shape one release too early.\n const unresolvedLegacy = config?.mode === \"authored-v2\"\n && Object.prototype.hasOwnProperty.call(config, \"allowedSections\")\n && config.legacyResolved !== true;\n if (config?.mode === \"authored-v2\" && !unresolvedLegacy) {\n return \"ReadonlyArray<{ readonly __id: string; readonly __section: string; readonly __type: string; readonly data: Record<string, unknown> }>\";\n }\n // The value IS the page block tree, so it gets the public block type rather than a\n // shape of its own — a section zone and a page's blockJson deliver the identical\n // thing, and emitting two names for one shape is how a consumer ends up writing a\n // converter between them.\n return \"ReadonlyArray<BetterCMSBlock>\";\n }\n case \"location\":\n return \"{ readonly lat: number; readonly lng: number; readonly label?: string }\";\n case \"file\":\n // The file envelope is the image envelope minus the pixel dimensions\n // ({ url, name?, ... }), and BetterCMSImage's width/height are optional — so\n // every file value is already a valid BetterCMSImage. Reused rather than\n // emitting a second near-identical public type into every generated SDK.\n return \"BetterCMSImage\";\n default: {\n // Exhaustiveness guard: if a new field type is added to the union and not\n // mapped here, this line becomes a compile error in the codegen build.\n const _exhaustive: never = t;\n return \"unknown\";\n }\n }\n}\n\n/**\n * Render the TS type for a zoned `array` field: an object with optional\n * `nonRepeatable` (a fixed block) and/or `repeatable` (a list of blocks). Recurses\n * through zone fields, so a zone field that is itself a zoned `array` nests naturally.\n */\nfunction arrayZoneType(field: ContentModelField, indent: string): string {\n const zones = field.config?.zones;\n const parts: string[] = [];\n if (zones?.nonRepeatable?.length) {\n const nested = fieldsToBody(zones.nonRepeatable, indent + \" \");\n parts.push(`${indent} readonly nonRepeatable?: {\\n${nested}\\n${indent} };`);\n }\n if (zones?.repeatable?.fields?.length) {\n const nested = fieldsToBody(zones.repeatable.fields, indent + \" \");\n parts.push(`${indent} readonly repeatable?: Array<{\\n${nested}\\n${indent} }>;`);\n }\n if (parts.length === 0) return \"Record<string, unknown>\"; // zoned array with no fields yet\n return `{\\n${parts.join(\"\\n\")}\\n${indent}}`;\n}\n\n/** Render the body of an object type from a field list, recursing into zones. */\nfunction fieldsToBody(fields: ContentModelField[], indent: string): string {\n const lines: string[] = [];\n for (const field of fields) {\n const optional = field.required ? \"\" : \"?\";\n let typeExpr: string;\n\n if (field.type === \"array\" && field.config?.zones) {\n typeExpr = arrayZoneType(field, indent);\n } else {\n typeExpr = scalarType(field);\n }\n\n const safeLabel = field.label ? escapeJsDoc(field.label) : \"\";\n if (safeLabel && safeLabel !== field.key) {\n lines.push(`${indent}/** ${safeLabel} */`);\n }\n lines.push(`${indent}readonly ${propName(field.key)}${optional}: ${typeExpr};`);\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Generate a complete `.ts` module from a set of content models.\n * Deterministic: models are sorted by slug; field order is preserved as authored.\n */\nexport function generateTypes(\n models: GeneratableModel[],\n opts: GenerateOptions = {},\n): string {\n const version = opts.version ?? \"0.1.0\";\n // Code-unit sort (NOT localeCompare): locale/ICU-independent so the generated\n // file is byte-identical on every machine — committed output diffs cleanly.\n const sorted = [...models].sort((a, b) =>\n a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0,\n );\n\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Regenerate with: npx @bettercms-ai/codegen\n// Source of truth: your BetterCMS content models (the same schema the dashboard\n// builder and the MCP tools write). Re-run codegen after any schema change.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const interfaces: string[] = [];\n const mapEntries: string[] = [];\n // Different slugs can PascalCase to the same base name (e.g. \"case-study\" and\n // \"case_study\" → \"CaseStudy\"). Emitting two identical interfaces would silently\n // declaration-merge into one wrong type, so disambiguate with a numeric suffix.\n const usedNames = new Set<string>();\n\n for (const model of sorted) {\n const base = `${pascalCase(model.slug)}Fields`;\n let typeName = base;\n for (let n = 2; usedNames.has(typeName); n++) typeName = `${base}_${n}`;\n usedNames.add(typeName);\n\n const name = model.name ? escapeJsDoc(model.name) : \"\";\n const desc = model.description ? escapeJsDoc(model.description) : \"\";\n const doc = name\n ? `/**\\n * ${name}${desc ? ` — ${desc}` : \"\"}\\n * Model slug: \\`${model.slug}\\`\\n */\\n`\n : \"\";\n const body = model.fields.length\n ? fieldsToBody(model.fields, \" \")\n : \" // (no fields defined yet)\";\n interfaces.push(`${doc}export interface ${typeName} {\\n${body}\\n}`);\n mapEntries.push(` readonly ${JSON.stringify(model.slug)}: ${typeName};`);\n }\n\n const schemaMap = `/**\n * Registry mapping each model slug to its typed fields. The Next adapter uses this to\n * type \\`getEntry(\"blog\", ...)\\` by slug — autocomplete and exhaustiveness for free.\n */\nexport interface BetterCMSSchema {\n${mapEntries.join(\"\\n\") || \" // (no models defined yet)\"}\n}\n\n/** Union of all model slugs. */\nexport type BetterCMSModelSlug = keyof BetterCMSSchema;`;\n\n return [header, PREAMBLE, interfaces.join(\"\\n\\n\"), schemaMap, \"\"].join(\"\\n\");\n}\n","/**\n * @bettercms-ai/codegen — schema → Live Preview binding helper generator.\n *\n * Companion to {@link generateTypes}. Where that emits the *types*, this emits a\n * tiny, schema-derived runtime that stamps `data-bcms-field` / `data-bcms-kind`\n * attributes onto the elements a site author binds to CMS content. Those\n * attributes are what the dashboard's Live Preview editor reads to turn the real,\n * running site into an editable canvas (the parent maps `data-bcms-field` → its\n * internal `data-node-id` on frame load).\n *\n * Why a helper and not auto-injection: BetterCMS never renders the customer's DOM\n * — the site does. So binding is opt-in per element via a spread:\n *\n * import { bcms } from \"./bettercms.bindings.generated\";\n *\n * <h1 {...bcms.blog.title}>{entry.fields.title}</h1> // scalar\n * <li {...bcms.blog.tags.value(i)}>{tag}</li> // primitive-array item\n * <article {...bcms.blog.features.$(i)}> // array item root\n * <h3 {...bcms.blog.features.label(i)}>{f.label}</h3> // array item sub-field\n * </article>\n *\n * The attributes are always emitted — inert `data-*` attributes on any build, exactly\n * like Storyblok's `data-blok-c` / Sanity's `data-sanity` — so a site is editable no\n * matter which pipeline built it. One generated file, no build modes.\n *\n * Pure + deterministic, exactly like the type generator: same models in → identical\n * string out (slug-sorted, field order preserved, no clock, no I/O). Field keys are\n * author/agent-controlled, so every embedded key is emitted as an escaped string\n * literal (never interpolated into code) — hostile input can't break the output.\n *\n * Grammar — mirrors what the editor's `fieldPathToNodeId` resolves:\n * `title` · `hero.heroTitle` · `hero.primaryCta.label` (group leaves, any depth)\n * `features[0]` · `features[0].label` · `intro.facts[0].label` (repeaters, one index)\n * Group (non-repeatable) zones recurse into nested binding objects; a repeater is an\n * object with `$(i)` (item root) + one accessor per scalar sub-field. Arrays nested\n * inside a repeater item (a second index) are still beyond what the editor can\n * address, so they are intentionally omitted rather than emitted as dead paths.\n */\n\nimport type { ContentModelField, ContentModelFieldType } from \"@bettercms-ai/types\";\nimport type { GeneratableModel, GenerateOptions } from \"./generate.js\";\n\nconst VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * Render a field key as an object property name. Keys aren't guaranteed to be valid\n * identifiers (e.g. \"my-field\", \"1title\"), so anything that isn't a bare identifier\n * is quoted — always valid TS. (Mirrors the same helper in `generate.ts`.)\n */\nfunction propName(key: string): string {\n return VALID_IDENT.test(key) ? key : JSON.stringify(key);\n}\n\n/**\n * The kind label written to `data-bcms-kind`, mapped to the editor's closed field-type\n * set (matches the dashboard adapter's `toEditorFieldType`): API-only types that have\n * no on-canvas control collapse to \"text\". Informational today — the editor derives the\n * authoritative kind from the loaded model — but kept truthful for debugging/forward use.\n */\nfunction bindingKind(t: ContentModelFieldType): string {\n switch (t) {\n case \"text\":\n case \"richtext\":\n case \"image\":\n case \"boolean\":\n case \"number\":\n case \"select\":\n case \"array\":\n return t;\n case \"document\":\n return \"richtext\";\n // reference / multi-reference / date / datetime → plain text in the editor v1.\n default:\n return \"text\";\n }\n}\n\n/**\n * Build a runtime path expression for an array element: a string literal split around\n * the index so it concatenates at call time. Both halves are JSON-escaped, so an\n * author-controlled key can never inject code. e.g. (\"features[\", \"].label\") →\n * `\"features[\" + i + \"].label\"`.\n */\nfunction indexedPath(prefix: string, suffix: string): string {\n return `${JSON.stringify(prefix)} + i + ${JSON.stringify(suffix)}`;\n}\n\n/** Render a repeater binding object: `$(i)` item root + one accessor per scalar\n * sub-field. `path` is the repeater's full (possibly dotted) field path. */\nfunction repeaterBinding(\n itemFields: ContentModelField[],\n path: string,\n indent: string,\n): string {\n const lines: string[] = [\n `${indent} $: (i: number) => bcmsField(${indexedPath(`${path}[`, \"]\")}, \"array\"),`,\n ];\n for (const sub of itemFields) {\n // A sub-field that is itself an array would need a second index the editor\n // can't address yet — skip it rather than emit a path that won't bind.\n if (sub.type === \"array\") continue;\n lines.push(\n `${indent} ${propName(sub.key)}: (i: number) => bcmsField(${indexedPath(`${path}[`, `].${sub.key}`)}, ${JSON.stringify(bindingKind(sub.type))}),`,\n );\n }\n return `{\\n${lines.join(\"\\n\")}\\n${indent}}`;\n}\n\n/** Render the binding for one field at `path`, recursing into group zones. */\nfunction fieldBinding(\n field: ContentModelField,\n prefix: string,\n indent: string,\n): string {\n const path = prefix ? `${prefix}.${field.key}` : field.key;\n const name = propName(field.key);\n\n if (field.type !== \"array\") {\n return `${indent}${name}: bcmsField(${JSON.stringify(path)}, ${JSON.stringify(bindingKind(field.type))}),`;\n }\n\n const zones = field.config?.zones;\n // Group (non-repeatable) → a nested object of dotted-path leaf bindings.\n if (zones?.nonRepeatable?.length) {\n const body = zones.nonRepeatable\n .map((child) => fieldBinding(child, path, `${indent} `))\n .join(\"\\n\");\n return `${indent}${name}: {\\n${body}\\n${indent}},`;\n }\n // Repeater → `$(i)` + scalar sub-field accessors.\n if (zones?.repeatable?.fields?.length) {\n return `${indent}${name}: ${repeaterBinding(zones.repeatable.fields, path, indent)},`;\n }\n // Primitive list (`config.itemType` or bare) → `$(i)` + synthetic `value(i)`.\n const lines = [\n `${indent} $: (i: number) => bcmsField(${indexedPath(`${path}[`, \"]\")}, \"array\"),`,\n `${indent} value: (i: number) => bcmsField(${indexedPath(`${path}[`, \"].value\")}, \"text\"),`,\n ];\n return `${indent}${name}: {\\n${lines.join(\"\\n\")}\\n${indent}},`;\n}\n\n/** Render the binding entries for one model's fields (field order preserved). */\nfunction fieldsToBindings(fields: ContentModelField[], indent: string): string {\n return fields.map((field) => fieldBinding(field, \"\", indent)).join(\"\\n\");\n}\n\n/** The self-contained runtime emitted once at the top of every bindings file. */\nconst PREAMBLE = `/**\n * Binding attributes for a CMS-bound element. Spread onto the element that renders a\n * field: \\`<h1 {...bcmsField(\"title\", \"text\")}>\\`. Always emitted, on every build —\n * two inert \\`data-*\\` attributes are what makes the site editable in Live Preview.\n */\nexport function bcmsField(path: string, kind: string): Record<string, string> {\n return { \"data-bcms-field\": path, \"data-bcms-kind\": kind };\n}\n`;\n\n/**\n * Generate the Live Preview bindings module from a set of content models.\n * Deterministic: models are sorted by slug; field order is preserved as authored.\n */\nexport function generateBindings(\n models: GeneratableModel[],\n opts: GenerateOptions = {},\n): string {\n const version = opts.version ?? \"0.1.0\";\n // Code-unit sort (NOT localeCompare) so output is byte-identical on every machine.\n const sorted = [...models].sort((a, b) =>\n a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0,\n );\n\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Live Preview field bindings. Regenerate with: npx @bettercms-ai/codegen --bindings-out <path>\n// Spread these onto the elements that render your content; they emit\n// data-bcms-field/data-bcms-kind on every build.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const entries = sorted.map((model) => {\n const body = model.fields.length\n ? `\\n${fieldsToBindings(model.fields, \" \")}\\n `\n : \"\";\n return ` ${JSON.stringify(model.slug)}: {${body}},`;\n });\n\n const bcms = `/**\n * Field bindings keyed by model slug. Spread a binding onto the element that renders\n * that field. Arrays expose \\`$(i)\\` for the item element and one accessor per\n * (one-level) sub-field; primitive arrays expose \\`value(i)\\` for the item's scalar.\n */\nexport const bcms = {\n${entries.join(\"\\n\") || \" // (no models defined yet)\"}\n} as const;`;\n\n return [header, PREAMBLE, bcms, \"\"].join(\"\\n\");\n}\n","/**\n * @bettercms-ai/codegen — schema → typed React render components generator.\n *\n * Companion to {@link generateTypes} (types) and {@link generateBindings} (Live\n * Preview attributes). This emits a small, self-contained `.tsx` module with two\n * components that render the canonical Delivery field shapes CORRECTLY, so authors\n * never hand-roll the rendering that produces the classic bugs:\n *\n * - <RichText> renders the server-sanitized `html` via `dangerouslySetInnerHTML`,\n * instead of interpolating the value as a JSX child (which React escapes, so the\n * page shows literal `<p>…</p>` tags — the #6 escaped-richtext bug).\n * - <Image> reads the normalized image object's `.url`/`.altText`, instead of\n * treating the object as a string.\n *\n * The emitted module is intentionally generic (not per-model) and dependency-free\n * beyond React, so it is a drop-in: point codegen at a path and import the two\n * components. It is deterministic (no clock, no I/O) like the sibling generators.\n *\n * Security: `html` is the Delivery API's server-rendered, DOMPurify-sanitized output\n * (see the RichText type docs). `<RichText>` injects exactly that field. If a caller\n * passes HTML from another, untrusted source they must sanitize it themselves.\n */\n\nimport type { GenerateOptions } from \"./generate.js\";\n\n/**\n * Generate the `bettercms.components.tsx` module: typed `<RichText>` and `<Image>`\n * components for the canonical Delivery field shapes. Deterministic — same options\n * in, identical string out.\n */\nexport function generateComponents(opts: GenerateOptions = {}): string {\n const version = opts.version ?? \"0.1.0\";\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Regenerate with: npx @bettercms-ai/codegen --components-out <path>\n// Typed render components for BetterCMS field shapes. Use these instead of\n// hand-rendering richtext/image values — they render the canonical shapes correctly.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const body = `import * as React from \"react\";\n\n/** Rich-text value from the Delivery API. \\`html\\` is server-rendered + sanitized. */\nexport type RichTextValue = {\n readonly format: string;\n readonly value: unknown;\n readonly html?: string;\n /**\n * Structured blocks — Body (\\`document\\`) fields only, and optional even there: derived at\n * write time, so entries saved before it existed carry none until re-saved. No backfill.\n * Block ids are stable within one document only, never a global key.\n */\n readonly doc?: { readonly version: 1; readonly blocks: readonly { readonly id: string; readonly type: string; readonly [k: string]: unknown }[] };\n};\n\n/** Normalized image/media value from the Delivery API. */\nexport interface BetterCMSImageValue {\n readonly url: string;\n readonly altText?: string | null;\n readonly width?: number;\n readonly height?: number;\n}\n\ntype RichTextProps = {\n /** The richtext field value (\\`entry.fields.someRichText\\`). */\n field?: RichTextValue | null;\n /** Element/component to render as. Default: \\`\"div\"\\`. */\n as?: React.ElementType;\n} & Omit<React.HTMLAttributes<HTMLElement>, \"dangerouslySetInnerHTML\" | \"children\">;\n\n/**\n * Render a richtext field as HTML. Uses the server-sanitized \\`html\\` via\n * \\`dangerouslySetInnerHTML\\` — NEVER interpolate a richtext value as a JSX child\n * (React escapes it, so the page shows literal tags). Renders nothing when unset.\n */\nexport function RichText({ field, as: Tag = \"div\", ...rest }: RichTextProps) {\n if (!field || !field.html) return null;\n return <Tag {...rest} dangerouslySetInnerHTML={{ __html: field.html }} />;\n}\n\ntype ImageProps = {\n /** The image field value (\\`entry.fields.someImage\\`). */\n field?: BetterCMSImageValue | null;\n /** Alt text override; defaults to the field's \\`altText\\`, then \\`\"\"\\`. */\n alt?: string;\n} & Omit<React.ImgHTMLAttributes<HTMLImageElement>, \"src\">;\n\n/**\n * Render an image field as an \\`<img>\\` from its normalized \\`.url\\`/\\`.altText\\`.\n * Renders nothing when unset. Pass \\`alt\\` to override the stored alt text.\n */\nexport function Image({ field, alt, ...rest }: ImageProps) {\n if (!field || !field.url) return null;\n return (\n <img\n src={field.url}\n alt={alt ?? field.altText ?? \"\"}\n width={field.width}\n height={field.height}\n {...rest}\n />\n );\n}\n`;\n\n return [header, body].join(\"\\n\");\n}\n"],"mappings":"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","names":["VALID_IDENT","propName","PREAMBLE"]}
package/dist/index.d.ts CHANGED
@@ -54,9 +54,9 @@ declare function generateTypes(models: GeneratableModel[], opts?: GenerateOption
54
54
  * <h3 {...bcms.blog.features.label(i)}>{f.label}</h3> // array item sub-field
55
55
  * </article>
56
56
  *
57
- * The attributes only appear when the site is built with `BCMS_ANNOTATE` set
58
- * (preview builds); a normal production build ships zero extra attributes, because
59
- * `bcmsField` returns `{}`. Same generated file, both builds — no separate mode.
57
+ * The attributes are always emitted inert `data-*` attributes on any build, exactly
58
+ * like Storyblok's `data-blok-c` / Sanity's `data-sanity` so a site is editable no
59
+ * matter which pipeline built it. One generated file, no build modes.
60
60
  *
61
61
  * Pure + deterministic, exactly like the type generator: same models in → identical
62
62
  * string out (slug-sorted, field order preserved, no clock, no I/O). Field keys are
package/dist/index.js CHANGED
@@ -10,12 +10,126 @@ var PREAMBLE = `/**
10
10
  *
11
11
  * The \`{ format, value }\` contract is unchanged; \`html\` is additive.
12
12
  */
13
+ /**
14
+ * One Portable Text block, or a block object (an image, an embed, a placed component).
15
+ *
16
+ * Inlined rather than imported: this preamble is emitted INTO your repo and is deliberately
17
+ * dependency-free. To render structure, \`npm i @portabletext/react\` and pass
18
+ * \`portableText(field)\` to it; to render without adding anything, keep using \`rich()\`.
19
+ */
20
+ export type PortableTextBlock = {
21
+ readonly _type: string;
22
+ readonly _key: string;
23
+ readonly style?: string;
24
+ readonly listItem?: string;
25
+ readonly level?: number;
26
+ readonly markDefs?: readonly { readonly _type: string; readonly _key: string; readonly [k: string]: unknown }[];
27
+ readonly children?: readonly { readonly _type: string; readonly _key: string; readonly text?: string; readonly marks?: readonly string[] }[];
28
+ readonly [k: string]: unknown;
29
+ };
30
+
13
31
  export type RichText = {
32
+ /**
33
+ * The storage format. Deliberately \`string\` and NOT a literal union: a project mid-backfill
34
+ * holds both \`"lexical-\u2026"\` and \`"portable-text-1"\` values, and narrowing this would give a
35
+ * type error to anyone regenerating types against it.
36
+ */
14
37
  readonly format: string;
38
+ /** Portable Text blocks when \`format\` is \`"portable-text-1"\`; editor state otherwise. */
15
39
  readonly value: unknown;
40
+ /**
41
+ * Server-rendered, sanitized HTML. ALWAYS present, in every format, forever \u2014 sites built
42
+ * before Portable Text existed read this directly and cannot be rebuilt.
43
+ */
16
44
  readonly html?: string;
45
+ /**
46
+ * @deprecated Superseded by Portable Text \u2014 read \`portableText(field)\` instead. Retained so
47
+ * entries written before the migration keep type-checking; nothing mints it any more.
48
+ *
49
+ * Structured blocks. Present on Body (\`document\`) fields ONLY, and optional even there \u2014
50
+ * it is derived at write time, so an entry saved before this existed carries none until its
51
+ * next save, and there is no backfill. Branch on its absence; \`html\` is always there.
52
+ *
53
+ * \`id\` is stable within ONE document, never a global key \u2014 two entries both have a "0.0".
54
+ * A cross-document anchor is (entryId, fieldKey, id).
55
+ */
56
+ readonly doc?: {
57
+ readonly version: 1;
58
+ readonly blocks: readonly { readonly id: string; readonly type: string; readonly [k: string]: unknown }[];
59
+ };
17
60
  };
18
61
 
62
+ /**
63
+ * A field that may arrive as EITHER shape.
64
+ *
65
+ * Switching a field between \`text\` and \`richtext\` in the CMS switches what Delivery
66
+ * returns for it \u2014 a bare string becomes \`{ format, value, html }\`. Type author-editable
67
+ * text with this and read it through \`plain()\`/\`rich()\` below, and that switch stops being
68
+ * a site-breaking change. Interpolating the value directly renders \`[object Object]\`.
69
+ */
70
+ export type TextOrRich = string | RichText | null | undefined;
71
+
72
+ /** True when the value is a rich-text envelope rather than a bare string. */
73
+ export function isRichText(value: unknown): value is RichText {
74
+ return (
75
+ typeof value === "object" && value !== null && !Array.isArray(value) &&
76
+ ("html" in value || "format" in value)
77
+ );
78
+ }
79
+
80
+ /** The marker on a Portable Text envelope. */
81
+ export const PORTABLE_TEXT_FORMAT = "portable-text-1";
82
+
83
+ /**
84
+ * Portable Text blocks when the field stores them, otherwise null.
85
+ *
86
+ * ADDITIVE. \`rich()\` and \`plain()\` keep working exactly as before on every format, so
87
+ * nothing you have already shipped needs to change. Use this only if you want to render the
88
+ * structure yourself \u2014 for example with \`@portabletext/react\`.
89
+ */
90
+ export function portableText(value: TextOrRich): readonly PortableTextBlock[] | null {
91
+ if (!isRichText(value)) return null;
92
+ return value.format === PORTABLE_TEXT_FORMAT && Array.isArray(value.value)
93
+ ? (value.value as readonly PortableTextBlock[])
94
+ : null;
95
+ }
96
+
97
+ /** Plain text for attribute contexts \u2014 \`<title>\`, meta description, JSON-LD, \`alt\`. */
98
+ export function plain(value: TextOrRich): string {
99
+ if (typeof value === "string") return value;
100
+ if (!isRichText(value) || typeof value.html !== "string") return "";
101
+ return decodeEntities(value.html.replace(/<[^>]+>/g, "")).trim();
102
+ }
103
+
104
+ /**
105
+ * Renderable HTML, for \`set:html\` / \`dangerouslySetInnerHTML\`. Rich text keeps its inline
106
+ * marks (the server sanitizes \`html\` on write); a bare string is escaped, so a plain field
107
+ * can never inject markup. A LONE wrapping block is unwrapped \u2014 a field switched from
108
+ * \`text\` stores \`<p>\u2026</p>\`, and \`<h1><p>\u2026</p></h1>\` is invalid HTML (the parser closes the
109
+ * heading early, dropping the text out of it). Real block structure is left alone.
110
+ */
111
+ export function rich(value: TextOrRich, fallback = ""): string {
112
+ const html = (
113
+ typeof value === "string" ? escapeHtml(value) : isRichText(value) ? (value.html ?? "") : ""
114
+ ).trim();
115
+ return html ? unwrapLoneBlock(html) : escapeHtml(fallback);
116
+ }
117
+
118
+ function unwrapLoneBlock(html: string): string {
119
+ const m = html.match(/^<(p|div|h[1-6])(?:\\s[^>]*)?>([\\s\\S]*)<\\/\\1>$/i);
120
+ return m && !new RegExp(\`</\${m[1]}>\`, "i").test(m[2]) ? m[2] : html;
121
+ }
122
+
123
+ function escapeHtml(s: string): string {
124
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
125
+ }
126
+
127
+ function decodeEntities(s: string): string {
128
+ return s
129
+ .replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"')
130
+ .replace(/&#0?39;/g, "'").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&");
131
+ }
132
+
19
133
  /**
20
134
  * Image / media field value as stored and returned verbatim by the Delivery API
21
135
  * (server-normalized on write to the canonical shape). \`url\` is always present; an
@@ -31,6 +145,45 @@ export interface BetterCMSImage {
31
145
  readonly height?: number;
32
146
  }
33
147
 
148
+ /**
149
+ * A component slot's value, as stored and delivered.
150
+ *
151
+ * \`componentId\` points at a component definition; \`overrides\` are the author's values,
152
+ * keyed by the component's declared prop keys.
153
+ *
154
+ * \`resolved\` is the SNAPSHOT: at publish time the component is resolved (its block tree
155
+ * with the overrides applied) and frozen onto the published value. That is why editing a
156
+ * component does not silently rewrite entries that were already published \u2014 a published
157
+ * entry carries what it was published with until it is published again.
158
+ *
159
+ * Read \`resolved\` when it is there; it is absent on draft-perspective reads, where you
160
+ * should resolve \`componentId\` yourself against the components endpoint.
161
+ */
162
+ export interface BetterCMSComponentRef {
163
+ readonly componentId: string;
164
+ readonly overrides?: Readonly<Record<string, unknown>>;
165
+ readonly resolved?: readonly unknown[];
166
+ }
167
+
168
+ /**
169
+ * One block in a section zone, as stored and delivered.
170
+ *
171
+ * This is the SAME shape a page's \`blockJson\` holds \u2014 a section zone is a composable page
172
+ * region, so it delivers page blocks, not a shape of its own. Deliberately structural rather
173
+ * than a discriminated union over every block type: the container blocks
174
+ * (\`columns\`, \`section\`, \`slider\`, \`tabs\`) nest \`BetterCMSBlock\` inside \`props\`,
175
+ * and the block vocabulary is server-side and versioned independently of any generated SDK.
176
+ * Narrow on \`type\` at the call site.
177
+ *
178
+ * \`style\` carries the block's design tokens when the author set any.
179
+ */
180
+ export interface BetterCMSBlock {
181
+ readonly type: string;
182
+ readonly id: string;
183
+ readonly props?: Readonly<Record<string, unknown>>;
184
+ readonly style?: Readonly<Record<string, unknown>>;
185
+ }
186
+
34
187
  /**
35
188
  * Delivery envelope around a model's typed \`data\`. \`getEntry\`/\`listEntries\` in the
36
189
  * Next adapter return this shape, with \`fields\` typed by the model.
@@ -41,6 +194,54 @@ export interface BetterCMSEntry<TFields> {
41
194
  readonly fields: TFields;
42
195
  readonly updatedAt: string;
43
196
  }
197
+
198
+ /** Fully resolved published or preview Layout returned beside a delivered page. */
199
+ export interface BetterCMSLayout {
200
+ readonly version: 1;
201
+ readonly nodes: ReadonlyArray<
202
+ | { readonly kind: "page-content"; readonly id: "page-content" }
203
+ | {
204
+ readonly kind: "section";
205
+ readonly id: string;
206
+ readonly slug: string;
207
+ readonly name: string;
208
+ readonly source: "inherit" | "override-content" | "customize-structure" | "page-only" | "detached";
209
+ }
210
+ >;
211
+ readonly sections: Readonly<Record<string, {
212
+ readonly id: string;
213
+ readonly slug: string;
214
+ readonly name: string;
215
+ /** Headless Section values, including values represented by direct field items. */
216
+ readonly fields: Readonly<Record<string, unknown>>;
217
+ readonly items: ReadonlyArray<
218
+ | { readonly id: string; readonly kind: "field"; readonly fieldId: string; readonly value: unknown }
219
+ | {
220
+ readonly id: string;
221
+ readonly kind: "component";
222
+ readonly componentId: string;
223
+ readonly variantGroupId?: string;
224
+ readonly canonicalInputs: readonly BetterCMSLayoutInput[];
225
+ readonly bindings: ReadonlyArray<{ readonly inputId: string; readonly fieldId: string }>;
226
+ readonly canonicalValues: Readonly<Record<string, unknown>>;
227
+ readonly resolvedProps: Readonly<Record<string, unknown>>;
228
+ readonly blocks: readonly unknown[];
229
+ }
230
+ >;
231
+ }>>;
232
+ }
233
+
234
+ /** Recursive canonical input description exposed with each delivered Component item. */
235
+ export interface BetterCMSLayoutInput {
236
+ readonly id: string;
237
+ readonly slug: string;
238
+ readonly label: string;
239
+ readonly type: string;
240
+ readonly required?: boolean;
241
+ readonly defaultValue?: unknown;
242
+ readonly config?: Readonly<Record<string, unknown>>;
243
+ readonly fields?: readonly BetterCMSLayoutInput[];
244
+ }
44
245
  `;
45
246
  function pascalCase(slug) {
46
247
  const parts = slug.split(/[-_\s]+/).filter(Boolean);
@@ -60,6 +261,10 @@ function scalarType(field) {
60
261
  case "text":
61
262
  return "string";
62
263
  case "richtext":
264
+ // A document field stores the SAME {format, value, html} envelope as richtext — the
265
+ // difference is the editor and the placement, not the wire shape. So it maps to the same
266
+ // generated type, and the `doc` rendition lands on `RichText` itself rather than here.
267
+ case "document":
63
268
  return "RichText";
64
269
  case "image":
65
270
  return "BetterCMSImage";
@@ -78,7 +283,12 @@ function scalarType(field) {
78
283
  case "reference":
79
284
  return "string";
80
285
  // referenced entry id
286
+ // Both spellings are live (see ContentModelFieldType). Template- and Webflow-seeded
287
+ // models carry the camelCase one; until it was handled here it fell through to the
288
+ // exhaustiveness default, so generated types for every template-created collection
289
+ // typed this field as `unknown` instead of `string[]`.
81
290
  case "multi-reference":
291
+ case "multiReference":
82
292
  return "string[]";
83
293
  // referenced entry ids
84
294
  case "array": {
@@ -86,6 +296,35 @@ function scalarType(field) {
86
296
  const inner = itemType === "number" ? "number" : "string";
87
297
  return `${inner}[]`;
88
298
  }
299
+ // ── Builder scalars ────────────────────────────────────────────────────────
300
+ // All string-shaped on the wire; each is value-validated on write (see
301
+ // src/lib/content/reference-validation.ts), so the generated type is the
302
+ // narrowest thing that is actually true of the stored value.
303
+ case "longtext":
304
+ case "slug":
305
+ case "email":
306
+ case "phone":
307
+ case "link":
308
+ case "color":
309
+ return "string";
310
+ case "json":
311
+ return "unknown";
312
+ case "component-ref":
313
+ return "BetterCMSComponentRef";
314
+ case "modular":
315
+ return "ReadonlyArray<{ readonly __id: string; readonly __type: string; readonly data: Record<string, unknown> }>";
316
+ case "sections": {
317
+ const config = field.config;
318
+ const unresolvedLegacy = config?.mode === "authored-v2" && Object.prototype.hasOwnProperty.call(config, "allowedSections") && config.legacyResolved !== true;
319
+ if (config?.mode === "authored-v2" && !unresolvedLegacy) {
320
+ return "ReadonlyArray<{ readonly __id: string; readonly __section: string; readonly __type: string; readonly data: Record<string, unknown> }>";
321
+ }
322
+ return "ReadonlyArray<BetterCMSBlock>";
323
+ }
324
+ case "location":
325
+ return "{ readonly lat: number; readonly lng: number; readonly label?: string }";
326
+ case "file":
327
+ return "BetterCMSImage";
89
328
  default: {
90
329
  const _exhaustive = t;
91
330
  return "unknown";
@@ -190,6 +429,8 @@ function bindingKind(t) {
190
429
  case "select":
191
430
  case "array":
192
431
  return t;
432
+ case "document":
433
+ return "richtext";
193
434
  // reference / multi-reference / date / datetime → plain text in the editor v1.
194
435
  default:
195
436
  return "text";
@@ -240,27 +481,12 @@ function fieldsToBindings(fields, indent) {
240
481
  return fields.map((field) => fieldBinding(field, "", indent)).join("\n");
241
482
  }
242
483
  var PREAMBLE2 = `/**
243
- * True when this site is built for Live Preview annotation. Set \`BCMS_ANNOTATE=1\`
244
- * in the preview build only; unset (the default) ships zero binding attributes.
245
- * Read defensively so the module is safe in any runtime (browser, Node, edge).
246
- */
247
- const BCMS_ANNOTATE: boolean = (() => {
248
- try {
249
- const v = (globalThis as { process?: { env?: Record<string, string | undefined> } })
250
- .process?.env?.BCMS_ANNOTATE;
251
- return v != null && v !== "" && v !== "0" && v !== "false";
252
- } catch {
253
- return false;
254
- }
255
- })();
256
-
257
- /**
258
484
  * Binding attributes for a CMS-bound element. Spread onto the element that renders a
259
- * field: \`<h1 {...bcmsField("title", "text")}>\`. Returns \`{}\` unless BCMS_ANNOTATE
260
- * is set, so production markup is untouched.
485
+ * field: \`<h1 {...bcmsField("title", "text")}>\`. Always emitted, on every build \u2014
486
+ * two inert \`data-*\` attributes are what makes the site editable in Live Preview.
261
487
  */
262
488
  export function bcmsField(path: string, kind: string): Record<string, string> {
263
- return BCMS_ANNOTATE ? { "data-bcms-field": path, "data-bcms-kind": kind } : {};
489
+ return { "data-bcms-field": path, "data-bcms-kind": kind };
264
490
  }
265
491
  `;
266
492
  function generateBindings(models, opts = {}) {
@@ -271,7 +497,7 @@ function generateBindings(models, opts = {}) {
271
497
  const header = `// \u26A0\uFE0F AUTO-GENERATED by @bettercms-ai/codegen v${version} \u2014 DO NOT EDIT.
272
498
  // Live Preview field bindings. Regenerate with: npx @bettercms-ai/codegen --bindings-out <path>
273
499
  // Spread these onto the elements that render your content; they emit
274
- // data-bcms-field/data-bcms-kind only when the site is built with BCMS_ANNOTATE set.
500
+ // data-bcms-field/data-bcms-kind on every build.
275
501
  ${opts.bannerComment ? `// ${opts.bannerComment}
276
502
  ` : ""}`;
277
503
  const entries = sorted.map((model) => {
@@ -307,6 +533,12 @@ export type RichTextValue = {
307
533
  readonly format: string;
308
534
  readonly value: unknown;
309
535
  readonly html?: string;
536
+ /**
537
+ * Structured blocks \u2014 Body (\`document\`) fields only, and optional even there: derived at
538
+ * write time, so entries saved before it existed carry none until re-saved. No backfill.
539
+ * Block ids are stable within one document only, never a global key.
540
+ */
541
+ readonly doc?: { readonly version: 1; readonly blocks: readonly { readonly id: string; readonly type: string; readonly [k: string]: unknown }[] };
310
542
  };
311
543
 
312
544
  /** Normalized image/media value from the Delivery API. */
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/generate.ts","../src/bindings.ts","../src/components.ts","../src/fetch-models.ts"],"sourcesContent":["/**\n * @bettercms-ai/codegen — schema → TypeScript generator (the single source of truth).\n *\n * Both the dashboard schema builder and the MCP `create_model`/`add_field` tools write\n * the SAME `content_models.fields` (an array of `ContentModelField`). This generator maps\n * that one array into TypeScript. Because there is exactly one schema representation, the\n * generated types can never drift from the editor or the agent — they are the same source.\n *\n * Pure + deterministic: same models in → identical string out (stable ordering, no clock,\n * no I/O). That makes it trivially testable and safe to commit + diff in a customer repo.\n */\n\nimport type { ContentModelField, ContentModelFieldType } from \"@bettercms-ai/types\";\n\n/** Minimal model shape the generator needs — a subset of the Management API model row. */\nexport interface GeneratableModel {\n /** Machine-safe slug, e.g. \"blog\" or \"case-study\". Used for the schema-map key. */\n slug: string;\n /** Human name, used only for the JSDoc header. */\n name?: string;\n description?: string | null;\n fields: ContentModelField[];\n}\n\nexport interface GenerateOptions {\n /** Generator version stamped into the header (defaults to the package version). */\n version?: string;\n /** Override the banner timestamp source — omitted by default so output is deterministic. */\n bannerComment?: string;\n}\n\n/** Helper types emitted once at the top of every generated file (self-contained, zero-dep). */\nconst PREAMBLE = `/**\n * Rich-text field value returned by the Delivery API.\n *\n * - \\`format\\`/\\`value\\`: the portable, editor-agnostic payload (Lexical EditorState) —\n * render it with your editor's serializer for full fidelity.\n * - \\`html\\`: server-rendered, sanitized HTML (computed render-on-write). Present on\n * Delivery reads; the simplest path for non-React consumers — safe to inject directly\n * (e.g. \\`dangerouslySetInnerHTML\\`). Optional: legacy/un-normalized values may omit it.\n *\n * The \\`{ format, value }\\` contract is unchanged; \\`html\\` is additive.\n */\nexport type RichText = {\n readonly format: string;\n readonly value: unknown;\n readonly html?: string;\n};\n\n/**\n * Image / media field value as stored and returned verbatim by the Delivery API\n * (server-normalized on write to the canonical shape). \\`url\\` is always present; an\n * unresolved/external value may carry only \\`url\\`. \\`altText\\` is the accessibility text\n * for \\`<img alt>\\`.\n */\nexport interface BetterCMSImage {\n readonly id?: string;\n readonly url: string;\n readonly name?: string;\n readonly altText?: string | null;\n readonly width?: number;\n readonly height?: number;\n}\n\n/**\n * Delivery envelope around a model's typed \\`data\\`. \\`getEntry\\`/\\`listEntries\\` in the\n * Next adapter return this shape, with \\`fields\\` typed by the model.\n */\nexport interface BetterCMSEntry<TFields> {\n readonly slug: string;\n readonly status: \"draft\" | \"published\";\n readonly fields: TFields;\n readonly updatedAt: string;\n}\n`;\n\n/** PascalCase an identifier from a slug: \"case-study\" → \"CaseStudy\". */\nfunction pascalCase(slug: string): string {\n const parts = slug.split(/[-_\\s]+/).filter(Boolean);\n const pascal = parts\n .map((p) => p.charAt(0).toUpperCase() + p.slice(1))\n .join(\"\");\n // Guard against an identifier that starts with a digit (invalid TS type name).\n return /^[0-9]/.test(pascal) ? `Model${pascal}` : pascal || \"Model\";\n}\n\n/**\n * Make a string safe to embed inside a `/** ... *​/` JSDoc comment. A field label\n * (free-text, author/agent-controlled) could contain `*​/` — which closes the comment\n * early and injects the remainder as code — or a newline, which breaks the single-line\n * comment. Both are neutralized here. Without this, hostile content produces non-\n * compiling (or worse, code-injected) output.\n */\nfunction escapeJsDoc(text: string): string {\n return text.replace(/\\*\\//g, \"* /\").replace(/[\\r\\n]+/g, \" \").trim();\n}\n\nconst VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * Render a field key as a TS property name. Field keys are author/agent-controlled and\n * not guaranteed to be valid identifiers (e.g. \"my-field\", \"1title\", \"\"), so anything\n * that isn't a bare identifier is emitted as a quoted property name — always valid TS.\n */\nfunction propName(key: string): string {\n return VALID_IDENT.test(key) ? key : JSON.stringify(key);\n}\n\n/** A scalar/primitive field maps to a TS type expression (no nesting). */\nfunction scalarType(field: ContentModelField): string {\n const t: ContentModelFieldType = field.type;\n switch (t) {\n case \"text\":\n return \"string\";\n case \"richtext\":\n return \"RichText\";\n case \"image\":\n return \"BetterCMSImage\";\n case \"boolean\":\n return \"boolean\";\n case \"number\":\n return \"number\";\n case \"date\":\n case \"datetime\":\n return \"string\"; // ISO 8601\n case \"select\": {\n const opts = field.options?.filter((o) => typeof o === \"string\") ?? [];\n return opts.length > 0\n ? opts.map((o) => JSON.stringify(o)).join(\" | \")\n : \"string\";\n }\n case \"reference\":\n return \"string\"; // referenced entry id\n case \"multi-reference\":\n return \"string[]\"; // referenced entry ids\n case \"array\": {\n // Zoned arrays (config.zones) are expanded by fieldsToBody before reaching here;\n // this branch handles only the primitive list form (config.itemType).\n const itemType = field.config?.itemType ?? \"text\";\n const inner =\n itemType === \"number\" ? \"number\" : \"string\"; // text | date → string\n return `${inner}[]`;\n }\n default: {\n // Exhaustiveness guard: if a new field type is added to the union and not\n // mapped here, this line becomes a compile error in the codegen build.\n const _exhaustive: never = t;\n return \"unknown\";\n }\n }\n}\n\n/**\n * Render the TS type for a zoned `array` field: an object with optional\n * `nonRepeatable` (a fixed block) and/or `repeatable` (a list of blocks). Recurses\n * through zone fields, so a zone field that is itself a zoned `array` nests naturally.\n */\nfunction arrayZoneType(field: ContentModelField, indent: string): string {\n const zones = field.config?.zones;\n const parts: string[] = [];\n if (zones?.nonRepeatable?.length) {\n const nested = fieldsToBody(zones.nonRepeatable, indent + \" \");\n parts.push(`${indent} readonly nonRepeatable?: {\\n${nested}\\n${indent} };`);\n }\n if (zones?.repeatable?.fields?.length) {\n const nested = fieldsToBody(zones.repeatable.fields, indent + \" \");\n parts.push(`${indent} readonly repeatable?: Array<{\\n${nested}\\n${indent} }>;`);\n }\n if (parts.length === 0) return \"Record<string, unknown>\"; // zoned array with no fields yet\n return `{\\n${parts.join(\"\\n\")}\\n${indent}}`;\n}\n\n/** Render the body of an object type from a field list, recursing into zones. */\nfunction fieldsToBody(fields: ContentModelField[], indent: string): string {\n const lines: string[] = [];\n for (const field of fields) {\n const optional = field.required ? \"\" : \"?\";\n let typeExpr: string;\n\n if (field.type === \"array\" && field.config?.zones) {\n typeExpr = arrayZoneType(field, indent);\n } else {\n typeExpr = scalarType(field);\n }\n\n const safeLabel = field.label ? escapeJsDoc(field.label) : \"\";\n if (safeLabel && safeLabel !== field.key) {\n lines.push(`${indent}/** ${safeLabel} */`);\n }\n lines.push(`${indent}readonly ${propName(field.key)}${optional}: ${typeExpr};`);\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Generate a complete `.ts` module from a set of content models.\n * Deterministic: models are sorted by slug; field order is preserved as authored.\n */\nexport function generateTypes(\n models: GeneratableModel[],\n opts: GenerateOptions = {},\n): string {\n const version = opts.version ?? \"0.1.0\";\n // Code-unit sort (NOT localeCompare): locale/ICU-independent so the generated\n // file is byte-identical on every machine — committed output diffs cleanly.\n const sorted = [...models].sort((a, b) =>\n a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0,\n );\n\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Regenerate with: npx @bettercms-ai/codegen\n// Source of truth: your BetterCMS content models (the same schema the dashboard\n// builder and the MCP tools write). Re-run codegen after any schema change.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const interfaces: string[] = [];\n const mapEntries: string[] = [];\n // Different slugs can PascalCase to the same base name (e.g. \"case-study\" and\n // \"case_study\" → \"CaseStudy\"). Emitting two identical interfaces would silently\n // declaration-merge into one wrong type, so disambiguate with a numeric suffix.\n const usedNames = new Set<string>();\n\n for (const model of sorted) {\n const base = `${pascalCase(model.slug)}Fields`;\n let typeName = base;\n for (let n = 2; usedNames.has(typeName); n++) typeName = `${base}_${n}`;\n usedNames.add(typeName);\n\n const name = model.name ? escapeJsDoc(model.name) : \"\";\n const desc = model.description ? escapeJsDoc(model.description) : \"\";\n const doc = name\n ? `/**\\n * ${name}${desc ? ` — ${desc}` : \"\"}\\n * Model slug: \\`${model.slug}\\`\\n */\\n`\n : \"\";\n const body = model.fields.length\n ? fieldsToBody(model.fields, \" \")\n : \" // (no fields defined yet)\";\n interfaces.push(`${doc}export interface ${typeName} {\\n${body}\\n}`);\n mapEntries.push(` readonly ${JSON.stringify(model.slug)}: ${typeName};`);\n }\n\n const schemaMap = `/**\n * Registry mapping each model slug to its typed fields. The Next adapter uses this to\n * type \\`getEntry(\"blog\", ...)\\` by slug — autocomplete and exhaustiveness for free.\n */\nexport interface BetterCMSSchema {\n${mapEntries.join(\"\\n\") || \" // (no models defined yet)\"}\n}\n\n/** Union of all model slugs. */\nexport type BetterCMSModelSlug = keyof BetterCMSSchema;`;\n\n return [header, PREAMBLE, interfaces.join(\"\\n\\n\"), schemaMap, \"\"].join(\"\\n\");\n}\n","/**\n * @bettercms-ai/codegen — schema → Live Preview binding helper generator.\n *\n * Companion to {@link generateTypes}. Where that emits the *types*, this emits a\n * tiny, schema-derived runtime that stamps `data-bcms-field` / `data-bcms-kind`\n * attributes onto the elements a site author binds to CMS content. Those\n * attributes are what the dashboard's Live Preview editor reads to turn the real,\n * running site into an editable canvas (the parent maps `data-bcms-field` → its\n * internal `data-node-id` on frame load).\n *\n * Why a helper and not auto-injection: BetterCMS never renders the customer's DOM\n * — the site does. So binding is opt-in per element via a spread:\n *\n * import { bcms } from \"./bettercms.bindings.generated\";\n *\n * <h1 {...bcms.blog.title}>{entry.fields.title}</h1> // scalar\n * <li {...bcms.blog.tags.value(i)}>{tag}</li> // primitive-array item\n * <article {...bcms.blog.features.$(i)}> // array item root\n * <h3 {...bcms.blog.features.label(i)}>{f.label}</h3> // array item sub-field\n * </article>\n *\n * The attributes only appear when the site is built with `BCMS_ANNOTATE` set\n * (preview builds); a normal production build ships zero extra attributes, because\n * `bcmsField` returns `{}`. Same generated file, both builds — no separate mode.\n *\n * Pure + deterministic, exactly like the type generator: same models in → identical\n * string out (slug-sorted, field order preserved, no clock, no I/O). Field keys are\n * author/agent-controlled, so every embedded key is emitted as an escaped string\n * literal (never interpolated into code) — hostile input can't break the output.\n *\n * Grammar — mirrors what the editor's `fieldPathToNodeId` resolves:\n * `title` · `hero.heroTitle` · `hero.primaryCta.label` (group leaves, any depth)\n * `features[0]` · `features[0].label` · `intro.facts[0].label` (repeaters, one index)\n * Group (non-repeatable) zones recurse into nested binding objects; a repeater is an\n * object with `$(i)` (item root) + one accessor per scalar sub-field. Arrays nested\n * inside a repeater item (a second index) are still beyond what the editor can\n * address, so they are intentionally omitted rather than emitted as dead paths.\n */\n\nimport type { ContentModelField, ContentModelFieldType } from \"@bettercms-ai/types\";\nimport type { GeneratableModel, GenerateOptions } from \"./generate.js\";\n\nconst VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * Render a field key as an object property name. Keys aren't guaranteed to be valid\n * identifiers (e.g. \"my-field\", \"1title\"), so anything that isn't a bare identifier\n * is quoted — always valid TS. (Mirrors the same helper in `generate.ts`.)\n */\nfunction propName(key: string): string {\n return VALID_IDENT.test(key) ? key : JSON.stringify(key);\n}\n\n/**\n * The kind label written to `data-bcms-kind`, mapped to the editor's closed field-type\n * set (matches the dashboard adapter's `toEditorFieldType`): API-only types that have\n * no on-canvas control collapse to \"text\". Informational today — the editor derives the\n * authoritative kind from the loaded model — but kept truthful for debugging/forward use.\n */\nfunction bindingKind(t: ContentModelFieldType): string {\n switch (t) {\n case \"text\":\n case \"richtext\":\n case \"image\":\n case \"boolean\":\n case \"number\":\n case \"select\":\n case \"array\":\n return t;\n // reference / multi-reference / date / datetime → plain text in the editor v1.\n default:\n return \"text\";\n }\n}\n\n/**\n * Build a runtime path expression for an array element: a string literal split around\n * the index so it concatenates at call time. Both halves are JSON-escaped, so an\n * author-controlled key can never inject code. e.g. (\"features[\", \"].label\") →\n * `\"features[\" + i + \"].label\"`.\n */\nfunction indexedPath(prefix: string, suffix: string): string {\n return `${JSON.stringify(prefix)} + i + ${JSON.stringify(suffix)}`;\n}\n\n/** Render a repeater binding object: `$(i)` item root + one accessor per scalar\n * sub-field. `path` is the repeater's full (possibly dotted) field path. */\nfunction repeaterBinding(\n itemFields: ContentModelField[],\n path: string,\n indent: string,\n): string {\n const lines: string[] = [\n `${indent} $: (i: number) => bcmsField(${indexedPath(`${path}[`, \"]\")}, \"array\"),`,\n ];\n for (const sub of itemFields) {\n // A sub-field that is itself an array would need a second index the editor\n // can't address yet — skip it rather than emit a path that won't bind.\n if (sub.type === \"array\") continue;\n lines.push(\n `${indent} ${propName(sub.key)}: (i: number) => bcmsField(${indexedPath(`${path}[`, `].${sub.key}`)}, ${JSON.stringify(bindingKind(sub.type))}),`,\n );\n }\n return `{\\n${lines.join(\"\\n\")}\\n${indent}}`;\n}\n\n/** Render the binding for one field at `path`, recursing into group zones. */\nfunction fieldBinding(\n field: ContentModelField,\n prefix: string,\n indent: string,\n): string {\n const path = prefix ? `${prefix}.${field.key}` : field.key;\n const name = propName(field.key);\n\n if (field.type !== \"array\") {\n return `${indent}${name}: bcmsField(${JSON.stringify(path)}, ${JSON.stringify(bindingKind(field.type))}),`;\n }\n\n const zones = field.config?.zones;\n // Group (non-repeatable) → a nested object of dotted-path leaf bindings.\n if (zones?.nonRepeatable?.length) {\n const body = zones.nonRepeatable\n .map((child) => fieldBinding(child, path, `${indent} `))\n .join(\"\\n\");\n return `${indent}${name}: {\\n${body}\\n${indent}},`;\n }\n // Repeater → `$(i)` + scalar sub-field accessors.\n if (zones?.repeatable?.fields?.length) {\n return `${indent}${name}: ${repeaterBinding(zones.repeatable.fields, path, indent)},`;\n }\n // Primitive list (`config.itemType` or bare) → `$(i)` + synthetic `value(i)`.\n const lines = [\n `${indent} $: (i: number) => bcmsField(${indexedPath(`${path}[`, \"]\")}, \"array\"),`,\n `${indent} value: (i: number) => bcmsField(${indexedPath(`${path}[`, \"].value\")}, \"text\"),`,\n ];\n return `${indent}${name}: {\\n${lines.join(\"\\n\")}\\n${indent}},`;\n}\n\n/** Render the binding entries for one model's fields (field order preserved). */\nfunction fieldsToBindings(fields: ContentModelField[], indent: string): string {\n return fields.map((field) => fieldBinding(field, \"\", indent)).join(\"\\n\");\n}\n\n/** The self-contained runtime emitted once at the top of every bindings file. */\nconst PREAMBLE = `/**\n * True when this site is built for Live Preview annotation. Set \\`BCMS_ANNOTATE=1\\`\n * in the preview build only; unset (the default) ships zero binding attributes.\n * Read defensively so the module is safe in any runtime (browser, Node, edge).\n */\nconst BCMS_ANNOTATE: boolean = (() => {\n try {\n const v = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env?.BCMS_ANNOTATE;\n return v != null && v !== \"\" && v !== \"0\" && v !== \"false\";\n } catch {\n return false;\n }\n})();\n\n/**\n * Binding attributes for a CMS-bound element. Spread onto the element that renders a\n * field: \\`<h1 {...bcmsField(\"title\", \"text\")}>\\`. Returns \\`{}\\` unless BCMS_ANNOTATE\n * is set, so production markup is untouched.\n */\nexport function bcmsField(path: string, kind: string): Record<string, string> {\n return BCMS_ANNOTATE ? { \"data-bcms-field\": path, \"data-bcms-kind\": kind } : {};\n}\n`;\n\n/**\n * Generate the Live Preview bindings module from a set of content models.\n * Deterministic: models are sorted by slug; field order is preserved as authored.\n */\nexport function generateBindings(\n models: GeneratableModel[],\n opts: GenerateOptions = {},\n): string {\n const version = opts.version ?? \"0.1.0\";\n // Code-unit sort (NOT localeCompare) so output is byte-identical on every machine.\n const sorted = [...models].sort((a, b) =>\n a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0,\n );\n\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Live Preview field bindings. Regenerate with: npx @bettercms-ai/codegen --bindings-out <path>\n// Spread these onto the elements that render your content; they emit\n// data-bcms-field/data-bcms-kind only when the site is built with BCMS_ANNOTATE set.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const entries = sorted.map((model) => {\n const body = model.fields.length\n ? `\\n${fieldsToBindings(model.fields, \" \")}\\n `\n : \"\";\n return ` ${JSON.stringify(model.slug)}: {${body}},`;\n });\n\n const bcms = `/**\n * Field bindings keyed by model slug. Spread a binding onto the element that renders\n * that field. Arrays expose \\`$(i)\\` for the item element and one accessor per\n * (one-level) sub-field; primitive arrays expose \\`value(i)\\` for the item's scalar.\n */\nexport const bcms = {\n${entries.join(\"\\n\") || \" // (no models defined yet)\"}\n} as const;`;\n\n return [header, PREAMBLE, bcms, \"\"].join(\"\\n\");\n}\n","/**\n * @bettercms-ai/codegen — schema → typed React render components generator.\n *\n * Companion to {@link generateTypes} (types) and {@link generateBindings} (Live\n * Preview attributes). This emits a small, self-contained `.tsx` module with two\n * components that render the canonical Delivery field shapes CORRECTLY, so authors\n * never hand-roll the rendering that produces the classic bugs:\n *\n * - <RichText> renders the server-sanitized `html` via `dangerouslySetInnerHTML`,\n * instead of interpolating the value as a JSX child (which React escapes, so the\n * page shows literal `<p>…</p>` tags — the #6 escaped-richtext bug).\n * - <Image> reads the normalized image object's `.url`/`.altText`, instead of\n * treating the object as a string.\n *\n * The emitted module is intentionally generic (not per-model) and dependency-free\n * beyond React, so it is a drop-in: point codegen at a path and import the two\n * components. It is deterministic (no clock, no I/O) like the sibling generators.\n *\n * Security: `html` is the Delivery API's server-rendered, DOMPurify-sanitized output\n * (see the RichText type docs). `<RichText>` injects exactly that field. If a caller\n * passes HTML from another, untrusted source they must sanitize it themselves.\n */\n\nimport type { GenerateOptions } from \"./generate.js\";\n\n/**\n * Generate the `bettercms.components.tsx` module: typed `<RichText>` and `<Image>`\n * components for the canonical Delivery field shapes. Deterministic — same options\n * in, identical string out.\n */\nexport function generateComponents(opts: GenerateOptions = {}): string {\n const version = opts.version ?? \"0.1.0\";\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Regenerate with: npx @bettercms-ai/codegen --components-out <path>\n// Typed render components for BetterCMS field shapes. Use these instead of\n// hand-rendering richtext/image values — they render the canonical shapes correctly.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const body = `import * as React from \"react\";\n\n/** Rich-text value from the Delivery API. \\`html\\` is server-rendered + sanitized. */\nexport type RichTextValue = {\n readonly format: string;\n readonly value: unknown;\n readonly html?: string;\n};\n\n/** Normalized image/media value from the Delivery API. */\nexport interface BetterCMSImageValue {\n readonly url: string;\n readonly altText?: string | null;\n readonly width?: number;\n readonly height?: number;\n}\n\ntype RichTextProps = {\n /** The richtext field value (\\`entry.fields.someRichText\\`). */\n field?: RichTextValue | null;\n /** Element/component to render as. Default: \\`\"div\"\\`. */\n as?: React.ElementType;\n} & Omit<React.HTMLAttributes<HTMLElement>, \"dangerouslySetInnerHTML\" | \"children\">;\n\n/**\n * Render a richtext field as HTML. Uses the server-sanitized \\`html\\` via\n * \\`dangerouslySetInnerHTML\\` — NEVER interpolate a richtext value as a JSX child\n * (React escapes it, so the page shows literal tags). Renders nothing when unset.\n */\nexport function RichText({ field, as: Tag = \"div\", ...rest }: RichTextProps) {\n if (!field || !field.html) return null;\n return <Tag {...rest} dangerouslySetInnerHTML={{ __html: field.html }} />;\n}\n\ntype ImageProps = {\n /** The image field value (\\`entry.fields.someImage\\`). */\n field?: BetterCMSImageValue | null;\n /** Alt text override; defaults to the field's \\`altText\\`, then \\`\"\"\\`. */\n alt?: string;\n} & Omit<React.ImgHTMLAttributes<HTMLImageElement>, \"src\">;\n\n/**\n * Render an image field as an \\`<img>\\` from its normalized \\`.url\\`/\\`.altText\\`.\n * Renders nothing when unset. Pass \\`alt\\` to override the stored alt text.\n */\nexport function Image({ field, alt, ...rest }: ImageProps) {\n if (!field || !field.url) return null;\n return (\n <img\n src={field.url}\n alt={alt ?? field.altText ?? \"\"}\n width={field.width}\n height={field.height}\n {...rest}\n />\n );\n}\n`;\n\n return [header, body].join(\"\\n\");\n}\n","/**\n * Fetches content models from the BetterCMS Management API so the CLI can generate\n * types against a live project. Kept dependency-free (plain fetch) so the generated\n * artifact and this fetcher can run anywhere — a GitHub Action, a postinstall, a script.\n */\n\nimport type { GeneratableModel } from \"./generate.js\";\n\nexport interface FetchModelsOptions {\n /** Management API base, e.g. \"https://api.bettercms.ai/api/v1\". */\n apiUrl: string;\n /** A management-scoped key (content:manage) or device-minted token. */\n apiKey: string;\n /** Optional fetch override (testing / custom runtime). */\n fetchImpl?: typeof fetch;\n}\n\ninterface ManagedModelRow {\n slug: string;\n name?: string;\n description?: string | null;\n fields: GeneratableModel[\"fields\"];\n}\n\n/**\n * GET /management/content/models — returns the project's models (the key is\n * project-scoped server-side, so this is exactly the schema for this site).\n */\nexport async function fetchModels(\n opts: FetchModelsOptions,\n): Promise<GeneratableModel[]> {\n const doFetch = opts.fetchImpl ?? globalThis.fetch;\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const url = `${base}/management/content/models`;\n\n let res: Response;\n try {\n res = await doFetch(url, {\n // No Content-Type: this is a bodyless GET; the header is incorrect here and\n // strict edge runtimes/proxies may reject it.\n headers: { Authorization: `Bearer ${opts.apiKey}`, Accept: \"application/json\" },\n });\n } catch (err) {\n throw new Error(\n `Could not reach the BetterCMS Management API at ${url}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n\n if (!res.ok) {\n const hint =\n res.status === 401 || res.status === 403\n ? \" — check your management API key (it must have the content:manage scope).\"\n : \"\";\n throw new Error(`Management API returned ${res.status} ${res.statusText}${hint}`);\n }\n\n const body = (await res.json()) as { data?: ManagedModelRow[] };\n const rows = body.data ?? [];\n return rows.map((r) => ({\n slug: r.slug,\n name: r.name,\n description: r.description ?? null,\n fields: r.fields ?? [],\n }));\n}\n"],"mappings":"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","names":["VALID_IDENT","propName","PREAMBLE"]}
1
+ {"version":3,"sources":["../src/generate.ts","../src/bindings.ts","../src/components.ts","../src/fetch-models.ts"],"sourcesContent":["/**\n * @bettercms-ai/codegen — schema → TypeScript generator (the single source of truth).\n *\n * Both the dashboard schema builder and the MCP `create_model`/`add_field` tools write\n * the SAME `content_models.fields` (an array of `ContentModelField`). This generator maps\n * that one array into TypeScript. Because there is exactly one schema representation, the\n * generated types can never drift from the editor or the agent — they are the same source.\n *\n * Pure + deterministic: same models in → identical string out (stable ordering, no clock,\n * no I/O). That makes it trivially testable and safe to commit + diff in a customer repo.\n */\n\nimport type { ContentModelField, ContentModelFieldType } from \"@bettercms-ai/types\";\n\n/** Minimal model shape the generator needs — a subset of the Management API model row. */\nexport interface GeneratableModel {\n /** Machine-safe slug, e.g. \"blog\" or \"case-study\". Used for the schema-map key. */\n slug: string;\n /** Human name, used only for the JSDoc header. */\n name?: string;\n description?: string | null;\n fields: ContentModelField[];\n}\n\nexport interface GenerateOptions {\n /** Generator version stamped into the header (defaults to the package version). */\n version?: string;\n /** Override the banner timestamp source — omitted by default so output is deterministic. */\n bannerComment?: string;\n}\n\n/** Helper types emitted once at the top of every generated file (self-contained, zero-dep). */\nconst PREAMBLE = `/**\n * Rich-text field value returned by the Delivery API.\n *\n * - \\`format\\`/\\`value\\`: the portable, editor-agnostic payload (Lexical EditorState) —\n * render it with your editor's serializer for full fidelity.\n * - \\`html\\`: server-rendered, sanitized HTML (computed render-on-write). Present on\n * Delivery reads; the simplest path for non-React consumers — safe to inject directly\n * (e.g. \\`dangerouslySetInnerHTML\\`). Optional: legacy/un-normalized values may omit it.\n *\n * The \\`{ format, value }\\` contract is unchanged; \\`html\\` is additive.\n */\n/**\n * One Portable Text block, or a block object (an image, an embed, a placed component).\n *\n * Inlined rather than imported: this preamble is emitted INTO your repo and is deliberately\n * dependency-free. To render structure, \\`npm i @portabletext/react\\` and pass\n * \\`portableText(field)\\` to it; to render without adding anything, keep using \\`rich()\\`.\n */\nexport type PortableTextBlock = {\n readonly _type: string;\n readonly _key: string;\n readonly style?: string;\n readonly listItem?: string;\n readonly level?: number;\n readonly markDefs?: readonly { readonly _type: string; readonly _key: string; readonly [k: string]: unknown }[];\n readonly children?: readonly { readonly _type: string; readonly _key: string; readonly text?: string; readonly marks?: readonly string[] }[];\n readonly [k: string]: unknown;\n};\n\nexport type RichText = {\n /**\n * The storage format. Deliberately \\`string\\` and NOT a literal union: a project mid-backfill\n * holds both \\`\"lexical-…\"\\` and \\`\"portable-text-1\"\\` values, and narrowing this would give a\n * type error to anyone regenerating types against it.\n */\n readonly format: string;\n /** Portable Text blocks when \\`format\\` is \\`\"portable-text-1\"\\`; editor state otherwise. */\n readonly value: unknown;\n /**\n * Server-rendered, sanitized HTML. ALWAYS present, in every format, forever — sites built\n * before Portable Text existed read this directly and cannot be rebuilt.\n */\n readonly html?: string;\n /**\n * @deprecated Superseded by Portable Text — read \\`portableText(field)\\` instead. Retained so\n * entries written before the migration keep type-checking; nothing mints it any more.\n *\n * Structured blocks. Present on Body (\\`document\\`) fields ONLY, and optional even there —\n * it is derived at write time, so an entry saved before this existed carries none until its\n * next save, and there is no backfill. Branch on its absence; \\`html\\` is always there.\n *\n * \\`id\\` is stable within ONE document, never a global key — two entries both have a \"0.0\".\n * A cross-document anchor is (entryId, fieldKey, id).\n */\n readonly doc?: {\n readonly version: 1;\n readonly blocks: readonly { readonly id: string; readonly type: string; readonly [k: string]: unknown }[];\n };\n};\n\n/**\n * A field that may arrive as EITHER shape.\n *\n * Switching a field between \\`text\\` and \\`richtext\\` in the CMS switches what Delivery\n * returns for it — a bare string becomes \\`{ format, value, html }\\`. Type author-editable\n * text with this and read it through \\`plain()\\`/\\`rich()\\` below, and that switch stops being\n * a site-breaking change. Interpolating the value directly renders \\`[object Object]\\`.\n */\nexport type TextOrRich = string | RichText | null | undefined;\n\n/** True when the value is a rich-text envelope rather than a bare string. */\nexport function isRichText(value: unknown): value is RichText {\n return (\n typeof value === \"object\" && value !== null && !Array.isArray(value) &&\n (\"html\" in value || \"format\" in value)\n );\n}\n\n/** The marker on a Portable Text envelope. */\nexport const PORTABLE_TEXT_FORMAT = \"portable-text-1\";\n\n/**\n * Portable Text blocks when the field stores them, otherwise null.\n *\n * ADDITIVE. \\`rich()\\` and \\`plain()\\` keep working exactly as before on every format, so\n * nothing you have already shipped needs to change. Use this only if you want to render the\n * structure yourself — for example with \\`@portabletext/react\\`.\n */\nexport function portableText(value: TextOrRich): readonly PortableTextBlock[] | null {\n if (!isRichText(value)) return null;\n return value.format === PORTABLE_TEXT_FORMAT && Array.isArray(value.value)\n ? (value.value as readonly PortableTextBlock[])\n : null;\n}\n\n/** Plain text for attribute contexts — \\`<title>\\`, meta description, JSON-LD, \\`alt\\`. */\nexport function plain(value: TextOrRich): string {\n if (typeof value === \"string\") return value;\n if (!isRichText(value) || typeof value.html !== \"string\") return \"\";\n return decodeEntities(value.html.replace(/<[^>]+>/g, \"\")).trim();\n}\n\n/**\n * Renderable HTML, for \\`set:html\\` / \\`dangerouslySetInnerHTML\\`. Rich text keeps its inline\n * marks (the server sanitizes \\`html\\` on write); a bare string is escaped, so a plain field\n * can never inject markup. A LONE wrapping block is unwrapped — a field switched from\n * \\`text\\` stores \\`<p>…</p>\\`, and \\`<h1><p>…</p></h1>\\` is invalid HTML (the parser closes the\n * heading early, dropping the text out of it). Real block structure is left alone.\n */\nexport function rich(value: TextOrRich, fallback = \"\"): string {\n const html = (\n typeof value === \"string\" ? escapeHtml(value) : isRichText(value) ? (value.html ?? \"\") : \"\"\n ).trim();\n return html ? unwrapLoneBlock(html) : escapeHtml(fallback);\n}\n\nfunction unwrapLoneBlock(html: string): string {\n const m = html.match(/^<(p|div|h[1-6])(?:\\\\s[^>]*)?>([\\\\s\\\\S]*)<\\\\/\\\\1>$/i);\n return m && !new RegExp(\\`</\\${m[1]}>\\`, \"i\").test(m[2]) ? m[2] : html;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nfunction decodeEntities(s: string): string {\n return s\n .replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\").replace(/&quot;/g, '\"')\n .replace(/&#0?39;/g, \"'\").replace(/&nbsp;/g, \" \").replace(/&amp;/g, \"&\");\n}\n\n/**\n * Image / media field value as stored and returned verbatim by the Delivery API\n * (server-normalized on write to the canonical shape). \\`url\\` is always present; an\n * unresolved/external value may carry only \\`url\\`. \\`altText\\` is the accessibility text\n * for \\`<img alt>\\`.\n */\nexport interface BetterCMSImage {\n readonly id?: string;\n readonly url: string;\n readonly name?: string;\n readonly altText?: string | null;\n readonly width?: number;\n readonly height?: number;\n}\n\n/**\n * A component slot's value, as stored and delivered.\n *\n * \\`componentId\\` points at a component definition; \\`overrides\\` are the author's values,\n * keyed by the component's declared prop keys.\n *\n * \\`resolved\\` is the SNAPSHOT: at publish time the component is resolved (its block tree\n * with the overrides applied) and frozen onto the published value. That is why editing a\n * component does not silently rewrite entries that were already published — a published\n * entry carries what it was published with until it is published again.\n *\n * Read \\`resolved\\` when it is there; it is absent on draft-perspective reads, where you\n * should resolve \\`componentId\\` yourself against the components endpoint.\n */\nexport interface BetterCMSComponentRef {\n readonly componentId: string;\n readonly overrides?: Readonly<Record<string, unknown>>;\n readonly resolved?: readonly unknown[];\n}\n\n/**\n * One block in a section zone, as stored and delivered.\n *\n * This is the SAME shape a page's \\`blockJson\\` holds — a section zone is a composable page\n * region, so it delivers page blocks, not a shape of its own. Deliberately structural rather\n * than a discriminated union over every block type: the container blocks\n * (\\`columns\\`, \\`section\\`, \\`slider\\`, \\`tabs\\`) nest \\`BetterCMSBlock\\` inside \\`props\\`,\n * and the block vocabulary is server-side and versioned independently of any generated SDK.\n * Narrow on \\`type\\` at the call site.\n *\n * \\`style\\` carries the block's design tokens when the author set any.\n */\nexport interface BetterCMSBlock {\n readonly type: string;\n readonly id: string;\n readonly props?: Readonly<Record<string, unknown>>;\n readonly style?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Delivery envelope around a model's typed \\`data\\`. \\`getEntry\\`/\\`listEntries\\` in the\n * Next adapter return this shape, with \\`fields\\` typed by the model.\n */\nexport interface BetterCMSEntry<TFields> {\n readonly slug: string;\n readonly status: \"draft\" | \"published\";\n readonly fields: TFields;\n readonly updatedAt: string;\n}\n\n/** Fully resolved published or preview Layout returned beside a delivered page. */\nexport interface BetterCMSLayout {\n readonly version: 1;\n readonly nodes: ReadonlyArray<\n | { readonly kind: \"page-content\"; readonly id: \"page-content\" }\n | {\n readonly kind: \"section\";\n readonly id: string;\n readonly slug: string;\n readonly name: string;\n readonly source: \"inherit\" | \"override-content\" | \"customize-structure\" | \"page-only\" | \"detached\";\n }\n >;\n readonly sections: Readonly<Record<string, {\n readonly id: string;\n readonly slug: string;\n readonly name: string;\n /** Headless Section values, including values represented by direct field items. */\n readonly fields: Readonly<Record<string, unknown>>;\n readonly items: ReadonlyArray<\n | { readonly id: string; readonly kind: \"field\"; readonly fieldId: string; readonly value: unknown }\n | {\n readonly id: string;\n readonly kind: \"component\";\n readonly componentId: string;\n readonly variantGroupId?: string;\n readonly canonicalInputs: readonly BetterCMSLayoutInput[];\n readonly bindings: ReadonlyArray<{ readonly inputId: string; readonly fieldId: string }>;\n readonly canonicalValues: Readonly<Record<string, unknown>>;\n readonly resolvedProps: Readonly<Record<string, unknown>>;\n readonly blocks: readonly unknown[];\n }\n >;\n }>>;\n}\n\n/** Recursive canonical input description exposed with each delivered Component item. */\nexport interface BetterCMSLayoutInput {\n readonly id: string;\n readonly slug: string;\n readonly label: string;\n readonly type: string;\n readonly required?: boolean;\n readonly defaultValue?: unknown;\n readonly config?: Readonly<Record<string, unknown>>;\n readonly fields?: readonly BetterCMSLayoutInput[];\n}\n`;\n\n/** PascalCase an identifier from a slug: \"case-study\" → \"CaseStudy\". */\nfunction pascalCase(slug: string): string {\n const parts = slug.split(/[-_\\s]+/).filter(Boolean);\n const pascal = parts\n .map((p) => p.charAt(0).toUpperCase() + p.slice(1))\n .join(\"\");\n // Guard against an identifier that starts with a digit (invalid TS type name).\n return /^[0-9]/.test(pascal) ? `Model${pascal}` : pascal || \"Model\";\n}\n\n/**\n * Make a string safe to embed inside a `/** ... *​/` JSDoc comment. A field label\n * (free-text, author/agent-controlled) could contain `*​/` — which closes the comment\n * early and injects the remainder as code — or a newline, which breaks the single-line\n * comment. Both are neutralized here. Without this, hostile content produces non-\n * compiling (or worse, code-injected) output.\n */\nfunction escapeJsDoc(text: string): string {\n return text.replace(/\\*\\//g, \"* /\").replace(/[\\r\\n]+/g, \" \").trim();\n}\n\nconst VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * Render a field key as a TS property name. Field keys are author/agent-controlled and\n * not guaranteed to be valid identifiers (e.g. \"my-field\", \"1title\", \"\"), so anything\n * that isn't a bare identifier is emitted as a quoted property name — always valid TS.\n */\nfunction propName(key: string): string {\n return VALID_IDENT.test(key) ? key : JSON.stringify(key);\n}\n\n/** A scalar/primitive field maps to a TS type expression (no nesting). */\nfunction scalarType(field: ContentModelField): string {\n const t: ContentModelFieldType = field.type;\n switch (t) {\n case \"text\":\n return \"string\";\n case \"richtext\":\n // A document field stores the SAME {format, value, html} envelope as richtext — the\n // difference is the editor and the placement, not the wire shape. So it maps to the same\n // generated type, and the `doc` rendition lands on `RichText` itself rather than here.\n case \"document\":\n return \"RichText\";\n case \"image\":\n return \"BetterCMSImage\";\n case \"boolean\":\n return \"boolean\";\n case \"number\":\n return \"number\";\n case \"date\":\n case \"datetime\":\n return \"string\"; // ISO 8601\n case \"select\": {\n const opts = field.options?.filter((o) => typeof o === \"string\") ?? [];\n return opts.length > 0\n ? opts.map((o) => JSON.stringify(o)).join(\" | \")\n : \"string\";\n }\n case \"reference\":\n return \"string\"; // referenced entry id\n // Both spellings are live (see ContentModelFieldType). Template- and Webflow-seeded\n // models carry the camelCase one; until it was handled here it fell through to the\n // exhaustiveness default, so generated types for every template-created collection\n // typed this field as `unknown` instead of `string[]`.\n case \"multi-reference\":\n case \"multiReference\":\n return \"string[]\"; // referenced entry ids\n case \"array\": {\n // Zoned arrays (config.zones) are expanded by fieldsToBody before reaching here;\n // this branch handles only the primitive list form (config.itemType).\n const itemType = field.config?.itemType ?? \"text\";\n const inner =\n itemType === \"number\" ? \"number\" : \"string\"; // text | date → string\n return `${inner}[]`;\n }\n // ── Builder scalars ────────────────────────────────────────────────────────\n // All string-shaped on the wire; each is value-validated on write (see\n // src/lib/content/reference-validation.ts), so the generated type is the\n // narrowest thing that is actually true of the stored value.\n case \"longtext\":\n case \"slug\":\n case \"email\":\n case \"phone\":\n case \"link\":\n case \"color\":\n return \"string\";\n case \"json\":\n // Arbitrary author-supplied JSON — object or parseable string. `unknown`\n // forces the consumer to narrow, which is correct: we genuinely don't know.\n return \"unknown\";\n case \"component-ref\":\n // The delivered value is the reference plus, on published reads, its frozen tree.\n return \"BetterCMSComponentRef\";\n case \"modular\":\n // ponytail: `data` stays an open record. A per-block discriminated union would need\n // every allowed block model resolved at generate time (they are separate rows, and\n // this function only sees the field), and consumers narrow on __type anyway. Emitting\n // the union later is additive — it only makes an existing `unknown` more specific.\n return \"ReadonlyArray<{ readonly __id: string; readonly __type: string; readonly data: Record<string, unknown> }>\";\n case \"sections\": {\n const config = field.config as {\n mode?: unknown;\n allowedSections?: unknown;\n legacyResolved?: unknown;\n } | null | undefined;\n // Creating the durable authored Zone intentionally preserves `allowedSections` until\n // migration is explicitly confirmed. During that window the writer, validator and\n // renderer still use the legacy BetterCMSBlock[] value, so codegen must tell the same\n // truth instead of exposing the authored instance shape one release too early.\n const unresolvedLegacy = config?.mode === \"authored-v2\"\n && Object.prototype.hasOwnProperty.call(config, \"allowedSections\")\n && config.legacyResolved !== true;\n if (config?.mode === \"authored-v2\" && !unresolvedLegacy) {\n return \"ReadonlyArray<{ readonly __id: string; readonly __section: string; readonly __type: string; readonly data: Record<string, unknown> }>\";\n }\n // The value IS the page block tree, so it gets the public block type rather than a\n // shape of its own — a section zone and a page's blockJson deliver the identical\n // thing, and emitting two names for one shape is how a consumer ends up writing a\n // converter between them.\n return \"ReadonlyArray<BetterCMSBlock>\";\n }\n case \"location\":\n return \"{ readonly lat: number; readonly lng: number; readonly label?: string }\";\n case \"file\":\n // The file envelope is the image envelope minus the pixel dimensions\n // ({ url, name?, ... }), and BetterCMSImage's width/height are optional — so\n // every file value is already a valid BetterCMSImage. Reused rather than\n // emitting a second near-identical public type into every generated SDK.\n return \"BetterCMSImage\";\n default: {\n // Exhaustiveness guard: if a new field type is added to the union and not\n // mapped here, this line becomes a compile error in the codegen build.\n const _exhaustive: never = t;\n return \"unknown\";\n }\n }\n}\n\n/**\n * Render the TS type for a zoned `array` field: an object with optional\n * `nonRepeatable` (a fixed block) and/or `repeatable` (a list of blocks). Recurses\n * through zone fields, so a zone field that is itself a zoned `array` nests naturally.\n */\nfunction arrayZoneType(field: ContentModelField, indent: string): string {\n const zones = field.config?.zones;\n const parts: string[] = [];\n if (zones?.nonRepeatable?.length) {\n const nested = fieldsToBody(zones.nonRepeatable, indent + \" \");\n parts.push(`${indent} readonly nonRepeatable?: {\\n${nested}\\n${indent} };`);\n }\n if (zones?.repeatable?.fields?.length) {\n const nested = fieldsToBody(zones.repeatable.fields, indent + \" \");\n parts.push(`${indent} readonly repeatable?: Array<{\\n${nested}\\n${indent} }>;`);\n }\n if (parts.length === 0) return \"Record<string, unknown>\"; // zoned array with no fields yet\n return `{\\n${parts.join(\"\\n\")}\\n${indent}}`;\n}\n\n/** Render the body of an object type from a field list, recursing into zones. */\nfunction fieldsToBody(fields: ContentModelField[], indent: string): string {\n const lines: string[] = [];\n for (const field of fields) {\n const optional = field.required ? \"\" : \"?\";\n let typeExpr: string;\n\n if (field.type === \"array\" && field.config?.zones) {\n typeExpr = arrayZoneType(field, indent);\n } else {\n typeExpr = scalarType(field);\n }\n\n const safeLabel = field.label ? escapeJsDoc(field.label) : \"\";\n if (safeLabel && safeLabel !== field.key) {\n lines.push(`${indent}/** ${safeLabel} */`);\n }\n lines.push(`${indent}readonly ${propName(field.key)}${optional}: ${typeExpr};`);\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Generate a complete `.ts` module from a set of content models.\n * Deterministic: models are sorted by slug; field order is preserved as authored.\n */\nexport function generateTypes(\n models: GeneratableModel[],\n opts: GenerateOptions = {},\n): string {\n const version = opts.version ?? \"0.1.0\";\n // Code-unit sort (NOT localeCompare): locale/ICU-independent so the generated\n // file is byte-identical on every machine — committed output diffs cleanly.\n const sorted = [...models].sort((a, b) =>\n a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0,\n );\n\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Regenerate with: npx @bettercms-ai/codegen\n// Source of truth: your BetterCMS content models (the same schema the dashboard\n// builder and the MCP tools write). Re-run codegen after any schema change.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const interfaces: string[] = [];\n const mapEntries: string[] = [];\n // Different slugs can PascalCase to the same base name (e.g. \"case-study\" and\n // \"case_study\" → \"CaseStudy\"). Emitting two identical interfaces would silently\n // declaration-merge into one wrong type, so disambiguate with a numeric suffix.\n const usedNames = new Set<string>();\n\n for (const model of sorted) {\n const base = `${pascalCase(model.slug)}Fields`;\n let typeName = base;\n for (let n = 2; usedNames.has(typeName); n++) typeName = `${base}_${n}`;\n usedNames.add(typeName);\n\n const name = model.name ? escapeJsDoc(model.name) : \"\";\n const desc = model.description ? escapeJsDoc(model.description) : \"\";\n const doc = name\n ? `/**\\n * ${name}${desc ? ` — ${desc}` : \"\"}\\n * Model slug: \\`${model.slug}\\`\\n */\\n`\n : \"\";\n const body = model.fields.length\n ? fieldsToBody(model.fields, \" \")\n : \" // (no fields defined yet)\";\n interfaces.push(`${doc}export interface ${typeName} {\\n${body}\\n}`);\n mapEntries.push(` readonly ${JSON.stringify(model.slug)}: ${typeName};`);\n }\n\n const schemaMap = `/**\n * Registry mapping each model slug to its typed fields. The Next adapter uses this to\n * type \\`getEntry(\"blog\", ...)\\` by slug — autocomplete and exhaustiveness for free.\n */\nexport interface BetterCMSSchema {\n${mapEntries.join(\"\\n\") || \" // (no models defined yet)\"}\n}\n\n/** Union of all model slugs. */\nexport type BetterCMSModelSlug = keyof BetterCMSSchema;`;\n\n return [header, PREAMBLE, interfaces.join(\"\\n\\n\"), schemaMap, \"\"].join(\"\\n\");\n}\n","/**\n * @bettercms-ai/codegen — schema → Live Preview binding helper generator.\n *\n * Companion to {@link generateTypes}. Where that emits the *types*, this emits a\n * tiny, schema-derived runtime that stamps `data-bcms-field` / `data-bcms-kind`\n * attributes onto the elements a site author binds to CMS content. Those\n * attributes are what the dashboard's Live Preview editor reads to turn the real,\n * running site into an editable canvas (the parent maps `data-bcms-field` → its\n * internal `data-node-id` on frame load).\n *\n * Why a helper and not auto-injection: BetterCMS never renders the customer's DOM\n * — the site does. So binding is opt-in per element via a spread:\n *\n * import { bcms } from \"./bettercms.bindings.generated\";\n *\n * <h1 {...bcms.blog.title}>{entry.fields.title}</h1> // scalar\n * <li {...bcms.blog.tags.value(i)}>{tag}</li> // primitive-array item\n * <article {...bcms.blog.features.$(i)}> // array item root\n * <h3 {...bcms.blog.features.label(i)}>{f.label}</h3> // array item sub-field\n * </article>\n *\n * The attributes are always emitted — inert `data-*` attributes on any build, exactly\n * like Storyblok's `data-blok-c` / Sanity's `data-sanity` — so a site is editable no\n * matter which pipeline built it. One generated file, no build modes.\n *\n * Pure + deterministic, exactly like the type generator: same models in → identical\n * string out (slug-sorted, field order preserved, no clock, no I/O). Field keys are\n * author/agent-controlled, so every embedded key is emitted as an escaped string\n * literal (never interpolated into code) — hostile input can't break the output.\n *\n * Grammar — mirrors what the editor's `fieldPathToNodeId` resolves:\n * `title` · `hero.heroTitle` · `hero.primaryCta.label` (group leaves, any depth)\n * `features[0]` · `features[0].label` · `intro.facts[0].label` (repeaters, one index)\n * Group (non-repeatable) zones recurse into nested binding objects; a repeater is an\n * object with `$(i)` (item root) + one accessor per scalar sub-field. Arrays nested\n * inside a repeater item (a second index) are still beyond what the editor can\n * address, so they are intentionally omitted rather than emitted as dead paths.\n */\n\nimport type { ContentModelField, ContentModelFieldType } from \"@bettercms-ai/types\";\nimport type { GeneratableModel, GenerateOptions } from \"./generate.js\";\n\nconst VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * Render a field key as an object property name. Keys aren't guaranteed to be valid\n * identifiers (e.g. \"my-field\", \"1title\"), so anything that isn't a bare identifier\n * is quoted — always valid TS. (Mirrors the same helper in `generate.ts`.)\n */\nfunction propName(key: string): string {\n return VALID_IDENT.test(key) ? key : JSON.stringify(key);\n}\n\n/**\n * The kind label written to `data-bcms-kind`, mapped to the editor's closed field-type\n * set (matches the dashboard adapter's `toEditorFieldType`): API-only types that have\n * no on-canvas control collapse to \"text\". Informational today — the editor derives the\n * authoritative kind from the loaded model — but kept truthful for debugging/forward use.\n */\nfunction bindingKind(t: ContentModelFieldType): string {\n switch (t) {\n case \"text\":\n case \"richtext\":\n case \"image\":\n case \"boolean\":\n case \"number\":\n case \"select\":\n case \"array\":\n return t;\n case \"document\":\n return \"richtext\";\n // reference / multi-reference / date / datetime → plain text in the editor v1.\n default:\n return \"text\";\n }\n}\n\n/**\n * Build a runtime path expression for an array element: a string literal split around\n * the index so it concatenates at call time. Both halves are JSON-escaped, so an\n * author-controlled key can never inject code. e.g. (\"features[\", \"].label\") →\n * `\"features[\" + i + \"].label\"`.\n */\nfunction indexedPath(prefix: string, suffix: string): string {\n return `${JSON.stringify(prefix)} + i + ${JSON.stringify(suffix)}`;\n}\n\n/** Render a repeater binding object: `$(i)` item root + one accessor per scalar\n * sub-field. `path` is the repeater's full (possibly dotted) field path. */\nfunction repeaterBinding(\n itemFields: ContentModelField[],\n path: string,\n indent: string,\n): string {\n const lines: string[] = [\n `${indent} $: (i: number) => bcmsField(${indexedPath(`${path}[`, \"]\")}, \"array\"),`,\n ];\n for (const sub of itemFields) {\n // A sub-field that is itself an array would need a second index the editor\n // can't address yet — skip it rather than emit a path that won't bind.\n if (sub.type === \"array\") continue;\n lines.push(\n `${indent} ${propName(sub.key)}: (i: number) => bcmsField(${indexedPath(`${path}[`, `].${sub.key}`)}, ${JSON.stringify(bindingKind(sub.type))}),`,\n );\n }\n return `{\\n${lines.join(\"\\n\")}\\n${indent}}`;\n}\n\n/** Render the binding for one field at `path`, recursing into group zones. */\nfunction fieldBinding(\n field: ContentModelField,\n prefix: string,\n indent: string,\n): string {\n const path = prefix ? `${prefix}.${field.key}` : field.key;\n const name = propName(field.key);\n\n if (field.type !== \"array\") {\n return `${indent}${name}: bcmsField(${JSON.stringify(path)}, ${JSON.stringify(bindingKind(field.type))}),`;\n }\n\n const zones = field.config?.zones;\n // Group (non-repeatable) → a nested object of dotted-path leaf bindings.\n if (zones?.nonRepeatable?.length) {\n const body = zones.nonRepeatable\n .map((child) => fieldBinding(child, path, `${indent} `))\n .join(\"\\n\");\n return `${indent}${name}: {\\n${body}\\n${indent}},`;\n }\n // Repeater → `$(i)` + scalar sub-field accessors.\n if (zones?.repeatable?.fields?.length) {\n return `${indent}${name}: ${repeaterBinding(zones.repeatable.fields, path, indent)},`;\n }\n // Primitive list (`config.itemType` or bare) → `$(i)` + synthetic `value(i)`.\n const lines = [\n `${indent} $: (i: number) => bcmsField(${indexedPath(`${path}[`, \"]\")}, \"array\"),`,\n `${indent} value: (i: number) => bcmsField(${indexedPath(`${path}[`, \"].value\")}, \"text\"),`,\n ];\n return `${indent}${name}: {\\n${lines.join(\"\\n\")}\\n${indent}},`;\n}\n\n/** Render the binding entries for one model's fields (field order preserved). */\nfunction fieldsToBindings(fields: ContentModelField[], indent: string): string {\n return fields.map((field) => fieldBinding(field, \"\", indent)).join(\"\\n\");\n}\n\n/** The self-contained runtime emitted once at the top of every bindings file. */\nconst PREAMBLE = `/**\n * Binding attributes for a CMS-bound element. Spread onto the element that renders a\n * field: \\`<h1 {...bcmsField(\"title\", \"text\")}>\\`. Always emitted, on every build —\n * two inert \\`data-*\\` attributes are what makes the site editable in Live Preview.\n */\nexport function bcmsField(path: string, kind: string): Record<string, string> {\n return { \"data-bcms-field\": path, \"data-bcms-kind\": kind };\n}\n`;\n\n/**\n * Generate the Live Preview bindings module from a set of content models.\n * Deterministic: models are sorted by slug; field order is preserved as authored.\n */\nexport function generateBindings(\n models: GeneratableModel[],\n opts: GenerateOptions = {},\n): string {\n const version = opts.version ?? \"0.1.0\";\n // Code-unit sort (NOT localeCompare) so output is byte-identical on every machine.\n const sorted = [...models].sort((a, b) =>\n a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0,\n );\n\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Live Preview field bindings. Regenerate with: npx @bettercms-ai/codegen --bindings-out <path>\n// Spread these onto the elements that render your content; they emit\n// data-bcms-field/data-bcms-kind on every build.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const entries = sorted.map((model) => {\n const body = model.fields.length\n ? `\\n${fieldsToBindings(model.fields, \" \")}\\n `\n : \"\";\n return ` ${JSON.stringify(model.slug)}: {${body}},`;\n });\n\n const bcms = `/**\n * Field bindings keyed by model slug. Spread a binding onto the element that renders\n * that field. Arrays expose \\`$(i)\\` for the item element and one accessor per\n * (one-level) sub-field; primitive arrays expose \\`value(i)\\` for the item's scalar.\n */\nexport const bcms = {\n${entries.join(\"\\n\") || \" // (no models defined yet)\"}\n} as const;`;\n\n return [header, PREAMBLE, bcms, \"\"].join(\"\\n\");\n}\n","/**\n * @bettercms-ai/codegen — schema → typed React render components generator.\n *\n * Companion to {@link generateTypes} (types) and {@link generateBindings} (Live\n * Preview attributes). This emits a small, self-contained `.tsx` module with two\n * components that render the canonical Delivery field shapes CORRECTLY, so authors\n * never hand-roll the rendering that produces the classic bugs:\n *\n * - <RichText> renders the server-sanitized `html` via `dangerouslySetInnerHTML`,\n * instead of interpolating the value as a JSX child (which React escapes, so the\n * page shows literal `<p>…</p>` tags — the #6 escaped-richtext bug).\n * - <Image> reads the normalized image object's `.url`/`.altText`, instead of\n * treating the object as a string.\n *\n * The emitted module is intentionally generic (not per-model) and dependency-free\n * beyond React, so it is a drop-in: point codegen at a path and import the two\n * components. It is deterministic (no clock, no I/O) like the sibling generators.\n *\n * Security: `html` is the Delivery API's server-rendered, DOMPurify-sanitized output\n * (see the RichText type docs). `<RichText>` injects exactly that field. If a caller\n * passes HTML from another, untrusted source they must sanitize it themselves.\n */\n\nimport type { GenerateOptions } from \"./generate.js\";\n\n/**\n * Generate the `bettercms.components.tsx` module: typed `<RichText>` and `<Image>`\n * components for the canonical Delivery field shapes. Deterministic — same options\n * in, identical string out.\n */\nexport function generateComponents(opts: GenerateOptions = {}): string {\n const version = opts.version ?? \"0.1.0\";\n const header = `// ⚠️ AUTO-GENERATED by @bettercms-ai/codegen v${version} — DO NOT EDIT.\n// Regenerate with: npx @bettercms-ai/codegen --components-out <path>\n// Typed render components for BetterCMS field shapes. Use these instead of\n// hand-rendering richtext/image values — they render the canonical shapes correctly.\n${opts.bannerComment ? `// ${opts.bannerComment}\\n` : \"\"}`;\n\n const body = `import * as React from \"react\";\n\n/** Rich-text value from the Delivery API. \\`html\\` is server-rendered + sanitized. */\nexport type RichTextValue = {\n readonly format: string;\n readonly value: unknown;\n readonly html?: string;\n /**\n * Structured blocks — Body (\\`document\\`) fields only, and optional even there: derived at\n * write time, so entries saved before it existed carry none until re-saved. No backfill.\n * Block ids are stable within one document only, never a global key.\n */\n readonly doc?: { readonly version: 1; readonly blocks: readonly { readonly id: string; readonly type: string; readonly [k: string]: unknown }[] };\n};\n\n/** Normalized image/media value from the Delivery API. */\nexport interface BetterCMSImageValue {\n readonly url: string;\n readonly altText?: string | null;\n readonly width?: number;\n readonly height?: number;\n}\n\ntype RichTextProps = {\n /** The richtext field value (\\`entry.fields.someRichText\\`). */\n field?: RichTextValue | null;\n /** Element/component to render as. Default: \\`\"div\"\\`. */\n as?: React.ElementType;\n} & Omit<React.HTMLAttributes<HTMLElement>, \"dangerouslySetInnerHTML\" | \"children\">;\n\n/**\n * Render a richtext field as HTML. Uses the server-sanitized \\`html\\` via\n * \\`dangerouslySetInnerHTML\\` — NEVER interpolate a richtext value as a JSX child\n * (React escapes it, so the page shows literal tags). Renders nothing when unset.\n */\nexport function RichText({ field, as: Tag = \"div\", ...rest }: RichTextProps) {\n if (!field || !field.html) return null;\n return <Tag {...rest} dangerouslySetInnerHTML={{ __html: field.html }} />;\n}\n\ntype ImageProps = {\n /** The image field value (\\`entry.fields.someImage\\`). */\n field?: BetterCMSImageValue | null;\n /** Alt text override; defaults to the field's \\`altText\\`, then \\`\"\"\\`. */\n alt?: string;\n} & Omit<React.ImgHTMLAttributes<HTMLImageElement>, \"src\">;\n\n/**\n * Render an image field as an \\`<img>\\` from its normalized \\`.url\\`/\\`.altText\\`.\n * Renders nothing when unset. Pass \\`alt\\` to override the stored alt text.\n */\nexport function Image({ field, alt, ...rest }: ImageProps) {\n if (!field || !field.url) return null;\n return (\n <img\n src={field.url}\n alt={alt ?? field.altText ?? \"\"}\n width={field.width}\n height={field.height}\n {...rest}\n />\n );\n}\n`;\n\n return [header, body].join(\"\\n\");\n}\n","/**\n * Fetches content models from the BetterCMS Management API so the CLI can generate\n * types against a live project. Kept dependency-free (plain fetch) so the generated\n * artifact and this fetcher can run anywhere — a GitHub Action, a postinstall, a script.\n */\n\nimport type { GeneratableModel } from \"./generate.js\";\n\nexport interface FetchModelsOptions {\n /** Management API base, e.g. \"https://api.bettercms.ai/api/v1\". */\n apiUrl: string;\n /** A management-scoped key (content:manage) or device-minted token. */\n apiKey: string;\n /** Optional fetch override (testing / custom runtime). */\n fetchImpl?: typeof fetch;\n}\n\ninterface ManagedModelRow {\n slug: string;\n name?: string;\n description?: string | null;\n fields: GeneratableModel[\"fields\"];\n}\n\n/**\n * GET /management/content/models — returns the project's models (the key is\n * project-scoped server-side, so this is exactly the schema for this site).\n */\nexport async function fetchModels(\n opts: FetchModelsOptions,\n): Promise<GeneratableModel[]> {\n const doFetch = opts.fetchImpl ?? globalThis.fetch;\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const url = `${base}/management/content/models`;\n\n let res: Response;\n try {\n res = await doFetch(url, {\n // No Content-Type: this is a bodyless GET; the header is incorrect here and\n // strict edge runtimes/proxies may reject it.\n headers: { Authorization: `Bearer ${opts.apiKey}`, Accept: \"application/json\" },\n });\n } catch (err) {\n throw new Error(\n `Could not reach the BetterCMS Management API at ${url}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n\n if (!res.ok) {\n const hint =\n res.status === 401 || res.status === 403\n ? \" — check your management API key (it must have the content:manage scope).\"\n : \"\";\n throw new Error(`Management API returned ${res.status} ${res.statusText}${hint}`);\n }\n\n const body = (await res.json()) as { data?: ManagedModelRow[] };\n const rows = body.data ?? [];\n return rows.map((r) => ({\n slug: r.slug,\n name: r.name,\n description: r.description ?? null,\n fields: r.fields ?? [],\n }));\n}\n"],"mappings":";AAgCA,IAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsPjB,SAAS,WAAW,MAAsB;AACxC,QAAM,QAAQ,KAAK,MAAM,SAAS,EAAE,OAAO,OAAO;AAClD,QAAM,SAAS,MACZ,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,CAAC,EACjD,KAAK,EAAE;AAEV,SAAO,SAAS,KAAK,MAAM,IAAI,QAAQ,MAAM,KAAK,UAAU;AAC9D;AASA,SAAS,YAAY,MAAsB;AACzC,SAAO,KAAK,QAAQ,SAAS,KAAK,EAAE,QAAQ,YAAY,GAAG,EAAE,KAAK;AACpE;AAEA,IAAM,cAAc;AAOpB,SAAS,SAAS,KAAqB;AACrC,SAAO,YAAY,KAAK,GAAG,IAAI,MAAM,KAAK,UAAU,GAAG;AACzD;AAGA,SAAS,WAAW,OAAkC;AACpD,QAAM,IAA2B,MAAM;AACvC,UAAQ,GAAG;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA;AAAA;AAAA;AAAA,IAIL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA;AAAA,IACT,KAAK,UAAU;AACb,YAAM,OAAO,MAAM,SAAS,OAAO,CAAC,MAAM,OAAO,MAAM,QAAQ,KAAK,CAAC;AACrE,aAAO,KAAK,SAAS,IACjB,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,KAAK,IAC7C;AAAA,IACN;AAAA,IACA,KAAK;AACH,aAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKT,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA;AAAA,IACT,KAAK,SAAS;AAGZ,YAAM,WAAW,MAAM,QAAQ,YAAY;AAC3C,YAAM,QACJ,aAAa,WAAW,WAAW;AACrC,aAAO,GAAG,KAAK;AAAA,IACjB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAGH,aAAO;AAAA,IACT,KAAK;AAEH,aAAO;AAAA,IACT,KAAK;AAKH,aAAO;AAAA,IACT,KAAK,YAAY;AACf,YAAM,SAAS,MAAM;AASrB,YAAM,mBAAmB,QAAQ,SAAS,iBACrC,OAAO,UAAU,eAAe,KAAK,QAAQ,iBAAiB,KAC9D,OAAO,mBAAmB;AAC/B,UAAI,QAAQ,SAAS,iBAAiB,CAAC,kBAAkB;AACvD,eAAO;AAAA,MACT;AAKA,aAAO;AAAA,IACT;AAAA,IACA,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAKH,aAAO;AAAA,IACT,SAAS;AAGP,YAAM,cAAqB;AAC3B,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAOA,SAAS,cAAc,OAA0B,QAAwB;AACvE,QAAM,QAAQ,MAAM,QAAQ;AAC5B,QAAM,QAAkB,CAAC;AACzB,MAAI,OAAO,eAAe,QAAQ;AAChC,UAAM,SAAS,aAAa,MAAM,eAAe,SAAS,IAAI;AAC9D,UAAM,KAAK,GAAG,MAAM;AAAA,EAAiC,MAAM;AAAA,EAAK,MAAM,MAAM;AAAA,EAC9E;AACA,MAAI,OAAO,YAAY,QAAQ,QAAQ;AACrC,UAAM,SAAS,aAAa,MAAM,WAAW,QAAQ,SAAS,MAAM;AACpE,UAAM,KAAK,GAAG,MAAM;AAAA,EAAoC,MAAM;AAAA,EAAK,MAAM,OAAO;AAAA,EAClF;AACA,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO;AAAA,EAAM,MAAM,KAAK,IAAI,CAAC;AAAA,EAAK,MAAM;AAC1C;AAGA,SAAS,aAAa,QAA6B,QAAwB;AACzE,QAAM,QAAkB,CAAC;AACzB,aAAW,SAAS,QAAQ;AAC1B,UAAM,WAAW,MAAM,WAAW,KAAK;AACvC,QAAI;AAEJ,QAAI,MAAM,SAAS,WAAW,MAAM,QAAQ,OAAO;AACjD,iBAAW,cAAc,OAAO,MAAM;AAAA,IACxC,OAAO;AACL,iBAAW,WAAW,KAAK;AAAA,IAC7B;AAEA,UAAM,YAAY,MAAM,QAAQ,YAAY,MAAM,KAAK,IAAI;AAC3D,QAAI,aAAa,cAAc,MAAM,KAAK;AACxC,YAAM,KAAK,GAAG,MAAM,OAAO,SAAS,KAAK;AAAA,IAC3C;AACA,UAAM,KAAK,GAAG,MAAM,YAAY,SAAS,MAAM,GAAG,CAAC,GAAG,QAAQ,KAAK,QAAQ,GAAG;AAAA,EAChF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAMO,SAAS,cACd,QACA,OAAwB,CAAC,GACjB;AACR,QAAM,UAAU,KAAK,WAAW;AAGhC,QAAM,SAAS,CAAC,GAAG,MAAM,EAAE;AAAA,IAAK,CAAC,GAAG,MAClC,EAAE,OAAO,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI;AAAA,EAC/C;AAEA,QAAM,SAAS,6DAAmD,OAAO;AAAA;AAAA;AAAA;AAAA,EAIzE,KAAK,gBAAgB,MAAM,KAAK,aAAa;AAAA,IAAO,EAAE;AAEtD,QAAM,aAAuB,CAAC;AAC9B,QAAM,aAAuB,CAAC;AAI9B,QAAM,YAAY,oBAAI,IAAY;AAElC,aAAW,SAAS,QAAQ;AAC1B,UAAM,OAAO,GAAG,WAAW,MAAM,IAAI,CAAC;AACtC,QAAI,WAAW;AACf,aAAS,IAAI,GAAG,UAAU,IAAI,QAAQ,GAAG,IAAK,YAAW,GAAG,IAAI,IAAI,CAAC;AACrE,cAAU,IAAI,QAAQ;AAEtB,UAAM,OAAO,MAAM,OAAO,YAAY,MAAM,IAAI,IAAI;AACpD,UAAM,OAAO,MAAM,cAAc,YAAY,MAAM,WAAW,IAAI;AAClE,UAAM,MAAM,OACR;AAAA,KAAW,IAAI,GAAG,OAAO,WAAM,IAAI,KAAK,EAAE;AAAA,mBAAsB,MAAM,IAAI;AAAA;AAAA,IAC1E;AACJ,UAAM,OAAO,MAAM,OAAO,SACtB,aAAa,MAAM,QAAQ,IAAI,IAC/B;AACJ,eAAW,KAAK,GAAG,GAAG,oBAAoB,QAAQ;AAAA,EAAO,IAAI;AAAA,EAAK;AAClE,eAAW,KAAK,cAAc,KAAK,UAAU,MAAM,IAAI,CAAC,KAAK,QAAQ,GAAG;AAAA,EAC1E;AAEA,QAAM,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,EAKlB,WAAW,KAAK,IAAI,KAAK,8BAA8B;AAAA;AAAA;AAAA;AAAA;AAMvD,SAAO,CAAC,QAAQ,UAAU,WAAW,KAAK,MAAM,GAAG,WAAW,EAAE,EAAE,KAAK,IAAI;AAC7E;;;AC1dA,IAAMA,eAAc;AAOpB,SAASC,UAAS,KAAqB;AACrC,SAAOD,aAAY,KAAK,GAAG,IAAI,MAAM,KAAK,UAAU,GAAG;AACzD;AAQA,SAAS,YAAY,GAAkC;AACrD,UAAQ,GAAG;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA;AAAA,IAET;AACE,aAAO;AAAA,EACX;AACF;AAQA,SAAS,YAAY,QAAgB,QAAwB;AAC3D,SAAO,GAAG,KAAK,UAAU,MAAM,CAAC,UAAU,KAAK,UAAU,MAAM,CAAC;AAClE;AAIA,SAAS,gBACP,YACA,MACA,QACQ;AACR,QAAM,QAAkB;AAAA,IACtB,GAAG,MAAM,iCAAiC,YAAY,GAAG,IAAI,KAAK,GAAG,CAAC;AAAA,EACxE;AACA,aAAW,OAAO,YAAY;AAG5B,QAAI,IAAI,SAAS,QAAS;AAC1B,UAAM;AAAA,MACJ,GAAG,MAAM,KAAKC,UAAS,IAAI,GAAG,CAAC,8BAA8B,YAAY,GAAG,IAAI,KAAK,KAAK,IAAI,GAAG,EAAE,CAAC,KAAK,KAAK,UAAU,YAAY,IAAI,IAAI,CAAC,CAAC;AAAA,IAChJ;AAAA,EACF;AACA,SAAO;AAAA,EAAM,MAAM,KAAK,IAAI,CAAC;AAAA,EAAK,MAAM;AAC1C;AAGA,SAAS,aACP,OACA,QACA,QACQ;AACR,QAAM,OAAO,SAAS,GAAG,MAAM,IAAI,MAAM,GAAG,KAAK,MAAM;AACvD,QAAM,OAAOA,UAAS,MAAM,GAAG;AAE/B,MAAI,MAAM,SAAS,SAAS;AAC1B,WAAO,GAAG,MAAM,GAAG,IAAI,eAAe,KAAK,UAAU,IAAI,CAAC,KAAK,KAAK,UAAU,YAAY,MAAM,IAAI,CAAC,CAAC;AAAA,EACxG;AAEA,QAAM,QAAQ,MAAM,QAAQ;AAE5B,MAAI,OAAO,eAAe,QAAQ;AAChC,UAAM,OAAO,MAAM,cAChB,IAAI,CAAC,UAAU,aAAa,OAAO,MAAM,GAAG,MAAM,IAAI,CAAC,EACvD,KAAK,IAAI;AACZ,WAAO,GAAG,MAAM,GAAG,IAAI;AAAA,EAAQ,IAAI;AAAA,EAAK,MAAM;AAAA,EAChD;AAEA,MAAI,OAAO,YAAY,QAAQ,QAAQ;AACrC,WAAO,GAAG,MAAM,GAAG,IAAI,KAAK,gBAAgB,MAAM,WAAW,QAAQ,MAAM,MAAM,CAAC;AAAA,EACpF;AAEA,QAAM,QAAQ;AAAA,IACZ,GAAG,MAAM,iCAAiC,YAAY,GAAG,IAAI,KAAK,GAAG,CAAC;AAAA,IACtE,GAAG,MAAM,qCAAqC,YAAY,GAAG,IAAI,KAAK,SAAS,CAAC;AAAA,EAClF;AACA,SAAO,GAAG,MAAM,GAAG,IAAI;AAAA,EAAQ,MAAM,KAAK,IAAI,CAAC;AAAA,EAAK,MAAM;AAC5D;AAGA,SAAS,iBAAiB,QAA6B,QAAwB;AAC7E,SAAO,OAAO,IAAI,CAAC,UAAU,aAAa,OAAO,IAAI,MAAM,CAAC,EAAE,KAAK,IAAI;AACzE;AAGA,IAAMC,YAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcV,SAAS,iBACd,QACA,OAAwB,CAAC,GACjB;AACR,QAAM,UAAU,KAAK,WAAW;AAEhC,QAAM,SAAS,CAAC,GAAG,MAAM,EAAE;AAAA,IAAK,CAAC,GAAG,MAClC,EAAE,OAAO,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI;AAAA,EAC/C;AAEA,QAAM,SAAS,6DAAmD,OAAO;AAAA;AAAA;AAAA;AAAA,EAIzE,KAAK,gBAAgB,MAAM,KAAK,aAAa;AAAA,IAAO,EAAE;AAEtD,QAAM,UAAU,OAAO,IAAI,CAAC,UAAU;AACpC,UAAM,OAAO,MAAM,OAAO,SACtB;AAAA,EAAK,iBAAiB,MAAM,QAAQ,MAAM,CAAC;AAAA,MAC3C;AACJ,WAAO,KAAK,KAAK,UAAU,MAAM,IAAI,CAAC,MAAM,IAAI;AAAA,EAClD,CAAC;AAED,QAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMb,QAAQ,KAAK,IAAI,KAAK,8BAA8B;AAAA;AAGpD,SAAO,CAAC,QAAQA,WAAU,MAAM,EAAE,EAAE,KAAK,IAAI;AAC/C;;;ACpKO,SAAS,mBAAmB,OAAwB,CAAC,GAAW;AACrE,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,SAAS,6DAAmD,OAAO;AAAA;AAAA;AAAA;AAAA,EAIzE,KAAK,gBAAgB,MAAM,KAAK,aAAa;AAAA,IAAO,EAAE;AAEtD,QAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiEb,SAAO,CAAC,QAAQ,IAAI,EAAE,KAAK,IAAI;AACjC;;;AC5EA,eAAsB,YACpB,MAC6B;AAC7B,QAAM,UAAU,KAAK,aAAa,WAAW;AAC7C,QAAM,OAAO,KAAK,OAAO,QAAQ,QAAQ,EAAE;AAC3C,QAAM,MAAM,GAAG,IAAI;AAEnB,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,QAAQ,KAAK;AAAA;AAAA;AAAA,MAGvB,SAAS,EAAE,eAAe,UAAU,KAAK,MAAM,IAAI,QAAQ,mBAAmB;AAAA,IAChF,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,mDAAmD,GAAG,KACpD,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CACjD;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,OACJ,IAAI,WAAW,OAAO,IAAI,WAAW,MACjC,mFACA;AACN,UAAM,IAAI,MAAM,2BAA2B,IAAI,MAAM,IAAI,IAAI,UAAU,GAAG,IAAI,EAAE;AAAA,EAClF;AAEA,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAM,OAAO,KAAK,QAAQ,CAAC;AAC3B,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,MAAM,EAAE;AAAA,IACR,MAAM,EAAE;AAAA,IACR,aAAa,EAAE,eAAe;AAAA,IAC9B,QAAQ,EAAE,UAAU,CAAC;AAAA,EACvB,EAAE;AACJ;","names":["VALID_IDENT","propName","PREAMBLE"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettercms-ai/codegen",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Generate TypeScript types from your BetterCMS content schema — the single source of truth shared by the dashboard builder and the MCP tools.",
6
6
  "bin": {