@bettercms-ai/codegen 0.5.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.
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@bettercms-ai/codegen",
3
+ "version": "0.5.0",
4
+ "type": "module",
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
+ "bin": {
7
+ "bettercms-codegen": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "sideEffects": false,
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "scripts": {
25
+ "typecheck": "tsc --noEmit",
26
+ "build": "tsup",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest"
29
+ },
30
+ "dependencies": {
31
+ "@bettercms-ai/types": "^1.0.17"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20",
35
+ "esbuild": "^0.27.7",
36
+ "tsup": "^8.5.1",
37
+ "vitest": "^4.1.4"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
42
+ }
43
+ }