@backmatter/core 0.0.15 → 0.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backmatter/core",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Experimental Backmatter SDK. APIs will break. Not ready for production.",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/src/config.ts CHANGED
@@ -8,6 +8,8 @@ export type BackmatterDirs = {
8
8
  models?: string;
9
9
  /** Folder with one `defineBlock` default export per file. Default: "blocks". */
10
10
  blocks?: string;
11
+ /** Folder with one in-content `defineBlock` per file. Default: "richtext". */
12
+ richtext?: string;
11
13
  /** Folder with one MDX tag component default export per file. Default: "mdx". */
12
14
  mdx?: string;
13
15
  /** Folder with one layout default export per model type. Default: "layouts". */
@@ -51,6 +53,7 @@ export type ResolvedConfig = Omit<BackmatterConfig, "dirs" | "defaultLocale"> &
51
53
  export const DEFAULT_DIRS: Required<BackmatterDirs> = {
52
54
  models: "models",
53
55
  blocks: "blocks",
56
+ richtext: "richtext",
54
57
  mdx: "mdx",
55
58
  layouts: "layouts",
56
59
  objects: "objects",
@@ -159,6 +162,7 @@ export function resolveConfig(
159
162
  dirs: {
160
163
  models: config.dirs?.models ?? DEFAULT_DIRS.models,
161
164
  blocks: config.dirs?.blocks ?? DEFAULT_DIRS.blocks,
165
+ richtext: config.dirs?.richtext ?? DEFAULT_DIRS.richtext,
162
166
  mdx: config.dirs?.mdx ?? DEFAULT_DIRS.mdx,
163
167
  layouts: config.dirs?.layouts ?? DEFAULT_DIRS.layouts,
164
168
  objects: config.dirs?.objects ?? DEFAULT_DIRS.objects,
package/src/model.ts CHANGED
@@ -85,6 +85,8 @@ export type SchemaSnapshot = {
85
85
  version: number;
86
86
  models: ModelSnapshot[];
87
87
  blocks?: BlockSnapshot[];
88
+ /** In-content blocks from `richtext/`. Separate from page-builder `blocks`. */
89
+ richtext?: BlockSnapshot[];
88
90
  objects?: ObjectSnapshot[];
89
91
  };
90
92
 
@@ -178,6 +180,7 @@ export function serializeSchema(
178
180
  models: ModelDef[],
179
181
  blocks?: BlockDef[],
180
182
  objects?: ObjectDef[],
183
+ richtext?: BlockDef[],
181
184
  ): SchemaSnapshot {
182
185
  const snapshot: SchemaSnapshot = {
183
186
  version: 1,
@@ -186,6 +189,9 @@ export function serializeSchema(
186
189
  if (blocks && blocks.length > 0) {
187
190
  snapshot.blocks = blocks.map(serializeBlock);
188
191
  }
192
+ if (richtext && richtext.length > 0) {
193
+ snapshot.richtext = richtext.map(serializeBlock);
194
+ }
189
195
  if (objects && objects.length > 0) {
190
196
  snapshot.objects = objects.map(serializeObject);
191
197
  }
@@ -301,6 +307,28 @@ export function parseSchemaSnapshot(input: unknown): SchemaSnapshot {
301
307
  }
302
308
  }
303
309
 
310
+ let richtext: BlockSnapshot[] = [];
311
+ if (value.richtext !== undefined) {
312
+ if (!Array.isArray(value.richtext)) {
313
+ throw new Error("schema.richtext must be an array");
314
+ }
315
+ richtext = value.richtext.map((raw, i) => {
316
+ try {
317
+ return parseBlockSnapshot(raw);
318
+ } catch (err) {
319
+ const msg = err instanceof Error ? err.message : "invalid block";
320
+ throw new Error(`schema.richtext[${i}]: ${msg}`);
321
+ }
322
+ });
323
+ const richtextNames = new Set<string>();
324
+ for (const block of richtext) {
325
+ if (richtextNames.has(block.name)) {
326
+ throw new Error(`duplicate richtext block name: ${block.name}`);
327
+ }
328
+ richtextNames.add(block.name);
329
+ }
330
+ }
331
+
304
332
  let objects: ObjectSnapshot[] = [];
305
333
  if (value.objects !== undefined) {
306
334
  if (!Array.isArray(value.objects)) {
@@ -323,5 +351,11 @@ export function parseSchemaSnapshot(input: unknown): SchemaSnapshot {
323
351
  }
324
352
  }
325
353
 
326
- return { version, models, blocks, objects };
354
+ return {
355
+ version,
356
+ models,
357
+ blocks,
358
+ ...(richtext.length > 0 ? { richtext } : {}),
359
+ objects,
360
+ };
327
361
  }
package/src/node/cli.ts CHANGED
@@ -234,8 +234,10 @@ export async function runGenerate(args: CliArgs): Promise<GenerateResult> {
234
234
  root,
235
235
  modelsDir: config.dirs.models,
236
236
  blocksDir: config.dirs.blocks,
237
+ richtextDir: config.dirs.richtext,
237
238
  mdxDir: config.dirs.mdx,
238
239
  layoutsDir: config.dirs.layouts,
240
+ objectsDir: config.dirs.objects,
239
241
  });
240
242
  }
241
243
 
@@ -247,8 +249,9 @@ function renderConfigFile(sdk: string): string {
247
249
  import { defineConfig } from "${from}";
248
250
 
249
251
  // General settings only. Models live in models/*.ts, composer blocks in
250
- // blocks/*.tsx, MDX tags in mdx/*.tsx, and content in
251
- // content/{locale}/{type}/{slug}.{mdx,json} — all discovered automatically.
252
+ // blocks/*.tsx, in-content blocks in richtext/*.tsx, MDX tags in mdx/*.tsx,
253
+ // and content in content/{locale}/{type}/{slug}.{mdx,json} — all discovered
254
+ // automatically.
252
255
  // Set workspaceId from General settings (not an env var).
253
256
  // API URL defaults to https://api.backmatter.app in production; tokens stay env.
254
257
  export default defineConfig({
@@ -263,11 +266,13 @@ function renderCatchAllPage(registryImport: string): string {
263
266
  import { cacheLife } from "next/cache";
264
267
  import { createCatchAll } from "@backmatter/next/routes";
265
268
  import { cms } from "${registryImport}";
266
- import { components } from "${registryImport}/mdx";
269
+ import { components as mdxComponents } from "${registryImport}/mdx";
270
+ import { richtextComponents } from "${registryImport}/richtext";
267
271
  import { blockLoaders } from "${registryImport}/blocks";
268
272
  import { blockFallbacks } from "${registryImport}/block-fallbacks";
269
273
  import { layouts } from "${registryImport}/layouts";
270
274
 
275
+ const components = { ...richtextComponents, ...mdxComponents };
271
276
  const route = createCatchAll(cms, { components, blocks: blockLoaders, fallbacks: blockFallbacks, layouts });
272
277
 
273
278
  export const generateStaticParams = route.generateStaticParams;
@@ -451,7 +456,7 @@ export async function runInit(args: CliArgs): Promise<string[]> {
451
456
  notes.push("no @backmatter/next dependency: skipped the catch-all route");
452
457
  }
453
458
 
454
- for (const dir of ["models", "content", "blocks", "mdx", "layouts"]) {
459
+ for (const dir of ["models", "content", "blocks", "richtext", "mdx", "layouts"]) {
455
460
  await mkdir(path.join(root, dir), { recursive: true });
456
461
  }
457
462
 
@@ -18,6 +18,8 @@ export type CreateBackmatterOptions<Models extends readonly ModelDef[] = readonl
18
18
  BackmatterConfig & {
19
19
  models: Models;
20
20
  blocks?: readonly BlockDef[];
21
+ /** In-content blocks (`richtext/*.tsx`). Synced separately from page blocks. */
22
+ richtext?: readonly BlockDef[];
21
23
  objects?: readonly ObjectDef[];
22
24
  /**
23
25
  * When true, API fetches use Next cache tags instead of `cache: "no-store"`.
@@ -67,7 +69,7 @@ function toSnapshots(models: readonly ModelDef[]): ModelSnapshot[] {
67
69
  export function createBackmatter<const Models extends readonly ModelDef[]>(
68
70
  options: CreateBackmatterOptions<Models>,
69
71
  ): BackmatterClient<Models> {
70
- const { models: modelDefs, blocks: blockDefs, objects: objectDefs, taggedApiFetch, sync = true, ...rest } = options;
72
+ const { models: modelDefs, blocks: blockDefs, richtext: richtextDefs, objects: objectDefs, taggedApiFetch, sync = true, ...rest } = options;
71
73
  const config = resolveConfig(rest);
72
74
  const models = toSnapshots(modelDefs);
73
75
  const siteRoot = config.siteRoot ?? process.cwd();
@@ -91,6 +93,7 @@ export function createBackmatter<const Models extends readonly ModelDef[]>(
91
93
  hostToken,
92
94
  models: [...modelDefs],
93
95
  blocks: blockDefs ? [...blockDefs] : undefined,
96
+ richtext: richtextDefs ? [...richtextDefs] : undefined,
94
97
  objects: objectDefs ? [...objectDefs] : undefined,
95
98
  branch: config.branch,
96
99
  siteUrl,
@@ -31,6 +31,7 @@ export type GenerateOptions = {
31
31
  outDir?: string;
32
32
  modelsDir?: string;
33
33
  blocksDir?: string;
34
+ richtextDir?: string;
34
35
  mdxDir?: string;
35
36
  layoutsDir?: string;
36
37
  objectsDir?: string;
@@ -59,6 +60,7 @@ export type GenerateResult = {
59
60
  changed: string[];
60
61
  models: ModelFile[];
61
62
  blocks: BlockFile[];
63
+ richtext: BlockFile[];
62
64
  mdx: BlockFile[];
63
65
  layouts: ModelFile[];
64
66
  objects: ModelFile[];
@@ -161,11 +163,13 @@ export function renderSyncBarrel(input: {
161
163
  outDir: string;
162
164
  models: ModelFile[];
163
165
  blocks: BlockFile[];
166
+ richtext?: BlockFile[];
164
167
  objects?: ModelFile[];
165
168
  configFile: string | null;
166
169
  sdk: string;
167
170
  }): string {
168
171
  const objects = input.objects ?? [];
172
+ const richtext = input.richtext ?? [];
169
173
  const lines = [GENERATED_HEADER, `import { createBackmatter } from "${input.sdk}";`];
170
174
  if (input.configFile) {
171
175
  lines.push(`import config from "${importSpecifier(input.outDir, input.configFile)}";`);
@@ -178,6 +182,9 @@ export function renderSyncBarrel(input: {
178
182
  for (const b of input.blocks) {
179
183
  lines.push(`import ${b.name} from "${importSpecifier(input.outDir, b.file)}";`);
180
184
  }
185
+ for (const b of richtext) {
186
+ lines.push(`import richtext${b.name} from "${importSpecifier(input.outDir, b.file)}";`);
187
+ }
181
188
  for (const object of objects) {
182
189
  lines.push(`import ${object.importName} from "${importSpecifier(input.outDir, object.file)}";`);
183
190
  }
@@ -187,10 +194,16 @@ export function renderSyncBarrel(input: {
187
194
  lines.push("");
188
195
  lines.push(`export const blocks = [${input.blocks.map((b) => b.name).join(", ")}] as const;`);
189
196
  lines.push("");
197
+ lines.push(
198
+ `export const richtext = [${richtext.map((b) => `richtext${b.name}`).join(", ")}] as const;`,
199
+ );
200
+ lines.push("");
190
201
  lines.push(`export const objects = [${objects.map((o) => o.importName).join(", ")}] as const;`);
191
202
  lines.push("");
192
203
  lines.push("/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */");
193
- lines.push("export const cms = createBackmatter({ ...config, models, blocks, objects, sync: true });");
204
+ lines.push(
205
+ "export const cms = createBackmatter({ ...config, models, blocks, richtext, objects, sync: true });",
206
+ );
194
207
  return `${lines.join("\n")}\n`;
195
208
  }
196
209
 
@@ -206,6 +219,18 @@ export function renderMdxBarrel(input: { outDir: string; mdx: BlockFile[] }): st
206
219
  return `${lines.join("\n")}\n`;
207
220
  }
208
221
 
222
+ export function renderRichtextBarrel(input: { outDir: string; richtext: BlockFile[] }): string {
223
+ const lines = [GENERATED_HEADER];
224
+ for (const b of input.richtext) {
225
+ lines.push(`import ${b.name} from "${importSpecifier(input.outDir, b.file)}";`);
226
+ }
227
+ lines.push("");
228
+ lines.push("/** In-content blocks: <Image /> in MDX resolves to richtext/Image.tsx. */");
229
+ const entries = input.richtext.map((b) => `${b.name}: ${b.name}.component`);
230
+ lines.push(`export const richtextComponents = ${entries.length ? `{ ${entries.join(", ")} }` : "{}"};`);
231
+ return `${lines.join("\n")}\n`;
232
+ }
233
+
209
234
  export function renderBlocksBarrel(input: { outDir: string; blocks: BlockFile[] }): string {
210
235
  const lines = [GENERATED_HEADER, ""];
211
236
  lines.push('/** Composer loaders: JSON `{ type: "Hero" }` → dynamic import of blocks/Hero.tsx. */');
@@ -257,6 +282,7 @@ export const REGISTRY_FILES = [
257
282
  "blocks.ts",
258
283
  "block-fallbacks.ts",
259
284
  "mdx.ts",
285
+ "richtext.ts",
260
286
  "layouts.ts",
261
287
  "sync.ts",
262
288
  ] as const;
@@ -269,6 +295,10 @@ export function generate(options: GenerateOptions): GenerateResult {
269
295
  root,
270
296
  options.blocksDir ?? DEFAULT_DIRS.blocks,
271
297
  );
298
+ const { blocks: richtext, skipped: skippedRichtext } = listBlockFiles(
299
+ root,
300
+ options.richtextDir ?? DEFAULT_DIRS.richtext,
301
+ );
272
302
  const fallbacks = listBlockFallbackFiles(root, options.blocksDir ?? DEFAULT_DIRS.blocks);
273
303
  const { blocks: mdx, skipped: skippedMdx } = listMdxFiles(
274
304
  root,
@@ -285,8 +315,9 @@ export function generate(options: GenerateOptions): GenerateResult {
285
315
  "blocks.ts": renderBlocksBarrel({ outDir, blocks }),
286
316
  "block-fallbacks.ts": renderBlockFallbacksBarrel({ outDir, fallbacks }),
287
317
  "mdx.ts": renderMdxBarrel({ outDir, mdx }),
318
+ "richtext.ts": renderRichtextBarrel({ outDir, richtext }),
288
319
  "layouts.ts": renderLayoutsBarrel({ outDir, layouts }),
289
- "sync.ts": renderSyncBarrel({ outDir, models, blocks, objects, configFile, sdk }),
320
+ "sync.ts": renderSyncBarrel({ outDir, models, blocks, richtext, objects, configFile, sdk }),
290
321
  };
291
322
  const outputs: GeneratedFile[] = REGISTRY_FILES.map((name) => {
292
323
  const file = path.join(outDir, name);
@@ -304,10 +335,11 @@ export function generate(options: GenerateOptions): GenerateResult {
304
335
  changed: outputs.filter((o) => o.changed).map((o) => o.rel),
305
336
  models,
306
337
  blocks,
338
+ richtext,
307
339
  mdx,
308
340
  layouts,
309
341
  objects,
310
- skippedBlocks,
342
+ skippedBlocks: [...skippedBlocks, ...skippedRichtext],
311
343
  skippedMdx,
312
344
  configFile,
313
345
  };
package/src/node/index.ts CHANGED
@@ -32,6 +32,7 @@ export {
32
32
  renderBlocksBarrel,
33
33
  renderLayoutsBarrel,
34
34
  renderMdxBarrel,
35
+ renderRichtextBarrel,
35
36
  renderModelsBarrel,
36
37
  renderSyncBarrel,
37
38
  DEFAULT_OUT_DIR,
@@ -23,6 +23,8 @@ export type SyncSchemaOptions = {
23
23
  hostToken: string;
24
24
  models: Array<ModelDef | ModelSnapshot>;
25
25
  blocks?: BlockDef[];
26
+ /** In-content blocks from the client `richtext/` directory. */
27
+ richtext?: BlockDef[];
26
28
  objects?: ObjectDef[];
27
29
  branch?: string;
28
30
  siteUrl?: string;
@@ -34,6 +36,7 @@ export async function syncSchema(options: SyncSchemaOptions): Promise<void> {
34
36
  options.models as ModelDef[],
35
37
  options.blocks,
36
38
  options.objects,
39
+ options.richtext,
37
40
  );
38
41
  const body: { payload: ReturnType<typeof serializeSchema>; branch: string; siteUrl?: string } = {
39
42
  payload,