@backmatter/core 0.0.15 → 0.0.17
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 +8 -2
- package/src/config.ts +30 -2
- package/src/model.ts +35 -1
- package/src/node/cli.ts +31 -32
- package/src/node/create-backmatter.ts +4 -1
- package/src/node/generate.ts +35 -3
- package/src/node/index.ts +10 -0
- package/src/node/registry-import.ts +335 -0
- package/src/sync-schema.ts +3 -0
package/package.json
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backmatter/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "Experimental Backmatter SDK. APIs will break. Not ready for production.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/MRued/backmatter.git",
|
|
12
|
+
"directory": "packages/core"
|
|
13
|
+
},
|
|
9
14
|
"exports": {
|
|
10
15
|
".": "./src/index.ts",
|
|
11
16
|
"./node": "./src/node/index.ts",
|
|
12
17
|
"./node/create-backmatter": "./src/node/create-backmatter.ts",
|
|
18
|
+
"./node/registry-import": "./src/node/registry-import.ts",
|
|
13
19
|
"./preview": "./src/preview.ts",
|
|
14
20
|
"./generate": "./src/node/generate.ts"
|
|
15
21
|
},
|
|
@@ -21,7 +27,7 @@
|
|
|
21
27
|
"!**/test-fixtures"
|
|
22
28
|
],
|
|
23
29
|
"bin": {
|
|
24
|
-
"backmatter": "
|
|
30
|
+
"backmatter": "bin/backmatter.js"
|
|
25
31
|
},
|
|
26
32
|
"scripts": {
|
|
27
33
|
"test": "bun test"
|
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",
|
|
@@ -95,14 +98,38 @@ export function resolveDevBarApiUrl(
|
|
|
95
98
|
return "";
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
function isLoopbackApiUrl(url: string): boolean {
|
|
102
|
+
try {
|
|
103
|
+
const { hostname } = new URL(url);
|
|
104
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Vercel, Netlify, Cloudflare Pages, or Railway — not a laptop `next build`. */
|
|
111
|
+
function onPlatformDeploy(env: Record<string, string | undefined>): boolean {
|
|
112
|
+
return Boolean(env.VERCEL || env.NETLIFY || env.CF_PAGES || env.RAILWAY_ENVIRONMENT);
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
function detectApiUrl(
|
|
99
116
|
config: BackmatterConfig,
|
|
100
117
|
env: Record<string, string | undefined>,
|
|
101
118
|
workspaceId: string | undefined,
|
|
102
119
|
): string | undefined {
|
|
103
|
-
const
|
|
120
|
+
const fromConfig = config.apiUrl !== undefined;
|
|
121
|
+
const explicit = fromConfig ? config.apiUrl : env.BACKMATTER_API_URL;
|
|
104
122
|
const trimmed = explicit?.trim();
|
|
105
|
-
|
|
123
|
+
// Next loads `.env` for every environment, so a dev loopback URL is present
|
|
124
|
+
// during `next build` and fails page-data collection when that API is down.
|
|
125
|
+
// A local production build falls through to the cloud API. An explicit
|
|
126
|
+
// config.apiUrl, `next dev`, and a real platform deploy keep the loopback URL.
|
|
127
|
+
const ignoreDevLoopback =
|
|
128
|
+
!fromConfig &&
|
|
129
|
+
Boolean(trimmed && isLoopbackApiUrl(trimmed)) &&
|
|
130
|
+
env.NEXT_PHASE === "phase-production-build" &&
|
|
131
|
+
!onPlatformDeploy(env);
|
|
132
|
+
if (trimmed && !ignoreDevLoopback) return stripTrailingSlash(trimmed);
|
|
106
133
|
if (workspaceId && isDeployedHost(env)) return CLOUD_API_URL;
|
|
107
134
|
return undefined;
|
|
108
135
|
}
|
|
@@ -159,6 +186,7 @@ export function resolveConfig(
|
|
|
159
186
|
dirs: {
|
|
160
187
|
models: config.dirs?.models ?? DEFAULT_DIRS.models,
|
|
161
188
|
blocks: config.dirs?.blocks ?? DEFAULT_DIRS.blocks,
|
|
189
|
+
richtext: config.dirs?.richtext ?? DEFAULT_DIRS.richtext,
|
|
162
190
|
mdx: config.dirs?.mdx ?? DEFAULT_DIRS.mdx,
|
|
163
191
|
layouts: config.dirs?.layouts ?? DEFAULT_DIRS.layouts,
|
|
164
192
|
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 {
|
|
354
|
+
return {
|
|
355
|
+
version,
|
|
356
|
+
models,
|
|
357
|
+
blocks,
|
|
358
|
+
...(richtext.length > 0 ? { richtext } : {}),
|
|
359
|
+
objects,
|
|
360
|
+
};
|
|
327
361
|
}
|
package/src/node/cli.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { resolveConfig, type BackmatterConfig, type ResolvedConfig } from "../co
|
|
|
17
17
|
import { discoverModels, discoverBlocks } from "./discover";
|
|
18
18
|
import { walkContentFiles } from "./fs-loader";
|
|
19
19
|
import { detectSdk, findConfigFile, generate, type GenerateResult } from "./generate";
|
|
20
|
+
import { ensureRegistryTsconfig, REGISTRY_SPECIFIER } from "./registry-import";
|
|
20
21
|
|
|
21
22
|
export type CliArgs = {
|
|
22
23
|
command?: string;
|
|
@@ -234,8 +235,10 @@ export async function runGenerate(args: CliArgs): Promise<GenerateResult> {
|
|
|
234
235
|
root,
|
|
235
236
|
modelsDir: config.dirs.models,
|
|
236
237
|
blocksDir: config.dirs.blocks,
|
|
238
|
+
richtextDir: config.dirs.richtext,
|
|
237
239
|
mdxDir: config.dirs.mdx,
|
|
238
240
|
layoutsDir: config.dirs.layouts,
|
|
241
|
+
objectsDir: config.dirs.objects,
|
|
239
242
|
});
|
|
240
243
|
}
|
|
241
244
|
|
|
@@ -247,8 +250,9 @@ function renderConfigFile(sdk: string): string {
|
|
|
247
250
|
import { defineConfig } from "${from}";
|
|
248
251
|
|
|
249
252
|
// General settings only. Models live in models/*.ts, composer blocks in
|
|
250
|
-
// blocks/*.tsx,
|
|
251
|
-
// content/{locale}/{type}/{slug}.{mdx,json} — all discovered
|
|
253
|
+
// blocks/*.tsx, in-content blocks in richtext/*.tsx, MDX tags in mdx/*.tsx,
|
|
254
|
+
// and content in content/{locale}/{type}/{slug}.{mdx,json} — all discovered
|
|
255
|
+
// automatically.
|
|
252
256
|
// Set workspaceId from General settings (not an env var).
|
|
253
257
|
// API URL defaults to https://api.backmatter.app in production; tokens stay env.
|
|
254
258
|
export default defineConfig({
|
|
@@ -258,16 +262,18 @@ export default defineConfig({
|
|
|
258
262
|
`;
|
|
259
263
|
}
|
|
260
264
|
|
|
261
|
-
function renderCatchAllPage(
|
|
265
|
+
function renderCatchAllPage(): string {
|
|
262
266
|
return `// ${CATCH_ALL_DIR}/page.tsx — mounts every model at its route, including / via indexSlug.
|
|
263
267
|
import { cacheLife } from "next/cache";
|
|
264
268
|
import { createCatchAll } from "@backmatter/next/routes";
|
|
265
|
-
import { cms } from "${
|
|
266
|
-
import { components } from "${
|
|
267
|
-
import {
|
|
268
|
-
import {
|
|
269
|
-
import {
|
|
270
|
-
|
|
269
|
+
import { cms } from "${REGISTRY_SPECIFIER}";
|
|
270
|
+
import { components as mdxComponents } from "${REGISTRY_SPECIFIER}/mdx";
|
|
271
|
+
import { richtextComponents } from "${REGISTRY_SPECIFIER}/richtext";
|
|
272
|
+
import { blockLoaders } from "${REGISTRY_SPECIFIER}/blocks";
|
|
273
|
+
import { blockFallbacks } from "${REGISTRY_SPECIFIER}/block-fallbacks";
|
|
274
|
+
import { layouts } from "${REGISTRY_SPECIFIER}/layouts";
|
|
275
|
+
|
|
276
|
+
const components = { ...richtextComponents, ...mdxComponents };
|
|
271
277
|
const route = createCatchAll(cms, { components, blocks: blockLoaders, fallbacks: blockFallbacks, layouts });
|
|
272
278
|
|
|
273
279
|
export const generateStaticParams = route.generateStaticParams;
|
|
@@ -298,12 +304,12 @@ export default async function Page(props: {
|
|
|
298
304
|
`;
|
|
299
305
|
}
|
|
300
306
|
|
|
301
|
-
function renderInstrumentation(
|
|
307
|
+
function renderInstrumentation(): string {
|
|
302
308
|
return `// instrumentation.ts — schema sync on Node. Next also runs register() on Edge;
|
|
303
|
-
// do not import
|
|
309
|
+
// do not import ${REGISTRY_SPECIFIER}/sync there (it pulls Node APIs).
|
|
304
310
|
export async function register() {
|
|
305
311
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
306
|
-
await import("${
|
|
312
|
+
await import("${REGISTRY_SPECIFIER}/sync");
|
|
307
313
|
}
|
|
308
314
|
}
|
|
309
315
|
`;
|
|
@@ -315,15 +321,15 @@ export function warnMissingInstrumentation(root: string): void {
|
|
|
315
321
|
const hasSrc = existsSync(path.join(root, "src", "instrumentation.ts"));
|
|
316
322
|
if (!hasRoot && !hasSrc) {
|
|
317
323
|
console.warn(
|
|
318
|
-
|
|
324
|
+
`backmatter: add instrumentation.ts that imports ${REGISTRY_SPECIFIER}/sync — block schema sync will not run without it`,
|
|
319
325
|
);
|
|
320
326
|
}
|
|
321
327
|
}
|
|
322
328
|
|
|
323
|
-
function renderPreviewRoute(
|
|
329
|
+
function renderPreviewRoute(): string {
|
|
324
330
|
return `// api/backmatter/preview/route.ts — preview session enable / disable / stream / session.
|
|
325
331
|
import { createPreviewRoute } from "@backmatter/next/preview-route";
|
|
326
|
-
import { cms } from "${
|
|
332
|
+
import { cms } from "${REGISTRY_SPECIFIER}";
|
|
327
333
|
|
|
328
334
|
export const { GET, POST } = createPreviewRoute(cms);
|
|
329
335
|
`;
|
|
@@ -342,10 +348,10 @@ export const config = {
|
|
|
342
348
|
`;
|
|
343
349
|
}
|
|
344
350
|
|
|
345
|
-
function renderRevalidateRoute(
|
|
351
|
+
function renderRevalidateRoute(): string {
|
|
346
352
|
return `// api/backmatter/revalidate/route.ts — content.committed from the BACKMATTER server.
|
|
347
353
|
import { createRevalidateRoute } from "@backmatter/next/revalidate";
|
|
348
|
-
import { cms } from "${
|
|
354
|
+
import { cms } from "${REGISTRY_SPECIFIER}";
|
|
349
355
|
|
|
350
356
|
export const POST = createRevalidateRoute(cms);
|
|
351
357
|
`;
|
|
@@ -381,21 +387,16 @@ export async function runInit(args: CliArgs): Promise<string[]> {
|
|
|
381
387
|
} else if (existsSync(requiredCatchAll)) {
|
|
382
388
|
notes.push(`kept existing ${path.join(appDir, "[...backmatter]", "page.tsx")}`);
|
|
383
389
|
} else {
|
|
384
|
-
const registry = path.relative(pageDir, path.join(root, ".backmatter")).split(path.sep).join("/");
|
|
385
390
|
await mkdir(pageDir, { recursive: true });
|
|
386
|
-
await writeFile(page, renderCatchAllPage(
|
|
391
|
+
await writeFile(page, renderCatchAllPage());
|
|
387
392
|
notes.push(`wrote ${path.join(appDir, CATCH_ALL_DIR, "page.tsx")}`);
|
|
388
393
|
}
|
|
389
394
|
|
|
390
395
|
const revalidateDir = path.join(root, appDir, "api", "backmatter", "revalidate");
|
|
391
396
|
const revalidateFile = path.join(revalidateDir, "route.ts");
|
|
392
397
|
if (!existsSync(revalidateFile)) {
|
|
393
|
-
const registry = path.relative(revalidateDir, path.join(root, ".backmatter")).split(path.sep).join("/");
|
|
394
398
|
await mkdir(revalidateDir, { recursive: true });
|
|
395
|
-
await writeFile(
|
|
396
|
-
revalidateFile,
|
|
397
|
-
renderRevalidateRoute(registry.startsWith(".") ? registry : `./${registry}`),
|
|
398
|
-
);
|
|
399
|
+
await writeFile(revalidateFile, renderRevalidateRoute());
|
|
399
400
|
notes.push(`wrote ${path.join(appDir, "api/backmatter/revalidate/route.ts")}`);
|
|
400
401
|
} else {
|
|
401
402
|
notes.push(`kept existing ${path.join(appDir, "api/backmatter/revalidate/route.ts")}`);
|
|
@@ -404,12 +405,8 @@ export async function runInit(args: CliArgs): Promise<string[]> {
|
|
|
404
405
|
const previewDir = path.join(root, appDir, "api", "backmatter", "preview");
|
|
405
406
|
const previewFile = path.join(previewDir, "route.ts");
|
|
406
407
|
if (!existsSync(previewFile)) {
|
|
407
|
-
const registry = path.relative(previewDir, path.join(root, ".backmatter")).split(path.sep).join("/");
|
|
408
408
|
await mkdir(previewDir, { recursive: true });
|
|
409
|
-
await writeFile(
|
|
410
|
-
previewFile,
|
|
411
|
-
renderPreviewRoute(registry.startsWith(".") ? registry : `./${registry}`),
|
|
412
|
-
);
|
|
409
|
+
await writeFile(previewFile, renderPreviewRoute());
|
|
413
410
|
notes.push(`wrote ${path.join(appDir, "api/backmatter/preview/route.ts")}`);
|
|
414
411
|
} else {
|
|
415
412
|
notes.push(`kept existing ${path.join(appDir, "api/backmatter/preview/route.ts")}`);
|
|
@@ -443,15 +440,17 @@ export async function runInit(args: CliArgs): Promise<string[]> {
|
|
|
443
440
|
notes.push("kept existing instrumentation.ts");
|
|
444
441
|
} else {
|
|
445
442
|
const dest = usesSrcApp ? srcInstrumentation : rootInstrumentation;
|
|
446
|
-
|
|
447
|
-
await writeFile(dest, renderInstrumentation(syncImport));
|
|
443
|
+
await writeFile(dest, renderInstrumentation());
|
|
448
444
|
notes.push(`wrote ${path.relative(root, dest)}`);
|
|
449
445
|
}
|
|
446
|
+
|
|
447
|
+
const tsconfigNote = ensureRegistryTsconfig(root);
|
|
448
|
+
if (tsconfigNote) notes.push(tsconfigNote);
|
|
450
449
|
} else {
|
|
451
450
|
notes.push("no @backmatter/next dependency: skipped the catch-all route");
|
|
452
451
|
}
|
|
453
452
|
|
|
454
|
-
for (const dir of ["models", "content", "blocks", "mdx", "layouts"]) {
|
|
453
|
+
for (const dir of ["models", "content", "blocks", "richtext", "mdx", "layouts"]) {
|
|
455
454
|
await mkdir(path.join(root, dir), { recursive: true });
|
|
456
455
|
}
|
|
457
456
|
|
|
@@ -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,
|
package/src/node/generate.ts
CHANGED
|
@@ -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(
|
|
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,12 +32,22 @@ export {
|
|
|
32
32
|
renderBlocksBarrel,
|
|
33
33
|
renderLayoutsBarrel,
|
|
34
34
|
renderMdxBarrel,
|
|
35
|
+
renderRichtextBarrel,
|
|
35
36
|
renderModelsBarrel,
|
|
36
37
|
renderSyncBarrel,
|
|
37
38
|
DEFAULT_OUT_DIR,
|
|
38
39
|
REGISTRY_FILES,
|
|
39
40
|
} from "./generate";
|
|
40
41
|
export type { GenerateOptions, GenerateResult, GeneratedFile } from "./generate";
|
|
42
|
+
export {
|
|
43
|
+
REGISTRY_SPECIFIER,
|
|
44
|
+
addRegistryPaths,
|
|
45
|
+
ensureRegistryTsconfig,
|
|
46
|
+
registryImportPaths,
|
|
47
|
+
inferTurbopackRoot,
|
|
48
|
+
registryTurbopackAlias,
|
|
49
|
+
registryWebpackAlias,
|
|
50
|
+
} from "./registry-import";
|
|
41
51
|
export {
|
|
42
52
|
contentRoots,
|
|
43
53
|
formatResults,
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// packages/core/src/node/registry-import.ts
|
|
2
|
+
// The generated client lives in `.backmatter/`. Apps import it as `backmatter`
|
|
3
|
+
// from any file. These helpers are the alias (`withBackmatter`) and the
|
|
4
|
+
// matching `tsconfig.json` `paths` (`init` and `withBackmatter`).
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { DEFAULT_OUT_DIR, REGISTRY_FILES } from "./generate";
|
|
8
|
+
|
|
9
|
+
export const REGISTRY_SPECIFIER = "backmatter";
|
|
10
|
+
|
|
11
|
+
export type RegistryAlias = {
|
|
12
|
+
/** Bare import, e.g. `backmatter` or `backmatter/mdx`. */
|
|
13
|
+
specifier: string;
|
|
14
|
+
/** File name inside the out dir, e.g. `index.ts`. */
|
|
15
|
+
file: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function registryAliases(): RegistryAlias[] {
|
|
19
|
+
return REGISTRY_FILES.map((file) => {
|
|
20
|
+
const stem = file.slice(0, -".ts".length);
|
|
21
|
+
return {
|
|
22
|
+
specifier: stem === "index" ? REGISTRY_SPECIFIER : `${REGISTRY_SPECIFIER}/${stem}`,
|
|
23
|
+
file,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const LOCK_FILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Highest workspace or lockfile at or above `appRoot`, inside the git repo.
|
|
32
|
+
* Next uses the same files to choose `turbopack.root`. In a single-package app
|
|
33
|
+
* that is the app root; in this monorepo it is the repository root.
|
|
34
|
+
*/
|
|
35
|
+
export function inferTurbopackRoot(appRoot: string): string {
|
|
36
|
+
const start = path.resolve(appRoot);
|
|
37
|
+
const gitRoot = findGitRoot(start);
|
|
38
|
+
let dir = start;
|
|
39
|
+
let root = start;
|
|
40
|
+
while (true) {
|
|
41
|
+
if (hasWorkspace(dir) || LOCK_FILES.some((name) => existsSync(path.join(dir, name)))) root = dir;
|
|
42
|
+
if (gitRoot && dir === gitRoot) break;
|
|
43
|
+
const parent = path.dirname(dir);
|
|
44
|
+
if (parent === dir) break;
|
|
45
|
+
dir = parent;
|
|
46
|
+
}
|
|
47
|
+
return root;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Turbopack `resolveAlias` values, relative to the Next app, with a `./` prefix.
|
|
52
|
+
* Next resolves these from the app directory even when `turbopack.root` is a
|
|
53
|
+
* parent workspace. A monorepo prefix (`./packages/docs/.backmatter/...`) makes
|
|
54
|
+
* the lookup `packages/docs/packages/docs/.backmatter`.
|
|
55
|
+
*/
|
|
56
|
+
export function registryTurbopackAlias(
|
|
57
|
+
appRoot: string,
|
|
58
|
+
outDir = DEFAULT_OUT_DIR,
|
|
59
|
+
): Record<string, string> {
|
|
60
|
+
const alias: Record<string, string> = {};
|
|
61
|
+
const base = path.resolve(appRoot);
|
|
62
|
+
for (const { specifier, file } of registryAliases()) {
|
|
63
|
+
const abs = path.resolve(appRoot, outDir, file);
|
|
64
|
+
alias[specifier] = asRelative(path.relative(base, abs).split(path.sep).join("/"));
|
|
65
|
+
}
|
|
66
|
+
return alias;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Webpack `resolve.alias`. Keys end in `$` so `backmatter` matches only that
|
|
71
|
+
* specifier, and `backmatter/mdx` stays its own entry. Values are absolute.
|
|
72
|
+
*/
|
|
73
|
+
export function registryWebpackAlias(root: string, outDir = DEFAULT_OUT_DIR): Record<string, string> {
|
|
74
|
+
const alias: Record<string, string> = {};
|
|
75
|
+
for (const { specifier, file } of registryAliases()) {
|
|
76
|
+
alias[`${specifier}$`] = path.resolve(root, outDir, file);
|
|
77
|
+
}
|
|
78
|
+
return alias;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `tsconfig` `paths` for the registry. Values are relative to `baseUrl` when
|
|
83
|
+
* set, otherwise to the tsconfig directory (the project root).
|
|
84
|
+
*/
|
|
85
|
+
export function registryImportPaths(outDir = DEFAULT_OUT_DIR, baseUrl?: string): Record<string, string[]> {
|
|
86
|
+
const base = !baseUrl || baseUrl === "." ? "." : baseUrl;
|
|
87
|
+
const from = path.resolve("/registry-root", base);
|
|
88
|
+
const to = path.resolve("/registry-root", outDir);
|
|
89
|
+
const folder = asRelative(path.relative(from, to).split(path.sep).join("/"));
|
|
90
|
+
const paths: Record<string, string[]> = {};
|
|
91
|
+
for (const { specifier, file } of registryAliases()) {
|
|
92
|
+
paths[specifier] = [`${folder}/${file}`];
|
|
93
|
+
}
|
|
94
|
+
return paths;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Insert missing `backmatter` paths. Returns the source unchanged when they are already present. */
|
|
98
|
+
export function addRegistryPaths(source: string, outDir = DEFAULT_OUT_DIR): string {
|
|
99
|
+
let parsed: unknown;
|
|
100
|
+
try {
|
|
101
|
+
parsed = looseParse(source);
|
|
102
|
+
} catch {
|
|
103
|
+
return source;
|
|
104
|
+
}
|
|
105
|
+
if (!isRecord(parsed)) return source;
|
|
106
|
+
const options = parsed.compilerOptions;
|
|
107
|
+
if (!isRecord(options)) return source;
|
|
108
|
+
const baseUrl = typeof options.baseUrl === "string" ? options.baseUrl : undefined;
|
|
109
|
+
const mappings = registryImportPaths(outDir, baseUrl);
|
|
110
|
+
const existing = isRecord(options.paths) ? options.paths : undefined;
|
|
111
|
+
if (options.paths !== undefined && !existing) return source;
|
|
112
|
+
const missing = Object.fromEntries(
|
|
113
|
+
Object.entries(mappings).filter(([key]) => !existing || existing[key] === undefined),
|
|
114
|
+
);
|
|
115
|
+
if (Object.keys(missing).length === 0) return source;
|
|
116
|
+
|
|
117
|
+
const compiler = findKeyBrace(source, "compilerOptions", 0, source.length);
|
|
118
|
+
if (!compiler) return source;
|
|
119
|
+
const compilerEnd = matchBrace(source, compiler.brace);
|
|
120
|
+
if (compilerEnd < 0) return source;
|
|
121
|
+
|
|
122
|
+
const nl = source.includes("\r\n") ? "\r\n" : "\n";
|
|
123
|
+
if (existing) {
|
|
124
|
+
const pathsBrace = findKeyBrace(source, "paths", compiler.brace + 1, compilerEnd);
|
|
125
|
+
if (!pathsBrace || pathsBrace.brace > compilerEnd) return source;
|
|
126
|
+
const indent = innerIndent(source, pathsBrace.brace);
|
|
127
|
+
const entries = formatEntries(missing, indent, nl);
|
|
128
|
+
return insertEntries(source, pathsBrace.brace, entries, indent, nl);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const indent = innerIndent(source, compiler.brace);
|
|
132
|
+
const child = `${indent} `;
|
|
133
|
+
const entries = formatEntries(missing, child, nl);
|
|
134
|
+
const block = `${indent}"paths": {${nl}${entries},${nl}${indent}}`;
|
|
135
|
+
return `${source.slice(0, compiler.brace + 1)}${nl}${block},${source.slice(compiler.brace + 1)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Write `paths` into tsconfig.json or jsconfig.json when the registry keys are missing. */
|
|
139
|
+
export function ensureRegistryTsconfig(root: string, outDir = DEFAULT_OUT_DIR): string | null {
|
|
140
|
+
const tsconfig = path.join(root, "tsconfig.json");
|
|
141
|
+
const jsconfig = path.join(root, "jsconfig.json");
|
|
142
|
+
const file = existsSync(tsconfig) ? tsconfig : existsSync(jsconfig) ? jsconfig : null;
|
|
143
|
+
if (!file) return null;
|
|
144
|
+
const before = readFileSync(file, "utf-8");
|
|
145
|
+
const after = addRegistryPaths(before, outDir);
|
|
146
|
+
if (after === before) return null;
|
|
147
|
+
writeFileSync(file, after);
|
|
148
|
+
return `updated ${path.basename(file)}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function hasWorkspace(dir: string): boolean {
|
|
152
|
+
const pkgPath = path.join(dir, "package.json");
|
|
153
|
+
if (!existsSync(pkgPath)) return false;
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).workspaces != null;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function findGitRoot(start: string): string | null {
|
|
162
|
+
let dir = start;
|
|
163
|
+
while (true) {
|
|
164
|
+
if (existsSync(path.join(dir, ".git"))) return dir;
|
|
165
|
+
const parent = path.dirname(dir);
|
|
166
|
+
if (parent === dir) return null;
|
|
167
|
+
dir = parent;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** `.backmatter/...` is relative, and still needs a `./` so bundlers do not treat it as a package. */
|
|
172
|
+
function asRelative(rel: string): string {
|
|
173
|
+
if (rel.startsWith("./") || rel.startsWith("../")) return rel;
|
|
174
|
+
return `./${rel}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function insertEntries(source: string, open: number, entries: string, indent: string, nl: string): string {
|
|
178
|
+
let rest = source.slice(open + 1);
|
|
179
|
+
if (!/^\r?\n/.test(rest)) rest = `${nl}${indent}${rest.trimStart()}`;
|
|
180
|
+
return `${source.slice(0, open + 1)}${nl}${entries},${rest}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function formatEntries(mappings: Record<string, string[]>, indent: string, nl: string): string {
|
|
184
|
+
return Object.entries(mappings)
|
|
185
|
+
.map(([key, value]) => `${indent}"${key}": [${value.map((entry) => JSON.stringify(entry)).join(", ")}]`)
|
|
186
|
+
.join(`,${nl}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function innerIndent(source: string, open: number): string {
|
|
190
|
+
const rest = source.slice(open + 1);
|
|
191
|
+
const nl = rest.search(/\r?\n/);
|
|
192
|
+
if (nl !== -1) {
|
|
193
|
+
const after = rest.slice(nl).replace(/^\r?\n/, "");
|
|
194
|
+
const indent = after.match(/^[ \t]*/)?.[0] ?? "";
|
|
195
|
+
const next = after[indent.length];
|
|
196
|
+
if (indent.length > 0 && next && next !== "}") return indent;
|
|
197
|
+
}
|
|
198
|
+
const lineStart = source.lastIndexOf("\n", open) + 1;
|
|
199
|
+
const base = source.slice(lineStart, open).match(/^[ \t]*/)?.[0] ?? "";
|
|
200
|
+
return `${base} `;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function looseParse(source: string): unknown {
|
|
204
|
+
const stripped = stripJsonComments(source).replace(/,\s*([}\]])/g, "$1");
|
|
205
|
+
return JSON.parse(stripped);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function stripJsonComments(input: string): string {
|
|
209
|
+
let out = "";
|
|
210
|
+
let i = 0;
|
|
211
|
+
let inString = false;
|
|
212
|
+
let escaped = false;
|
|
213
|
+
while (i < input.length) {
|
|
214
|
+
const c = input[i]!;
|
|
215
|
+
const next = input[i + 1];
|
|
216
|
+
if (inString) {
|
|
217
|
+
out += c;
|
|
218
|
+
if (escaped) escaped = false;
|
|
219
|
+
else if (c === "\\") escaped = true;
|
|
220
|
+
else if (c === '"') inString = false;
|
|
221
|
+
i++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (c === '"') {
|
|
225
|
+
inString = true;
|
|
226
|
+
out += c;
|
|
227
|
+
i++;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (c === "/" && next === "/") {
|
|
231
|
+
i += 2;
|
|
232
|
+
while (i < input.length && input[i] !== "\n") i++;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (c === "/" && next === "*") {
|
|
236
|
+
i += 2;
|
|
237
|
+
while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
|
|
238
|
+
i += 2;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
out += c;
|
|
242
|
+
i++;
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
248
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function findKeyBrace(source: string, key: string, start: number, end: number): { brace: number } | null {
|
|
252
|
+
const needle = `"${key}"`;
|
|
253
|
+
let i = start;
|
|
254
|
+
let inString = false;
|
|
255
|
+
let escaped = false;
|
|
256
|
+
while (i < end) {
|
|
257
|
+
const c = source[i]!;
|
|
258
|
+
const next = source[i + 1];
|
|
259
|
+
if (inString) {
|
|
260
|
+
if (escaped) escaped = false;
|
|
261
|
+
else if (c === "\\") escaped = true;
|
|
262
|
+
else if (c === '"') inString = false;
|
|
263
|
+
i++;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (c === '"') {
|
|
267
|
+
if (source.startsWith(needle, i)) {
|
|
268
|
+
let j = i + needle.length;
|
|
269
|
+
while (j < end && /\s/.test(source[j]!)) j++;
|
|
270
|
+
if (source[j] === ":") {
|
|
271
|
+
j++;
|
|
272
|
+
while (j < end && /\s/.test(source[j]!)) j++;
|
|
273
|
+
if (source[j] === "{") return { brace: j };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
inString = true;
|
|
277
|
+
i++;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (c === "/" && next === "/") {
|
|
281
|
+
i += 2;
|
|
282
|
+
while (i < end && source[i] !== "\n") i++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (c === "/" && next === "*") {
|
|
286
|
+
i += 2;
|
|
287
|
+
while (i < end && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
288
|
+
i += 2;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
i++;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function matchBrace(source: string, open: number): number {
|
|
297
|
+
let depth = 0;
|
|
298
|
+
let i = open;
|
|
299
|
+
let inString = false;
|
|
300
|
+
let escaped = false;
|
|
301
|
+
while (i < source.length) {
|
|
302
|
+
const c = source[i]!;
|
|
303
|
+
const next = source[i + 1];
|
|
304
|
+
if (inString) {
|
|
305
|
+
if (escaped) escaped = false;
|
|
306
|
+
else if (c === "\\") escaped = true;
|
|
307
|
+
else if (c === '"') inString = false;
|
|
308
|
+
i++;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (c === '"') {
|
|
312
|
+
inString = true;
|
|
313
|
+
i++;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (c === "/" && next === "/") {
|
|
317
|
+
i += 2;
|
|
318
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (c === "/" && next === "*") {
|
|
322
|
+
i += 2;
|
|
323
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
324
|
+
i += 2;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (c === "{") depth++;
|
|
328
|
+
else if (c === "}") {
|
|
329
|
+
depth--;
|
|
330
|
+
if (depth === 0) return i;
|
|
331
|
+
}
|
|
332
|
+
i++;
|
|
333
|
+
}
|
|
334
|
+
return -1;
|
|
335
|
+
}
|
package/src/sync-schema.ts
CHANGED
|
@@ -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,
|