@betttercms/codegen 0.1.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 +77 -0
- package/dist/cli.js +264 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +185 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @betttercms/codegen
|
|
2
|
+
|
|
3
|
+
Generate TypeScript types from your BetterCMS content schema.
|
|
4
|
+
|
|
5
|
+
Your models are defined once — in the dashboard schema builder **or** by your AI through
|
|
6
|
+
the MCP tools. Both write the same underlying field schema, so the types this generates can
|
|
7
|
+
never drift from what your editors and agents actually create. This is the **deterministic
|
|
8
|
+
floor** under the AI integration: even if an agent mis-wires something, you still have exact,
|
|
9
|
+
committed types to fall back on.
|
|
10
|
+
|
|
11
|
+
## Use it
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# In your site repo (key from the dashboard → Settings → API Keys, content:manage scope)
|
|
15
|
+
BETTERCMS_API_KEY=bcms_xxx npx @betttercms/codegen --out src/bettercms.generated.ts
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
✓ Generated 2 model types → src/bettercms.generated.ts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Commit the generated file and re-run codegen after any schema change (a CI step or the
|
|
23
|
+
BetterCMS GitHub Action does this for you on every build).
|
|
24
|
+
|
|
25
|
+
### Options
|
|
26
|
+
|
|
27
|
+
| Flag | Env | Default |
|
|
28
|
+
|------|-----|---------|
|
|
29
|
+
| `--out, -o` | — | `bettercms.generated.ts` |
|
|
30
|
+
| `--api-url` | `BETTERCMS_API_URL` | `https://api.bettercms.ai/api/v1` |
|
|
31
|
+
| `--key` | `BETTERCMS_API_KEY` | — |
|
|
32
|
+
|
|
33
|
+
## What it emits
|
|
34
|
+
|
|
35
|
+
For a `blog` model with `title` (text, required), `body` (richtext), `hero` (group):
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
export interface BlogFields {
|
|
39
|
+
readonly title: string;
|
|
40
|
+
readonly body?: RichText;
|
|
41
|
+
readonly hero?: {
|
|
42
|
+
readonly heading: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BetterCMSSchema {
|
|
47
|
+
readonly "blog": BlogFields;
|
|
48
|
+
}
|
|
49
|
+
export type BetterCMSModelSlug = keyof BetterCMSSchema;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The `BetterCMSSchema` registry lets the Next.js adapter type `getEntry("blog", …)` by slug.
|
|
53
|
+
|
|
54
|
+
## Field type → TypeScript
|
|
55
|
+
|
|
56
|
+
| Field | TS |
|
|
57
|
+
|-------|-----|
|
|
58
|
+
| text · date · datetime | `string` |
|
|
59
|
+
| richtext | `RichText` |
|
|
60
|
+
| image | `BetterCMSImage` |
|
|
61
|
+
| boolean / number | `boolean` / `number` |
|
|
62
|
+
| select | `"a" \| "b"` (or `string`) |
|
|
63
|
+
| reference / multi-reference | `string` / `string[]` (entry ids) |
|
|
64
|
+
| array | `string[]` / `number[]` |
|
|
65
|
+
| group | `{ …nested }` |
|
|
66
|
+
| repeater | `Array<{ …nested }>` |
|
|
67
|
+
|
|
68
|
+
## Library API
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { generateTypes, fetchModels } from "@betttercms/codegen";
|
|
72
|
+
|
|
73
|
+
const models = await fetchModels({ apiUrl, apiKey });
|
|
74
|
+
const source = generateTypes(models); // pure, deterministic
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`generateTypes` is pure and deterministic — same models in, byte-identical output out.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
5
|
+
import { dirname, resolve } from "path";
|
|
6
|
+
|
|
7
|
+
// src/fetch-models.ts
|
|
8
|
+
async function fetchModels(opts) {
|
|
9
|
+
const doFetch = opts.fetchImpl ?? globalThis.fetch;
|
|
10
|
+
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
11
|
+
const url = `${base}/management/content/models`;
|
|
12
|
+
let res;
|
|
13
|
+
try {
|
|
14
|
+
res = await doFetch(url, {
|
|
15
|
+
// No Content-Type: this is a bodyless GET; the header is incorrect here and
|
|
16
|
+
// strict edge runtimes/proxies may reject it.
|
|
17
|
+
headers: { Authorization: `Bearer ${opts.apiKey}`, Accept: "application/json" }
|
|
18
|
+
});
|
|
19
|
+
} catch (err) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Could not reach the BetterCMS Management API at ${url}: ${err instanceof Error ? err.message : String(err)}`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
const hint = res.status === 401 || res.status === 403 ? " \u2014 check your management API key (it must have the content:manage scope)." : "";
|
|
26
|
+
throw new Error(`Management API returned ${res.status} ${res.statusText}${hint}`);
|
|
27
|
+
}
|
|
28
|
+
const body = await res.json();
|
|
29
|
+
const rows = body.data ?? [];
|
|
30
|
+
return rows.map((r) => ({
|
|
31
|
+
slug: r.slug,
|
|
32
|
+
name: r.name,
|
|
33
|
+
description: r.description ?? null,
|
|
34
|
+
fields: r.fields ?? []
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/generate.ts
|
|
39
|
+
var PREAMBLE = `/**
|
|
40
|
+
* Rich-text field value \u2014 the serialized portable rich-text payload returned by the
|
|
41
|
+
* Delivery API. Treated as opaque; render it with your editor's serializer.
|
|
42
|
+
*/
|
|
43
|
+
export type RichText = { readonly format: string; readonly value: unknown };
|
|
44
|
+
|
|
45
|
+
/** Image / media field value as resolved by the Delivery API. */
|
|
46
|
+
export interface BetterCMSImage {
|
|
47
|
+
readonly url: string;
|
|
48
|
+
readonly alt?: string;
|
|
49
|
+
readonly width?: number;
|
|
50
|
+
readonly height?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Delivery envelope around a model's typed \`data\`. \`getEntry\`/\`listEntries\` in the
|
|
55
|
+
* Next adapter return this shape, with \`fields\` typed by the model.
|
|
56
|
+
*/
|
|
57
|
+
export interface BetterCMSEntry<TFields> {
|
|
58
|
+
readonly slug: string;
|
|
59
|
+
readonly status: "draft" | "published";
|
|
60
|
+
readonly fields: TFields;
|
|
61
|
+
readonly updatedAt: string;
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
function pascalCase(slug) {
|
|
65
|
+
const parts = slug.split(/[-_\s]+/).filter(Boolean);
|
|
66
|
+
const pascal = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
67
|
+
return /^[0-9]/.test(pascal) ? `Model${pascal}` : pascal || "Model";
|
|
68
|
+
}
|
|
69
|
+
function escapeJsDoc(text) {
|
|
70
|
+
return text.replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ").trim();
|
|
71
|
+
}
|
|
72
|
+
var VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
73
|
+
function propName(key) {
|
|
74
|
+
return VALID_IDENT.test(key) ? key : JSON.stringify(key);
|
|
75
|
+
}
|
|
76
|
+
function scalarType(field) {
|
|
77
|
+
const t = field.type;
|
|
78
|
+
switch (t) {
|
|
79
|
+
case "text":
|
|
80
|
+
return "string";
|
|
81
|
+
case "richtext":
|
|
82
|
+
return "RichText";
|
|
83
|
+
case "image":
|
|
84
|
+
return "BetterCMSImage";
|
|
85
|
+
case "boolean":
|
|
86
|
+
return "boolean";
|
|
87
|
+
case "number":
|
|
88
|
+
return "number";
|
|
89
|
+
case "date":
|
|
90
|
+
case "datetime":
|
|
91
|
+
return "string";
|
|
92
|
+
// ISO 8601
|
|
93
|
+
case "select": {
|
|
94
|
+
const opts = field.options?.filter((o) => typeof o === "string") ?? [];
|
|
95
|
+
return opts.length > 0 ? opts.map((o) => JSON.stringify(o)).join(" | ") : "string";
|
|
96
|
+
}
|
|
97
|
+
case "reference":
|
|
98
|
+
return "string";
|
|
99
|
+
// referenced entry id
|
|
100
|
+
case "multi-reference":
|
|
101
|
+
return "string[]";
|
|
102
|
+
// referenced entry ids
|
|
103
|
+
case "array": {
|
|
104
|
+
const itemType = field.config?.itemType ?? "text";
|
|
105
|
+
const inner = itemType === "number" ? "number" : "string";
|
|
106
|
+
return `${inner}[]`;
|
|
107
|
+
}
|
|
108
|
+
case "group":
|
|
109
|
+
case "repeater":
|
|
110
|
+
return "unknown";
|
|
111
|
+
default: {
|
|
112
|
+
const _exhaustive = t;
|
|
113
|
+
return "unknown";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function fieldsToBody(fields, indent) {
|
|
118
|
+
const lines = [];
|
|
119
|
+
for (const field of fields) {
|
|
120
|
+
const optional = field.required ? "" : "?";
|
|
121
|
+
let typeExpr;
|
|
122
|
+
if (field.type === "group") {
|
|
123
|
+
const nested = fieldsToBody(field.fields ?? [], indent + " ");
|
|
124
|
+
typeExpr = `{
|
|
125
|
+
${nested}
|
|
126
|
+
${indent}}`;
|
|
127
|
+
} else if (field.type === "repeater") {
|
|
128
|
+
const nested = fieldsToBody(field.fields ?? [], indent + " ");
|
|
129
|
+
typeExpr = `Array<{
|
|
130
|
+
${nested}
|
|
131
|
+
${indent}}>`;
|
|
132
|
+
} else {
|
|
133
|
+
typeExpr = scalarType(field);
|
|
134
|
+
}
|
|
135
|
+
const safeLabel = field.label ? escapeJsDoc(field.label) : "";
|
|
136
|
+
if (safeLabel && safeLabel !== field.key) {
|
|
137
|
+
lines.push(`${indent}/** ${safeLabel} */`);
|
|
138
|
+
}
|
|
139
|
+
lines.push(`${indent}readonly ${propName(field.key)}${optional}: ${typeExpr};`);
|
|
140
|
+
}
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
function generateTypes(models, opts = {}) {
|
|
144
|
+
const version = opts.version ?? "0.1.0";
|
|
145
|
+
const sorted = [...models].sort(
|
|
146
|
+
(a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0
|
|
147
|
+
);
|
|
148
|
+
const header = `// \u26A0\uFE0F AUTO-GENERATED by @betttercms/codegen v${version} \u2014 DO NOT EDIT.
|
|
149
|
+
// Regenerate with: npx @betttercms/codegen
|
|
150
|
+
// Source of truth: your BetterCMS content models (the same schema the dashboard
|
|
151
|
+
// builder and the MCP tools write). Re-run codegen after any schema change.
|
|
152
|
+
${opts.bannerComment ? `// ${opts.bannerComment}
|
|
153
|
+
` : ""}`;
|
|
154
|
+
const interfaces = [];
|
|
155
|
+
const mapEntries = [];
|
|
156
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
157
|
+
for (const model of sorted) {
|
|
158
|
+
const base = `${pascalCase(model.slug)}Fields`;
|
|
159
|
+
let typeName = base;
|
|
160
|
+
for (let n = 2; usedNames.has(typeName); n++) typeName = `${base}_${n}`;
|
|
161
|
+
usedNames.add(typeName);
|
|
162
|
+
const name = model.name ? escapeJsDoc(model.name) : "";
|
|
163
|
+
const desc = model.description ? escapeJsDoc(model.description) : "";
|
|
164
|
+
const doc = name ? `/**
|
|
165
|
+
* ${name}${desc ? ` \u2014 ${desc}` : ""}
|
|
166
|
+
* Model slug: \`${model.slug}\`
|
|
167
|
+
*/
|
|
168
|
+
` : "";
|
|
169
|
+
const body = model.fields.length ? fieldsToBody(model.fields, " ") : " // (no fields defined yet)";
|
|
170
|
+
interfaces.push(`${doc}export interface ${typeName} {
|
|
171
|
+
${body}
|
|
172
|
+
}`);
|
|
173
|
+
mapEntries.push(` readonly ${JSON.stringify(model.slug)}: ${typeName};`);
|
|
174
|
+
}
|
|
175
|
+
const schemaMap = `/**
|
|
176
|
+
* Registry mapping each model slug to its typed fields. The Next adapter uses this to
|
|
177
|
+
* type \`getEntry("blog", ...)\` by slug \u2014 autocomplete and exhaustiveness for free.
|
|
178
|
+
*/
|
|
179
|
+
export interface BetterCMSSchema {
|
|
180
|
+
${mapEntries.join("\n") || " // (no models defined yet)"}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Union of all model slugs. */
|
|
184
|
+
export type BetterCMSModelSlug = keyof BetterCMSSchema;`;
|
|
185
|
+
return [header, PREAMBLE, interfaces.join("\n\n"), schemaMap, ""].join("\n");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/cli.ts
|
|
189
|
+
var VERSION = "0.1.0";
|
|
190
|
+
var DEFAULT_API_URL = "https://api.bettercms.ai/api/v1";
|
|
191
|
+
var DEFAULT_OUT = "bettercms.generated.ts";
|
|
192
|
+
function parseArgs(argv) {
|
|
193
|
+
const args = {
|
|
194
|
+
apiUrl: process.env.BETTERCMS_API_URL ?? DEFAULT_API_URL,
|
|
195
|
+
apiKey: process.env.BETTERCMS_API_KEY,
|
|
196
|
+
out: DEFAULT_OUT,
|
|
197
|
+
help: false
|
|
198
|
+
};
|
|
199
|
+
for (let i = 0; i < argv.length; i++) {
|
|
200
|
+
const arg = argv[i];
|
|
201
|
+
const next = () => argv[++i];
|
|
202
|
+
switch (arg) {
|
|
203
|
+
case "--api-url":
|
|
204
|
+
args.apiUrl = next() ?? args.apiUrl;
|
|
205
|
+
break;
|
|
206
|
+
case "--key":
|
|
207
|
+
case "--api-key":
|
|
208
|
+
args.apiKey = next();
|
|
209
|
+
break;
|
|
210
|
+
case "--out":
|
|
211
|
+
case "-o":
|
|
212
|
+
args.out = next() ?? args.out;
|
|
213
|
+
break;
|
|
214
|
+
case "--help":
|
|
215
|
+
case "-h":
|
|
216
|
+
args.help = true;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return args;
|
|
221
|
+
}
|
|
222
|
+
var HELP = `bettercms-codegen v${VERSION} \u2014 generate TypeScript types from your BetterCMS schema
|
|
223
|
+
|
|
224
|
+
Usage:
|
|
225
|
+
npx @betttercms/codegen [options]
|
|
226
|
+
|
|
227
|
+
Options:
|
|
228
|
+
-o, --out <path> Output file (default: ${DEFAULT_OUT})
|
|
229
|
+
--api-url <url> Management API base (default: ${DEFAULT_API_URL})
|
|
230
|
+
--key <key> Management API key (or set BETTERCMS_API_KEY)
|
|
231
|
+
-h, --help Show this help
|
|
232
|
+
|
|
233
|
+
Env:
|
|
234
|
+
BETTERCMS_API_KEY Management-scoped key (content:manage)
|
|
235
|
+
BETTERCMS_API_URL Override the API base
|
|
236
|
+
`;
|
|
237
|
+
async function main() {
|
|
238
|
+
const args = parseArgs(process.argv.slice(2));
|
|
239
|
+
if (args.help) {
|
|
240
|
+
process.stdout.write(HELP);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (!args.apiKey) {
|
|
244
|
+
process.stderr.write(
|
|
245
|
+
"error: no API key. Pass --key <key> or set BETTERCMS_API_KEY.\n"
|
|
246
|
+
);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
const models = await fetchModels({ apiUrl: args.apiUrl, apiKey: args.apiKey });
|
|
250
|
+
const source = generateTypes(models, { version: VERSION });
|
|
251
|
+
const outPath = resolve(process.cwd(), args.out);
|
|
252
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
253
|
+
await writeFile(outPath, source, "utf8");
|
|
254
|
+
process.stdout.write(
|
|
255
|
+
`\u2713 Generated ${models.length} model type${models.length === 1 ? "" : "s"} \u2192 ${args.out}
|
|
256
|
+
`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
main().catch((err) => {
|
|
260
|
+
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
|
|
261
|
+
`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|
|
264
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/fetch-models.ts","../src/generate.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 @betttercms/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\";\n\nconst VERSION = \"0.1.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 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 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 \"--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 @betttercms/codegen [options]\n\nOptions:\n -o, --out <path> Output file (default: ${DEFAULT_OUT})\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 const source = generateTypes(models, { version: VERSION });\n\n const outPath = resolve(process.cwd(), args.out);\n await mkdir(dirname(outPath), { recursive: true });\n await writeFile(outPath, source, \"utf8\");\n\n process.stdout.write(\n `✓ Generated ${models.length} model type${models.length === 1 ? \"\" : \"s\"} → ${args.out}\\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 * @betttercms/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 \"@betttercms/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 — the serialized portable rich-text payload returned by the\n * Delivery API. Treated as opaque; render it with your editor's serializer.\n */\nexport type RichText = { readonly format: string; readonly value: unknown };\n\n/** Image / media field value as resolved by the Delivery API. */\nexport interface BetterCMSImage {\n readonly url: string;\n readonly alt?: string;\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 const itemType = (field.config?.itemType as string) ?? \"text\";\n const inner =\n itemType === \"number\" ? \"number\" : \"string\"; // text | date → string\n return `${inner}[]`;\n }\n case \"group\":\n case \"repeater\":\n // Handled by the caller (needs nested-field expansion); never reached.\n return \"unknown\";\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/** 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 === \"group\") {\n const nested = fieldsToBody(field.fields ?? [], indent + \" \");\n typeExpr = `{\\n${nested}\\n${indent}}`;\n } else if (field.type === \"repeater\") {\n const nested = fieldsToBody(field.fields ?? [], indent + \" \");\n typeExpr = `Array<{\\n${nested}\\n${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 @betttercms/codegen v${version} — DO NOT EDIT.\n// Regenerate with: npx @betttercms/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"],"mappings":";;;AAUA,SAAS,WAAW,aAAa;AACjC,SAAS,SAAS,eAAe;;;ACiBjC,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;;;AClCA,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;AA2BjB,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;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,IACT,KAAK;AACH,aAAO;AAAA;AAAA,IACT,KAAK,SAAS;AACZ,YAAM,WAAY,MAAM,QAAQ,YAAuB;AACvD,YAAM,QACJ,aAAa,WAAW,WAAW;AACrC,aAAO,GAAG,KAAK;AAAA,IACjB;AAAA,IACA,KAAK;AAAA,IACL,KAAK;AAEH,aAAO;AAAA,IACT,SAAS;AAGP,YAAM,cAAqB;AAC3B,aAAO;AAAA,IACT;AAAA,EACF;AACF;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,SAAS;AAC1B,YAAM,SAAS,aAAa,MAAM,UAAU,CAAC,GAAG,SAAS,IAAI;AAC7D,iBAAW;AAAA,EAAM,MAAM;AAAA,EAAK,MAAM;AAAA,IACpC,WAAW,MAAM,SAAS,YAAY;AACpC,YAAM,SAAS,aAAa,MAAM,UAAU,CAAC,GAAG,SAAS,IAAI;AAC7D,iBAAW;AAAA,EAAY,MAAM;AAAA,EAAK,MAAM;AAAA,IAC1C,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,2DAAiD,OAAO;AAAA;AAAA;AAAA;AAAA,EAIvE,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;;;AF7MA,IAAM,UAAU;AAChB,IAAM,kBAAkB;AACxB,IAAM,cAAc;AASpB,SAAS,UAAU,MAAyB;AAC1C,QAAM,OAAgB;AAAA,IACpB,QAAQ,QAAQ,IAAI,qBAAqB;AAAA,IACzC,QAAQ,QAAQ,IAAI;AAAA,IACpB,KAAK;AAAA,IACL,MAAM;AAAA,EACR;AACA,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,MAAM,KAAK,CAAC;AAClB,UAAM,OAAO,MAAM,KAAK,EAAE,CAAC;AAC3B,YAAQ,KAAK;AAAA,MACX,KAAK;AACH,aAAK,SAAS,KAAK,KAAK,KAAK;AAC7B;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,aAAK,SAAS,KAAK;AACnB;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,aAAK,MAAM,KAAK,KAAK,KAAK;AAC1B;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,aAAK,OAAO;AACZ;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;AAEA,IAAM,OAAO,sBAAsB,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gDAMM,WAAW;AAAA,wDACH,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASvE,eAAe,OAAsB;AACnC,QAAM,OAAO,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;AAE5C,MAAI,KAAK,MAAM;AACb,YAAQ,OAAO,MAAM,IAAI;AACzB;AAAA,EACF;AACA,MAAI,CAAC,KAAK,QAAQ;AAChB,YAAQ,OAAO;AAAA,MACb;AAAA,IACF;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,SAAS,MAAM,YAAY,EAAE,QAAQ,KAAK,QAAQ,QAAQ,KAAK,OAAO,CAAC;AAC7E,QAAM,SAAS,cAAc,QAAQ,EAAE,SAAS,QAAQ,CAAC;AAEzD,QAAM,UAAU,QAAQ,QAAQ,IAAI,GAAG,KAAK,GAAG;AAC/C,QAAM,MAAM,QAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AACjD,QAAM,UAAU,SAAS,QAAQ,MAAM;AAEvC,UAAQ,OAAO;AAAA,IACb,oBAAe,OAAO,MAAM,cAAc,OAAO,WAAW,IAAI,KAAK,GAAG,WAAM,KAAK,GAAG;AAAA;AAAA,EACxF;AACF;AAEA,KAAK,EAAE,MAAM,CAAC,QAAiB;AAC7B,UAAQ,OAAO,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AACnF,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ContentModelField } from '@betttercms/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @betttercms/codegen — schema → TypeScript generator (the single source of truth).
|
|
5
|
+
*
|
|
6
|
+
* Both the dashboard schema builder and the MCP `create_model`/`add_field` tools write
|
|
7
|
+
* the SAME `content_models.fields` (an array of `ContentModelField`). This generator maps
|
|
8
|
+
* that one array into TypeScript. Because there is exactly one schema representation, the
|
|
9
|
+
* generated types can never drift from the editor or the agent — they are the same source.
|
|
10
|
+
*
|
|
11
|
+
* Pure + deterministic: same models in → identical string out (stable ordering, no clock,
|
|
12
|
+
* no I/O). That makes it trivially testable and safe to commit + diff in a customer repo.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Minimal model shape the generator needs — a subset of the Management API model row. */
|
|
16
|
+
interface GeneratableModel {
|
|
17
|
+
/** Machine-safe slug, e.g. "blog" or "case-study". Used for the schema-map key. */
|
|
18
|
+
slug: string;
|
|
19
|
+
/** Human name, used only for the JSDoc header. */
|
|
20
|
+
name?: string;
|
|
21
|
+
description?: string | null;
|
|
22
|
+
fields: ContentModelField[];
|
|
23
|
+
}
|
|
24
|
+
interface GenerateOptions {
|
|
25
|
+
/** Generator version stamped into the header (defaults to the package version). */
|
|
26
|
+
version?: string;
|
|
27
|
+
/** Override the banner timestamp source — omitted by default so output is deterministic. */
|
|
28
|
+
bannerComment?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate a complete `.ts` module from a set of content models.
|
|
32
|
+
* Deterministic: models are sorted by slug; field order is preserved as authored.
|
|
33
|
+
*/
|
|
34
|
+
declare function generateTypes(models: GeneratableModel[], opts?: GenerateOptions): string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fetches content models from the BetterCMS Management API so the CLI can generate
|
|
38
|
+
* types against a live project. Kept dependency-free (plain fetch) so the generated
|
|
39
|
+
* artifact and this fetcher can run anywhere — a GitHub Action, a postinstall, a script.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
interface FetchModelsOptions {
|
|
43
|
+
/** Management API base, e.g. "https://api.bettercms.ai/api/v1". */
|
|
44
|
+
apiUrl: string;
|
|
45
|
+
/** A management-scoped key (content:manage) or device-minted token. */
|
|
46
|
+
apiKey: string;
|
|
47
|
+
/** Optional fetch override (testing / custom runtime). */
|
|
48
|
+
fetchImpl?: typeof fetch;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* GET /management/content/models — returns the project's models (the key is
|
|
52
|
+
* project-scoped server-side, so this is exactly the schema for this site).
|
|
53
|
+
*/
|
|
54
|
+
declare function fetchModels(opts: FetchModelsOptions): Promise<GeneratableModel[]>;
|
|
55
|
+
|
|
56
|
+
export { type FetchModelsOptions, type GeneratableModel, type GenerateOptions, fetchModels, generateTypes };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// src/generate.ts
|
|
2
|
+
var PREAMBLE = `/**
|
|
3
|
+
* Rich-text field value \u2014 the serialized portable rich-text payload returned by the
|
|
4
|
+
* Delivery API. Treated as opaque; render it with your editor's serializer.
|
|
5
|
+
*/
|
|
6
|
+
export type RichText = { readonly format: string; readonly value: unknown };
|
|
7
|
+
|
|
8
|
+
/** Image / media field value as resolved by the Delivery API. */
|
|
9
|
+
export interface BetterCMSImage {
|
|
10
|
+
readonly url: string;
|
|
11
|
+
readonly alt?: string;
|
|
12
|
+
readonly width?: number;
|
|
13
|
+
readonly height?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Delivery envelope around a model's typed \`data\`. \`getEntry\`/\`listEntries\` in the
|
|
18
|
+
* Next adapter return this shape, with \`fields\` typed by the model.
|
|
19
|
+
*/
|
|
20
|
+
export interface BetterCMSEntry<TFields> {
|
|
21
|
+
readonly slug: string;
|
|
22
|
+
readonly status: "draft" | "published";
|
|
23
|
+
readonly fields: TFields;
|
|
24
|
+
readonly updatedAt: string;
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
function pascalCase(slug) {
|
|
28
|
+
const parts = slug.split(/[-_\s]+/).filter(Boolean);
|
|
29
|
+
const pascal = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
30
|
+
return /^[0-9]/.test(pascal) ? `Model${pascal}` : pascal || "Model";
|
|
31
|
+
}
|
|
32
|
+
function escapeJsDoc(text) {
|
|
33
|
+
return text.replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ").trim();
|
|
34
|
+
}
|
|
35
|
+
var VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
36
|
+
function propName(key) {
|
|
37
|
+
return VALID_IDENT.test(key) ? key : JSON.stringify(key);
|
|
38
|
+
}
|
|
39
|
+
function scalarType(field) {
|
|
40
|
+
const t = field.type;
|
|
41
|
+
switch (t) {
|
|
42
|
+
case "text":
|
|
43
|
+
return "string";
|
|
44
|
+
case "richtext":
|
|
45
|
+
return "RichText";
|
|
46
|
+
case "image":
|
|
47
|
+
return "BetterCMSImage";
|
|
48
|
+
case "boolean":
|
|
49
|
+
return "boolean";
|
|
50
|
+
case "number":
|
|
51
|
+
return "number";
|
|
52
|
+
case "date":
|
|
53
|
+
case "datetime":
|
|
54
|
+
return "string";
|
|
55
|
+
// ISO 8601
|
|
56
|
+
case "select": {
|
|
57
|
+
const opts = field.options?.filter((o) => typeof o === "string") ?? [];
|
|
58
|
+
return opts.length > 0 ? opts.map((o) => JSON.stringify(o)).join(" | ") : "string";
|
|
59
|
+
}
|
|
60
|
+
case "reference":
|
|
61
|
+
return "string";
|
|
62
|
+
// referenced entry id
|
|
63
|
+
case "multi-reference":
|
|
64
|
+
return "string[]";
|
|
65
|
+
// referenced entry ids
|
|
66
|
+
case "array": {
|
|
67
|
+
const itemType = field.config?.itemType ?? "text";
|
|
68
|
+
const inner = itemType === "number" ? "number" : "string";
|
|
69
|
+
return `${inner}[]`;
|
|
70
|
+
}
|
|
71
|
+
case "group":
|
|
72
|
+
case "repeater":
|
|
73
|
+
return "unknown";
|
|
74
|
+
default: {
|
|
75
|
+
const _exhaustive = t;
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function fieldsToBody(fields, indent) {
|
|
81
|
+
const lines = [];
|
|
82
|
+
for (const field of fields) {
|
|
83
|
+
const optional = field.required ? "" : "?";
|
|
84
|
+
let typeExpr;
|
|
85
|
+
if (field.type === "group") {
|
|
86
|
+
const nested = fieldsToBody(field.fields ?? [], indent + " ");
|
|
87
|
+
typeExpr = `{
|
|
88
|
+
${nested}
|
|
89
|
+
${indent}}`;
|
|
90
|
+
} else if (field.type === "repeater") {
|
|
91
|
+
const nested = fieldsToBody(field.fields ?? [], indent + " ");
|
|
92
|
+
typeExpr = `Array<{
|
|
93
|
+
${nested}
|
|
94
|
+
${indent}}>`;
|
|
95
|
+
} else {
|
|
96
|
+
typeExpr = scalarType(field);
|
|
97
|
+
}
|
|
98
|
+
const safeLabel = field.label ? escapeJsDoc(field.label) : "";
|
|
99
|
+
if (safeLabel && safeLabel !== field.key) {
|
|
100
|
+
lines.push(`${indent}/** ${safeLabel} */`);
|
|
101
|
+
}
|
|
102
|
+
lines.push(`${indent}readonly ${propName(field.key)}${optional}: ${typeExpr};`);
|
|
103
|
+
}
|
|
104
|
+
return lines.join("\n");
|
|
105
|
+
}
|
|
106
|
+
function generateTypes(models, opts = {}) {
|
|
107
|
+
const version = opts.version ?? "0.1.0";
|
|
108
|
+
const sorted = [...models].sort(
|
|
109
|
+
(a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0
|
|
110
|
+
);
|
|
111
|
+
const header = `// \u26A0\uFE0F AUTO-GENERATED by @betttercms/codegen v${version} \u2014 DO NOT EDIT.
|
|
112
|
+
// Regenerate with: npx @betttercms/codegen
|
|
113
|
+
// Source of truth: your BetterCMS content models (the same schema the dashboard
|
|
114
|
+
// builder and the MCP tools write). Re-run codegen after any schema change.
|
|
115
|
+
${opts.bannerComment ? `// ${opts.bannerComment}
|
|
116
|
+
` : ""}`;
|
|
117
|
+
const interfaces = [];
|
|
118
|
+
const mapEntries = [];
|
|
119
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
120
|
+
for (const model of sorted) {
|
|
121
|
+
const base = `${pascalCase(model.slug)}Fields`;
|
|
122
|
+
let typeName = base;
|
|
123
|
+
for (let n = 2; usedNames.has(typeName); n++) typeName = `${base}_${n}`;
|
|
124
|
+
usedNames.add(typeName);
|
|
125
|
+
const name = model.name ? escapeJsDoc(model.name) : "";
|
|
126
|
+
const desc = model.description ? escapeJsDoc(model.description) : "";
|
|
127
|
+
const doc = name ? `/**
|
|
128
|
+
* ${name}${desc ? ` \u2014 ${desc}` : ""}
|
|
129
|
+
* Model slug: \`${model.slug}\`
|
|
130
|
+
*/
|
|
131
|
+
` : "";
|
|
132
|
+
const body = model.fields.length ? fieldsToBody(model.fields, " ") : " // (no fields defined yet)";
|
|
133
|
+
interfaces.push(`${doc}export interface ${typeName} {
|
|
134
|
+
${body}
|
|
135
|
+
}`);
|
|
136
|
+
mapEntries.push(` readonly ${JSON.stringify(model.slug)}: ${typeName};`);
|
|
137
|
+
}
|
|
138
|
+
const schemaMap = `/**
|
|
139
|
+
* Registry mapping each model slug to its typed fields. The Next adapter uses this to
|
|
140
|
+
* type \`getEntry("blog", ...)\` by slug \u2014 autocomplete and exhaustiveness for free.
|
|
141
|
+
*/
|
|
142
|
+
export interface BetterCMSSchema {
|
|
143
|
+
${mapEntries.join("\n") || " // (no models defined yet)"}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Union of all model slugs. */
|
|
147
|
+
export type BetterCMSModelSlug = keyof BetterCMSSchema;`;
|
|
148
|
+
return [header, PREAMBLE, interfaces.join("\n\n"), schemaMap, ""].join("\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/fetch-models.ts
|
|
152
|
+
async function fetchModels(opts) {
|
|
153
|
+
const doFetch = opts.fetchImpl ?? globalThis.fetch;
|
|
154
|
+
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
155
|
+
const url = `${base}/management/content/models`;
|
|
156
|
+
let res;
|
|
157
|
+
try {
|
|
158
|
+
res = await doFetch(url, {
|
|
159
|
+
// No Content-Type: this is a bodyless GET; the header is incorrect here and
|
|
160
|
+
// strict edge runtimes/proxies may reject it.
|
|
161
|
+
headers: { Authorization: `Bearer ${opts.apiKey}`, Accept: "application/json" }
|
|
162
|
+
});
|
|
163
|
+
} catch (err) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Could not reach the BetterCMS Management API at ${url}: ${err instanceof Error ? err.message : String(err)}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
const hint = res.status === 401 || res.status === 403 ? " \u2014 check your management API key (it must have the content:manage scope)." : "";
|
|
170
|
+
throw new Error(`Management API returned ${res.status} ${res.statusText}${hint}`);
|
|
171
|
+
}
|
|
172
|
+
const body = await res.json();
|
|
173
|
+
const rows = body.data ?? [];
|
|
174
|
+
return rows.map((r) => ({
|
|
175
|
+
slug: r.slug,
|
|
176
|
+
name: r.name,
|
|
177
|
+
description: r.description ?? null,
|
|
178
|
+
fields: r.fields ?? []
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
export {
|
|
182
|
+
fetchModels,
|
|
183
|
+
generateTypes
|
|
184
|
+
};
|
|
185
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/generate.ts","../src/fetch-models.ts"],"sourcesContent":["/**\n * @betttercms/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 \"@betttercms/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 — the serialized portable rich-text payload returned by the\n * Delivery API. Treated as opaque; render it with your editor's serializer.\n */\nexport type RichText = { readonly format: string; readonly value: unknown };\n\n/** Image / media field value as resolved by the Delivery API. */\nexport interface BetterCMSImage {\n readonly url: string;\n readonly alt?: string;\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 const itemType = (field.config?.itemType as string) ?? \"text\";\n const inner =\n itemType === \"number\" ? \"number\" : \"string\"; // text | date → string\n return `${inner}[]`;\n }\n case \"group\":\n case \"repeater\":\n // Handled by the caller (needs nested-field expansion); never reached.\n return \"unknown\";\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/** 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 === \"group\") {\n const nested = fieldsToBody(field.fields ?? [], indent + \" \");\n typeExpr = `{\\n${nested}\\n${indent}}`;\n } else if (field.type === \"repeater\") {\n const nested = fieldsToBody(field.fields ?? [], indent + \" \");\n typeExpr = `Array<{\\n${nested}\\n${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 @betttercms/codegen v${version} — DO NOT EDIT.\n// Regenerate with: npx @betttercms/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 * 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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@betttercms/codegen",
|
|
3
|
+
"version": "0.1.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
|
+
"@betttercms/types": "^1.0.17"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20",
|
|
35
|
+
"tsup": "^8.5.1",
|
|
36
|
+
"vitest": "^4.1.4"
|
|
37
|
+
}
|
|
38
|
+
}
|