@backmatter/next 0.0.20 → 0.0.22
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/dist/config.cjs +71 -6
- package/dist/loader.cjs +71 -5
- package/package.json +5 -2
- package/src/cms-proxy.ts +108 -0
- package/src/config.ts +7 -1
- package/src/devbar-client.tsx +29 -14
- package/src/layout-doc.ts +38 -0
- package/src/loader.ts +6 -2
- package/src/page-render.ts +7 -3
- package/src/preview-content.ts +49 -0
- package/src/preview-live.tsx +9 -0
- package/src/preview-refresh.tsx +220 -3
- package/src/preview-route.ts +51 -21
- package/src/render-preview.ts +186 -0
- package/src/revalidate.ts +2 -2
- package/src/routes.ts +207 -45
- package/src/site-channel.ts +37 -0
- package/src/site-doc-bridge.tsx +50 -0
- package/src/site-doc.ts +123 -0
- package/src/site-editor.tsx +341 -0
- package/src/source-data.ts +42 -0
package/dist/config.cjs
CHANGED
|
@@ -87,7 +87,8 @@ var DEFAULT_DIRS = {
|
|
|
87
87
|
richtext: "richtext",
|
|
88
88
|
mdx: "mdx",
|
|
89
89
|
layouts: "layouts",
|
|
90
|
-
objects: "objects"
|
|
90
|
+
objects: "objects",
|
|
91
|
+
sources: "sources"
|
|
91
92
|
};
|
|
92
93
|
|
|
93
94
|
// ../core/src/node/discover.ts
|
|
@@ -151,6 +152,9 @@ function listLayoutFiles(root, layoutsDir = "layouts") {
|
|
|
151
152
|
function listObjectFiles(root, objectsDir = "objects") {
|
|
152
153
|
return listModelFiles(root, objectsDir);
|
|
153
154
|
}
|
|
155
|
+
function listSourceDefFiles(root, sourcesDir = "sources") {
|
|
156
|
+
return listModelFiles(root, sourcesDir);
|
|
157
|
+
}
|
|
154
158
|
function listBlockFiles(root, blocksDir = "blocks") {
|
|
155
159
|
const { files, skipped } = listPascalCaseFiles(root, blocksDir);
|
|
156
160
|
return { blocks: files, skipped };
|
|
@@ -189,20 +193,32 @@ function renderCachedCmsClient() {
|
|
|
189
193
|
return [
|
|
190
194
|
"const raw = createBackmatter({ ...config, models, sync: false });",
|
|
191
195
|
"",
|
|
192
|
-
"async function
|
|
196
|
+
"async function cachedGetDoc(locale: string, type: string, slug: string) {",
|
|
193
197
|
' "use cache";',
|
|
194
198
|
' cacheLife("hours");',
|
|
195
199
|
" cacheTag(...contentCacheTags(locale, type, slug));",
|
|
196
200
|
" return raw.getDoc(locale, type, slug);",
|
|
197
201
|
"}",
|
|
198
202
|
"",
|
|
199
|
-
"async function
|
|
203
|
+
"async function cachedListDocs(filter?: { type?: string; locale?: string }) {",
|
|
200
204
|
' "use cache";',
|
|
201
205
|
' cacheLife("hours");',
|
|
202
206
|
" cacheTag(...listCacheTags(filter?.type));",
|
|
203
207
|
" return raw.listDocs(filter);",
|
|
204
208
|
"}",
|
|
205
209
|
"",
|
|
210
|
+
"async function getDoc(locale: string, type: string, slug: string) {",
|
|
211
|
+
" const preview = currentPreviewContent();",
|
|
212
|
+
" if (preview) return preview.getDoc(locale, type, slug);",
|
|
213
|
+
" return cachedGetDoc(locale, type, slug);",
|
|
214
|
+
"}",
|
|
215
|
+
"",
|
|
216
|
+
"async function listDocs(filter?: { type?: string; locale?: string }) {",
|
|
217
|
+
" const preview = currentPreviewContent();",
|
|
218
|
+
" if (preview) return preview.listDocs(filter);",
|
|
219
|
+
" return cachedListDocs(filter);",
|
|
220
|
+
"}",
|
|
221
|
+
"",
|
|
206
222
|
"/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */",
|
|
207
223
|
"export const cms = {",
|
|
208
224
|
" ...raw,",
|
|
@@ -217,6 +233,7 @@ function renderModelsBarrel(input) {
|
|
|
217
233
|
const lines = [GENERATED_HEADER, `import { createBackmatter } from "${input.sdk}";`];
|
|
218
234
|
if (nextHost) {
|
|
219
235
|
lines.push('import { contentCacheTags, createQuery, listCacheTags } from "@backmatter/core";');
|
|
236
|
+
lines.push('import { currentPreviewContent } from "@backmatter/next/preview-content";');
|
|
220
237
|
lines.push('import { cacheLife, cacheTag } from "next/cache";');
|
|
221
238
|
}
|
|
222
239
|
if (input.configFile) {
|
|
@@ -325,6 +342,46 @@ function renderBlockFallbacksBarrel(input) {
|
|
|
325
342
|
`)}
|
|
326
343
|
`;
|
|
327
344
|
}
|
|
345
|
+
function renderPreviewAction(sdk) {
|
|
346
|
+
if (sdk !== "@backmatter/next") {
|
|
347
|
+
return `${GENERATED_HEADER}
|
|
348
|
+
/** Instant preview needs @backmatter/next. This host has no server action. */
|
|
349
|
+
export const renderPreview = null;
|
|
350
|
+
`;
|
|
351
|
+
}
|
|
352
|
+
return `"use server";
|
|
353
|
+
${GENERATED_HEADER}
|
|
354
|
+
import { createRenderPreview } from "@backmatter/next/render-preview";
|
|
355
|
+
import { cms } from "./index";
|
|
356
|
+
import { blockLoaders } from "./blocks";
|
|
357
|
+
import { layouts } from "./layouts";
|
|
358
|
+
import { sources } from "./sources";
|
|
359
|
+
import { components as mdxComponents } from "./mdx";
|
|
360
|
+
import { richtextComponents } from "./richtext";
|
|
361
|
+
|
|
362
|
+
const components = { ...richtextComponents, ...mdxComponents };
|
|
363
|
+
|
|
364
|
+
export const renderPreview = createRenderPreview(cms, {
|
|
365
|
+
components,
|
|
366
|
+
blocks: blockLoaders,
|
|
367
|
+
layouts,
|
|
368
|
+
sources,
|
|
369
|
+
});
|
|
370
|
+
`;
|
|
371
|
+
}
|
|
372
|
+
function renderSourcesBarrel(input) {
|
|
373
|
+
const lines = [GENERATED_HEADER];
|
|
374
|
+
for (const source of input.sources) {
|
|
375
|
+
lines.push(`import ${source.importName} from "${importSpecifier(input.outDir, source.file)}";`);
|
|
376
|
+
}
|
|
377
|
+
lines.push("");
|
|
378
|
+
lines.push("/** External record loaders: sources/{type}.ts resolves that model's route data. */");
|
|
379
|
+
const entries = input.sources.map((s) => s.importName === s.stem ? s.stem : `${JSON.stringify(s.stem)}: ${s.importName}`);
|
|
380
|
+
lines.push(`export const sources = ${entries.length ? `{ ${entries.join(", ")} }` : "{}"};`);
|
|
381
|
+
return `${lines.join(`
|
|
382
|
+
`)}
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
328
385
|
function renderLayoutsBarrel(input) {
|
|
329
386
|
const lines = [GENERATED_HEADER];
|
|
330
387
|
for (const layout of input.layouts) {
|
|
@@ -348,7 +405,9 @@ var REGISTRY_FILES = [
|
|
|
348
405
|
"mdx.ts",
|
|
349
406
|
"richtext.ts",
|
|
350
407
|
"layouts.ts",
|
|
351
|
-
"
|
|
408
|
+
"sources.ts",
|
|
409
|
+
"sync.ts",
|
|
410
|
+
"preview-action.ts"
|
|
352
411
|
];
|
|
353
412
|
function generate(options) {
|
|
354
413
|
const root = import_node_path2.default.resolve(options.root);
|
|
@@ -360,6 +419,7 @@ function generate(options) {
|
|
|
360
419
|
const { blocks: mdx, skipped: skippedMdx } = listMdxFiles(root, options.mdxDir ?? DEFAULT_DIRS.mdx);
|
|
361
420
|
const layouts = listLayoutFiles(root, options.layoutsDir ?? DEFAULT_DIRS.layouts);
|
|
362
421
|
const objects = listObjectFiles(root, options.objectsDir ?? DEFAULT_DIRS.objects);
|
|
422
|
+
const sources = listSourceDefFiles(root, options.sourcesDir ?? DEFAULT_DIRS.sources);
|
|
363
423
|
const sdk = options.sdk ?? detectSdk(root);
|
|
364
424
|
const configFile = findConfigFile(root);
|
|
365
425
|
const write = options.write ?? true;
|
|
@@ -370,7 +430,9 @@ function generate(options) {
|
|
|
370
430
|
"mdx.ts": renderMdxBarrel({ outDir, mdx }),
|
|
371
431
|
"richtext.ts": renderRichtextBarrel({ outDir, richtext }),
|
|
372
432
|
"layouts.ts": renderLayoutsBarrel({ outDir, layouts }),
|
|
373
|
-
"
|
|
433
|
+
"sources.ts": renderSourcesBarrel({ outDir, sources }),
|
|
434
|
+
"sync.ts": renderSyncBarrel({ outDir, models, blocks, richtext, objects, configFile, sdk }),
|
|
435
|
+
"preview-action.ts": renderPreviewAction(sdk)
|
|
374
436
|
};
|
|
375
437
|
const outputs = REGISTRY_FILES.map((name) => {
|
|
376
438
|
const file = import_node_path2.default.join(outDir, name);
|
|
@@ -392,6 +454,7 @@ function generate(options) {
|
|
|
392
454
|
mdx,
|
|
393
455
|
layouts,
|
|
394
456
|
objects,
|
|
457
|
+
sources,
|
|
395
458
|
skippedBlocks: [...skippedBlocks, ...skippedRichtext],
|
|
396
459
|
skippedMdx,
|
|
397
460
|
configFile
|
|
@@ -710,6 +773,7 @@ function resolveLoaderOptions(options = {}) {
|
|
|
710
773
|
mdxDir: options.mdxDir ?? DEFAULT_DIRS.mdx,
|
|
711
774
|
layoutsDir: options.layoutsDir ?? DEFAULT_DIRS.layouts,
|
|
712
775
|
objectsDir: options.objectsDir ?? DEFAULT_DIRS.objects,
|
|
776
|
+
sourcesDir: options.sourcesDir ?? DEFAULT_DIRS.sources,
|
|
713
777
|
sdk: SDK
|
|
714
778
|
};
|
|
715
779
|
}
|
|
@@ -723,9 +787,10 @@ function generateRegistry(options = {}) {
|
|
|
723
787
|
function registryMatchers(loaderOptions) {
|
|
724
788
|
const rel = import_node_path4.default.relative(loaderOptions.root, import_node_path4.default.resolve(loaderOptions.root, loaderOptions.outDir)).split(import_node_path4.default.sep).join("/");
|
|
725
789
|
const escaped = rel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
790
|
+
const names = REGISTRY_FILES.map((file) => file.replace(/\.ts$/, "")).join("|");
|
|
726
791
|
return {
|
|
727
792
|
glob: `**/${rel}/*.ts`,
|
|
728
|
-
test: new RegExp(`(^|[\\\\/])${escaped.replace(/\//g, "[\\\\/]")}[\\\\/](
|
|
793
|
+
test: new RegExp(`(^|[\\\\/])${escaped.replace(/\//g, "[\\\\/]")}[\\\\/](${names})\\.ts$`)
|
|
729
794
|
};
|
|
730
795
|
}
|
|
731
796
|
function withBackmatter(nextConfig = {}, options = {}) {
|
package/dist/loader.cjs
CHANGED
|
@@ -81,7 +81,8 @@ var DEFAULT_DIRS = {
|
|
|
81
81
|
richtext: "richtext",
|
|
82
82
|
mdx: "mdx",
|
|
83
83
|
layouts: "layouts",
|
|
84
|
-
objects: "objects"
|
|
84
|
+
objects: "objects",
|
|
85
|
+
sources: "sources"
|
|
85
86
|
};
|
|
86
87
|
|
|
87
88
|
// ../core/src/node/discover.ts
|
|
@@ -145,6 +146,9 @@ function listLayoutFiles(root, layoutsDir = "layouts") {
|
|
|
145
146
|
function listObjectFiles(root, objectsDir = "objects") {
|
|
146
147
|
return listModelFiles(root, objectsDir);
|
|
147
148
|
}
|
|
149
|
+
function listSourceDefFiles(root, sourcesDir = "sources") {
|
|
150
|
+
return listModelFiles(root, sourcesDir);
|
|
151
|
+
}
|
|
148
152
|
function listBlockFiles(root, blocksDir = "blocks") {
|
|
149
153
|
const { files, skipped } = listPascalCaseFiles(root, blocksDir);
|
|
150
154
|
return { blocks: files, skipped };
|
|
@@ -183,20 +187,32 @@ function renderCachedCmsClient() {
|
|
|
183
187
|
return [
|
|
184
188
|
"const raw = createBackmatter({ ...config, models, sync: false });",
|
|
185
189
|
"",
|
|
186
|
-
"async function
|
|
190
|
+
"async function cachedGetDoc(locale: string, type: string, slug: string) {",
|
|
187
191
|
' "use cache";',
|
|
188
192
|
' cacheLife("hours");',
|
|
189
193
|
" cacheTag(...contentCacheTags(locale, type, slug));",
|
|
190
194
|
" return raw.getDoc(locale, type, slug);",
|
|
191
195
|
"}",
|
|
192
196
|
"",
|
|
193
|
-
"async function
|
|
197
|
+
"async function cachedListDocs(filter?: { type?: string; locale?: string }) {",
|
|
194
198
|
' "use cache";',
|
|
195
199
|
' cacheLife("hours");',
|
|
196
200
|
" cacheTag(...listCacheTags(filter?.type));",
|
|
197
201
|
" return raw.listDocs(filter);",
|
|
198
202
|
"}",
|
|
199
203
|
"",
|
|
204
|
+
"async function getDoc(locale: string, type: string, slug: string) {",
|
|
205
|
+
" const preview = currentPreviewContent();",
|
|
206
|
+
" if (preview) return preview.getDoc(locale, type, slug);",
|
|
207
|
+
" return cachedGetDoc(locale, type, slug);",
|
|
208
|
+
"}",
|
|
209
|
+
"",
|
|
210
|
+
"async function listDocs(filter?: { type?: string; locale?: string }) {",
|
|
211
|
+
" const preview = currentPreviewContent();",
|
|
212
|
+
" if (preview) return preview.listDocs(filter);",
|
|
213
|
+
" return cachedListDocs(filter);",
|
|
214
|
+
"}",
|
|
215
|
+
"",
|
|
200
216
|
"/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */",
|
|
201
217
|
"export const cms = {",
|
|
202
218
|
" ...raw,",
|
|
@@ -211,6 +227,7 @@ function renderModelsBarrel(input) {
|
|
|
211
227
|
const lines = [GENERATED_HEADER, `import { createBackmatter } from "${input.sdk}";`];
|
|
212
228
|
if (nextHost) {
|
|
213
229
|
lines.push('import { contentCacheTags, createQuery, listCacheTags } from "@backmatter/core";');
|
|
230
|
+
lines.push('import { currentPreviewContent } from "@backmatter/next/preview-content";');
|
|
214
231
|
lines.push('import { cacheLife, cacheTag } from "next/cache";');
|
|
215
232
|
}
|
|
216
233
|
if (input.configFile) {
|
|
@@ -319,6 +336,46 @@ function renderBlockFallbacksBarrel(input) {
|
|
|
319
336
|
`)}
|
|
320
337
|
`;
|
|
321
338
|
}
|
|
339
|
+
function renderPreviewAction(sdk) {
|
|
340
|
+
if (sdk !== "@backmatter/next") {
|
|
341
|
+
return `${GENERATED_HEADER}
|
|
342
|
+
/** Instant preview needs @backmatter/next. This host has no server action. */
|
|
343
|
+
export const renderPreview = null;
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
return `"use server";
|
|
347
|
+
${GENERATED_HEADER}
|
|
348
|
+
import { createRenderPreview } from "@backmatter/next/render-preview";
|
|
349
|
+
import { cms } from "./index";
|
|
350
|
+
import { blockLoaders } from "./blocks";
|
|
351
|
+
import { layouts } from "./layouts";
|
|
352
|
+
import { sources } from "./sources";
|
|
353
|
+
import { components as mdxComponents } from "./mdx";
|
|
354
|
+
import { richtextComponents } from "./richtext";
|
|
355
|
+
|
|
356
|
+
const components = { ...richtextComponents, ...mdxComponents };
|
|
357
|
+
|
|
358
|
+
export const renderPreview = createRenderPreview(cms, {
|
|
359
|
+
components,
|
|
360
|
+
blocks: blockLoaders,
|
|
361
|
+
layouts,
|
|
362
|
+
sources,
|
|
363
|
+
});
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
function renderSourcesBarrel(input) {
|
|
367
|
+
const lines = [GENERATED_HEADER];
|
|
368
|
+
for (const source of input.sources) {
|
|
369
|
+
lines.push(`import ${source.importName} from "${importSpecifier(input.outDir, source.file)}";`);
|
|
370
|
+
}
|
|
371
|
+
lines.push("");
|
|
372
|
+
lines.push("/** External record loaders: sources/{type}.ts resolves that model's route data. */");
|
|
373
|
+
const entries = input.sources.map((s) => s.importName === s.stem ? s.stem : `${JSON.stringify(s.stem)}: ${s.importName}`);
|
|
374
|
+
lines.push(`export const sources = ${entries.length ? `{ ${entries.join(", ")} }` : "{}"};`);
|
|
375
|
+
return `${lines.join(`
|
|
376
|
+
`)}
|
|
377
|
+
`;
|
|
378
|
+
}
|
|
322
379
|
function renderLayoutsBarrel(input) {
|
|
323
380
|
const lines = [GENERATED_HEADER];
|
|
324
381
|
for (const layout of input.layouts) {
|
|
@@ -342,7 +399,9 @@ var REGISTRY_FILES = [
|
|
|
342
399
|
"mdx.ts",
|
|
343
400
|
"richtext.ts",
|
|
344
401
|
"layouts.ts",
|
|
345
|
-
"
|
|
402
|
+
"sources.ts",
|
|
403
|
+
"sync.ts",
|
|
404
|
+
"preview-action.ts"
|
|
346
405
|
];
|
|
347
406
|
function generate(options) {
|
|
348
407
|
const root = import_node_path2.default.resolve(options.root);
|
|
@@ -354,6 +413,7 @@ function generate(options) {
|
|
|
354
413
|
const { blocks: mdx, skipped: skippedMdx } = listMdxFiles(root, options.mdxDir ?? DEFAULT_DIRS.mdx);
|
|
355
414
|
const layouts = listLayoutFiles(root, options.layoutsDir ?? DEFAULT_DIRS.layouts);
|
|
356
415
|
const objects = listObjectFiles(root, options.objectsDir ?? DEFAULT_DIRS.objects);
|
|
416
|
+
const sources = listSourceDefFiles(root, options.sourcesDir ?? DEFAULT_DIRS.sources);
|
|
357
417
|
const sdk = options.sdk ?? detectSdk(root);
|
|
358
418
|
const configFile = findConfigFile(root);
|
|
359
419
|
const write = options.write ?? true;
|
|
@@ -364,7 +424,9 @@ function generate(options) {
|
|
|
364
424
|
"mdx.ts": renderMdxBarrel({ outDir, mdx }),
|
|
365
425
|
"richtext.ts": renderRichtextBarrel({ outDir, richtext }),
|
|
366
426
|
"layouts.ts": renderLayoutsBarrel({ outDir, layouts }),
|
|
367
|
-
"
|
|
427
|
+
"sources.ts": renderSourcesBarrel({ outDir, sources }),
|
|
428
|
+
"sync.ts": renderSyncBarrel({ outDir, models, blocks, richtext, objects, configFile, sdk }),
|
|
429
|
+
"preview-action.ts": renderPreviewAction(sdk)
|
|
368
430
|
};
|
|
369
431
|
const outputs = REGISTRY_FILES.map((name) => {
|
|
370
432
|
const file = import_node_path2.default.join(outDir, name);
|
|
@@ -386,6 +448,7 @@ function generate(options) {
|
|
|
386
448
|
mdx,
|
|
387
449
|
layouts,
|
|
388
450
|
objects,
|
|
451
|
+
sources,
|
|
389
452
|
skippedBlocks: [...skippedBlocks, ...skippedRichtext],
|
|
390
453
|
skippedMdx,
|
|
391
454
|
configFile
|
|
@@ -402,12 +465,14 @@ function backmatterLoader(source) {
|
|
|
402
465
|
const mdxDir = import_node_path3.default.resolve(root, options.mdxDir);
|
|
403
466
|
const layoutsDir = import_node_path3.default.resolve(root, options.layoutsDir);
|
|
404
467
|
const objectsDir = import_node_path3.default.resolve(root, options.objectsDir);
|
|
468
|
+
const sourcesDir = import_node_path3.default.resolve(root, options.sourcesDir);
|
|
405
469
|
this.addContextDependency(modelsDir);
|
|
406
470
|
this.addContextDependency(blocksDir);
|
|
407
471
|
this.addContextDependency(richtextDir);
|
|
408
472
|
this.addContextDependency(mdxDir);
|
|
409
473
|
this.addContextDependency(layoutsDir);
|
|
410
474
|
this.addContextDependency(objectsDir);
|
|
475
|
+
this.addContextDependency(sourcesDir);
|
|
411
476
|
this.cacheable?.(true);
|
|
412
477
|
const result = generate({
|
|
413
478
|
root,
|
|
@@ -418,6 +483,7 @@ function backmatterLoader(source) {
|
|
|
418
483
|
mdxDir: options.mdxDir,
|
|
419
484
|
layoutsDir: options.layoutsDir,
|
|
420
485
|
objectsDir: options.objectsDir,
|
|
486
|
+
sourcesDir: options.sourcesDir,
|
|
421
487
|
sdk: options.sdk
|
|
422
488
|
});
|
|
423
489
|
if (result.configFile)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backmatter/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "Experimental Next.js adapter for Backmatter. APIs will break. Not ready for production.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
"default": "./dist/loader.cjs"
|
|
27
27
|
},
|
|
28
28
|
"./routes": "./src/routes.ts",
|
|
29
|
+
"./render-preview": "./src/render-preview.ts",
|
|
30
|
+
"./preview-content": "./src/preview-content.ts",
|
|
29
31
|
"./revalidate": "./src/revalidate.ts",
|
|
30
32
|
"./preview-route": "./src/preview-route.ts",
|
|
33
|
+
"./cms-proxy": "./src/cms-proxy.ts",
|
|
31
34
|
"./proxy": "./src/proxy.ts",
|
|
32
35
|
"./middleware": "./src/middleware.ts",
|
|
33
36
|
"./page": "./src/page-render.ts"
|
|
@@ -43,7 +46,7 @@
|
|
|
43
46
|
"test": "bun test"
|
|
44
47
|
},
|
|
45
48
|
"dependencies": {
|
|
46
|
-
"@backmatter/core": "^0.0.
|
|
49
|
+
"@backmatter/core": "^0.0.19",
|
|
47
50
|
"@mdx-js/mdx": "^3.1.1",
|
|
48
51
|
"remark-gfm": "^4.0.1"
|
|
49
52
|
},
|
package/src/cms-proxy.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// packages/next/src/cms-proxy.ts
|
|
2
|
+
// Same-origin proxy. The browser calls this with the preview session cookie.
|
|
3
|
+
// The host attaches that cookie as a bearer token and forwards an allow-list.
|
|
4
|
+
import { PREVIEW_SESSION_COOKIE, readPreviewSession } from "@backmatter/core";
|
|
5
|
+
import type { BackmatterClient } from "@backmatter/core/node/create-backmatter";
|
|
6
|
+
|
|
7
|
+
const DOC = "/workspaces/[^/]+/docs/[^/]+/[^/]+/[^/]+";
|
|
8
|
+
|
|
9
|
+
const ALLOWED: Array<{ method: string; pattern: RegExp }> = [
|
|
10
|
+
{ method: "GET", pattern: /^\/workspaces\/[^/]+\/docs$/ },
|
|
11
|
+
{ method: "GET", pattern: new RegExp(`^${DOC}$`) },
|
|
12
|
+
{ method: "PUT", pattern: new RegExp(`^${DOC}$`) },
|
|
13
|
+
{ method: "DELETE", pattern: new RegExp(`^${DOC}$`) },
|
|
14
|
+
{ method: "DELETE", pattern: new RegExp(`^${DOC}/tree$`) },
|
|
15
|
+
{ method: "POST", pattern: new RegExp(`^${DOC}/resolve$`) },
|
|
16
|
+
{ method: "GET", pattern: /^\/workspaces\/[^/]+\/git$/ },
|
|
17
|
+
{ method: "POST", pattern: /^\/workspaces\/[^/]+\/git\/commit$/ },
|
|
18
|
+
{ method: "POST", pattern: /^\/workspaces\/[^/]+\/preview-ticket$/ },
|
|
19
|
+
{ method: "GET", pattern: /^\/preview\/workspaces\/[^/]+\/docs\/[^/]+\/[^/]+\/[^/]+\/stream$/ },
|
|
20
|
+
{ method: "GET", pattern: /^\/preview\/workspaces\/[^/]+\/tree\/stream$/ },
|
|
21
|
+
{ method: "GET", pattern: /^\/workspaces\/[^/]+\/ai\/threads\/[^/]+$/ },
|
|
22
|
+
{ method: "POST", pattern: /^\/workspaces\/[^/]+\/ai\/threads$/ },
|
|
23
|
+
{ method: "POST", pattern: /^\/workspaces\/[^/]+\/ai\/threads\/[^/]+\/messages$/ },
|
|
24
|
+
{ method: "GET", pattern: /^\/preview\/session$/ },
|
|
25
|
+
{ method: "POST", pattern: /^\/preview\/session\/renew$/ },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function cmsProxyAllowed(method: string, path: string): boolean {
|
|
29
|
+
return ALLOWED.some((rule) => rule.method === method && rule.pattern.test(path));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Workspace id in an allow-listed path, when the path names one. */
|
|
33
|
+
export function workspaceIdFromProxyPath(path: string): string | null {
|
|
34
|
+
const match = path.match(/^\/(?:preview\/)?workspaces\/([^/]+)/);
|
|
35
|
+
return match?.[1] ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cookieValue(header: string | null, name: string): string | undefined {
|
|
39
|
+
if (!header) return undefined;
|
|
40
|
+
for (const part of header.split(";")) {
|
|
41
|
+
const [key, ...rest] = part.trim().split("=");
|
|
42
|
+
if (key === name) return rest.join("=");
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type CmsProxyDeps = {
|
|
48
|
+
fetch?: typeof fetch;
|
|
49
|
+
readSessionToken?: (req: Request) => Promise<string | undefined> | string | undefined;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function createCmsProxy(
|
|
53
|
+
cms: Pick<BackmatterClient, "config">,
|
|
54
|
+
deps: CmsProxyDeps = {},
|
|
55
|
+
) {
|
|
56
|
+
const forward = deps.fetch ?? fetch;
|
|
57
|
+
const readSessionToken =
|
|
58
|
+
deps.readSessionToken ??
|
|
59
|
+
((req: Request) => cookieValue(req.headers.get("cookie"), PREVIEW_SESSION_COOKIE));
|
|
60
|
+
|
|
61
|
+
const handle = async (req: Request): Promise<Response> => {
|
|
62
|
+
const secret = cms.config.previewToken;
|
|
63
|
+
const apiUrl = cms.config.apiUrl;
|
|
64
|
+
if (!secret || !apiUrl) return Response.json({ error: "preview_not_configured" }, { status: 503 });
|
|
65
|
+
|
|
66
|
+
const url = new URL(req.url);
|
|
67
|
+
const marker = "/api/backmatter/cms";
|
|
68
|
+
const at = url.pathname.indexOf(marker);
|
|
69
|
+
const path = at === -1 ? "" : url.pathname.slice(at + marker.length) || "/";
|
|
70
|
+
if (!cmsProxyAllowed(req.method, path)) {
|
|
71
|
+
return Response.json({ error: "not_found" }, { status: 404 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const token = await readSessionToken(req);
|
|
75
|
+
const session = await readPreviewSession(secret, token);
|
|
76
|
+
if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
|
|
77
|
+
const pathWorkspace = workspaceIdFromProxyPath(path);
|
|
78
|
+
if (pathWorkspace && pathWorkspace !== session.workspaceId) {
|
|
79
|
+
return Response.json({ error: "forbidden" }, { status: 403 });
|
|
80
|
+
}
|
|
81
|
+
if (cms.config.workspaceId && session.workspaceId !== cms.config.workspaceId) {
|
|
82
|
+
return Response.json({ error: "forbidden" }, { status: 403 });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const upstream = new URL(`${apiUrl.replace(/\/$/, "")}${path}`);
|
|
86
|
+
upstream.search = url.search;
|
|
87
|
+
const headers = new Headers();
|
|
88
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
89
|
+
const contentType = req.headers.get("content-type");
|
|
90
|
+
if (contentType) headers.set("Content-Type", contentType);
|
|
91
|
+
const accept = req.headers.get("accept");
|
|
92
|
+
if (accept) headers.set("Accept", accept);
|
|
93
|
+
|
|
94
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
95
|
+
const res = await forward(upstream, {
|
|
96
|
+
method: req.method,
|
|
97
|
+
headers,
|
|
98
|
+
body: hasBody ? await req.arrayBuffer() : undefined,
|
|
99
|
+
});
|
|
100
|
+
const responseHeaders = new Headers();
|
|
101
|
+
const type = res.headers.get("content-type");
|
|
102
|
+
if (type) responseHeaders.set("Content-Type", type);
|
|
103
|
+
responseHeaders.set("Cache-Control", "no-store");
|
|
104
|
+
return new Response(res.body, { status: res.status, headers: responseHeaders });
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return { GET: handle, POST: handle, PUT: handle, DELETE: handle };
|
|
108
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
DEFAULT_OUT_DIR,
|
|
15
15
|
detectSdk,
|
|
16
16
|
generate,
|
|
17
|
+
REGISTRY_FILES,
|
|
17
18
|
type GenerateResult,
|
|
18
19
|
} from "@backmatter/core/generate";
|
|
19
20
|
import {
|
|
@@ -33,6 +34,7 @@ export type WithBackmatterOptions = {
|
|
|
33
34
|
mdxDir?: string;
|
|
34
35
|
layoutsDir?: string;
|
|
35
36
|
objectsDir?: string;
|
|
37
|
+
sourcesDir?: string;
|
|
36
38
|
/** Called after the boot-time generation. Default: log what changed. */
|
|
37
39
|
onGenerate?: (result: GenerateResult) => void;
|
|
38
40
|
};
|
|
@@ -92,6 +94,7 @@ export function resolveLoaderOptions(options: WithBackmatterOptions = {}): Backm
|
|
|
92
94
|
mdxDir: options.mdxDir ?? DEFAULT_DIRS.mdx,
|
|
93
95
|
layoutsDir: options.layoutsDir ?? DEFAULT_DIRS.layouts,
|
|
94
96
|
objectsDir: options.objectsDir ?? DEFAULT_DIRS.objects,
|
|
97
|
+
sourcesDir: options.sourcesDir ?? DEFAULT_DIRS.sources,
|
|
95
98
|
sdk: SDK,
|
|
96
99
|
};
|
|
97
100
|
}
|
|
@@ -112,9 +115,12 @@ export function registryMatchers(loaderOptions: BackmatterLoaderOptions): { glob
|
|
|
112
115
|
.split(path.sep)
|
|
113
116
|
.join("/");
|
|
114
117
|
const escaped = rel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
118
|
+
const names = REGISTRY_FILES.map((file) => file.replace(/\.ts$/, "")).join("|");
|
|
115
119
|
return {
|
|
116
120
|
glob: `**/${rel}/*.ts`,
|
|
117
|
-
test: new RegExp(
|
|
121
|
+
test: new RegExp(
|
|
122
|
+
`(^|[\\\\/])${escaped.replace(/\//g, "[\\\\/]")}[\\\\/](${names})\\.ts$`,
|
|
123
|
+
),
|
|
118
124
|
};
|
|
119
125
|
}
|
|
120
126
|
|
package/src/devbar-client.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
|
|
5
|
+
import { SiteChrome } from "./site-editor";
|
|
5
6
|
import {
|
|
6
7
|
clearHostPreviewSession,
|
|
7
8
|
fetchCmsSession,
|
|
@@ -80,7 +81,7 @@ const barStyle: CSSProperties = {
|
|
|
80
81
|
alignItems: "center",
|
|
81
82
|
gap: 10,
|
|
82
83
|
minHeight: 32,
|
|
83
|
-
maxWidth: "min(
|
|
84
|
+
maxWidth: "min(520px, calc(100vw - 24px))",
|
|
84
85
|
padding: "0 12px",
|
|
85
86
|
paddingBottom: "env(safe-area-inset-bottom, 0px)",
|
|
86
87
|
background: "#f4f4f5",
|
|
@@ -187,10 +188,12 @@ function Divider() {
|
|
|
187
188
|
export function DevBarView({
|
|
188
189
|
state,
|
|
189
190
|
onExit,
|
|
191
|
+
onEdit,
|
|
190
192
|
exiting,
|
|
191
193
|
}: {
|
|
192
194
|
state: DevModeState;
|
|
193
195
|
onExit?: () => void;
|
|
196
|
+
onEdit?: () => void;
|
|
194
197
|
exiting?: boolean;
|
|
195
198
|
}) {
|
|
196
199
|
if (state.status !== "ready") return null;
|
|
@@ -240,6 +243,20 @@ export function DevBarView({
|
|
|
240
243
|
<Segment>draft</Segment>
|
|
241
244
|
</>
|
|
242
245
|
) : null}
|
|
246
|
+
{!live && onEdit ? (
|
|
247
|
+
<>
|
|
248
|
+
<Divider />
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
className="backmatter-devbar-exit"
|
|
252
|
+
aria-label="Edit this page"
|
|
253
|
+
onClick={() => onEdit()}
|
|
254
|
+
style={exitButtonStyle}
|
|
255
|
+
>
|
|
256
|
+
Edit
|
|
257
|
+
</button>
|
|
258
|
+
</>
|
|
259
|
+
) : null}
|
|
243
260
|
{!live ? (
|
|
244
261
|
<>
|
|
245
262
|
<Divider />
|
|
@@ -262,19 +279,17 @@ export function DevBarView({
|
|
|
262
279
|
export function DevBarClient({ apiUrl }: { apiUrl: string }) {
|
|
263
280
|
const state = useDevMode({ apiUrl });
|
|
264
281
|
const [exiting, setExiting] = useState(false);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
/>
|
|
277
|
-
);
|
|
282
|
+
const exit = () => {
|
|
283
|
+
if (exiting) return;
|
|
284
|
+
setExiting(true);
|
|
285
|
+
void exitHostPreview().then((ok) => {
|
|
286
|
+
if (!ok) setExiting(false);
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
if (state.status === "ready" && state.mode === "preview") {
|
|
290
|
+
return <SiteChrome branch={state.branch} tree={state.tree} exiting={exiting} onExit={exit} />;
|
|
291
|
+
}
|
|
292
|
+
return <DevBarView state={state} exiting={exiting} onExit={exit} />;
|
|
278
293
|
}
|
|
279
294
|
|
|
280
295
|
export async function exitHostPreview(
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// packages/next/src/layout-doc.ts
|
|
2
|
+
// Shared body render for the catch-all and `renderPreview`. No preview chrome.
|
|
3
|
+
// `data` is the external source record already resolved by the caller
|
|
4
|
+
// (see source-data.ts); this module only threads it to blocks and the layout.
|
|
5
|
+
import { createElement, type ReactNode } from "react";
|
|
6
|
+
import { modelFormat, type BackmatterDoc, type ModelDef, type SourceDef } from "@backmatter/core";
|
|
7
|
+
import { renderDoc, type MdxComponents } from "./mdx-compile";
|
|
8
|
+
import { renderPage, type BlockComponents, type BlockFallbacks } from "./page-render";
|
|
9
|
+
|
|
10
|
+
export type DocLayout = (doc: BackmatterDoc, body: ReactNode, data?: unknown) => ReactNode;
|
|
11
|
+
|
|
12
|
+
export type LayoutDocOptions = {
|
|
13
|
+
models: readonly ModelDef[];
|
|
14
|
+
components?: MdxComponents;
|
|
15
|
+
blocks?: BlockComponents;
|
|
16
|
+
fallbacks?: BlockFallbacks;
|
|
17
|
+
layouts?: Record<string, DocLayout>;
|
|
18
|
+
/** `sources/{type}.ts` loaders, usually the generated `sources`. Resolved by the caller. */
|
|
19
|
+
sources?: Record<string, SourceDef>;
|
|
20
|
+
render?: (doc: BackmatterDoc, body: ReactNode) => ReactNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function renderLayoutDoc(
|
|
24
|
+
doc: BackmatterDoc,
|
|
25
|
+
options: LayoutDocOptions,
|
|
26
|
+
data?: unknown,
|
|
27
|
+
): Promise<ReactNode> {
|
|
28
|
+
const model = options.models.find((item) => item.type === doc.type);
|
|
29
|
+
const isPage = Boolean(model && modelFormat(model) === "page");
|
|
30
|
+
const rendered = isPage
|
|
31
|
+
? await renderPage(doc, options.blocks, options.fallbacks, data)
|
|
32
|
+
: await renderDoc(doc, { components: options.components });
|
|
33
|
+
const layout = options.layouts?.[doc.type];
|
|
34
|
+
const laidOut = layout ? layout(doc, rendered, data) : rendered;
|
|
35
|
+
if (options.render) return options.render(doc, laidOut);
|
|
36
|
+
if (layout || isPage) return laidOut;
|
|
37
|
+
return createElement("article", null, laidOut);
|
|
38
|
+
}
|
package/src/loader.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// packages/next/src/loader.ts
|
|
2
2
|
// Bundler loader for `.backmatter/index.ts`, `.backmatter/blocks.ts`,
|
|
3
|
-
// `.backmatter/mdx.ts`, `.backmatter/richtext.ts`, and `.backmatter/
|
|
3
|
+
// `.backmatter/mdx.ts`, `.backmatter/richtext.ts`, `.backmatter/layouts.ts`, and `.backmatter/sources.ts`.
|
|
4
4
|
//
|
|
5
5
|
// This is the Tailwind-JIT mechanism: instead of running our own watcher we
|
|
6
6
|
// tell the bundler that these modules depend on the `models/`, `blocks/`,
|
|
7
|
-
// `richtext/`, `mdx/`, and `
|
|
7
|
+
// `richtext/`, `mdx/`, `layouts/`, and `sources/` directories. webpack and Turbopack already watch the filesystem;
|
|
8
8
|
// when a file is added, removed or renamed there they invalidate the module,
|
|
9
9
|
// re-run this loader, and we regenerate the barrel from a fresh `readdir`.
|
|
10
10
|
import path from "node:path";
|
|
@@ -19,6 +19,7 @@ export type BackmatterLoaderOptions = {
|
|
|
19
19
|
mdxDir: string;
|
|
20
20
|
layoutsDir: string;
|
|
21
21
|
objectsDir: string;
|
|
22
|
+
sourcesDir: string;
|
|
22
23
|
sdk: string;
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -40,6 +41,7 @@ export default function backmatterLoader(this: BackmatterLoaderContext, source:
|
|
|
40
41
|
const mdxDir = path.resolve(root, options.mdxDir);
|
|
41
42
|
const layoutsDir = path.resolve(root, options.layoutsDir);
|
|
42
43
|
const objectsDir = path.resolve(root, options.objectsDir);
|
|
44
|
+
const sourcesDir = path.resolve(root, options.sourcesDir);
|
|
43
45
|
|
|
44
46
|
this.addContextDependency(modelsDir);
|
|
45
47
|
this.addContextDependency(blocksDir);
|
|
@@ -47,6 +49,7 @@ export default function backmatterLoader(this: BackmatterLoaderContext, source:
|
|
|
47
49
|
this.addContextDependency(mdxDir);
|
|
48
50
|
this.addContextDependency(layoutsDir);
|
|
49
51
|
this.addContextDependency(objectsDir);
|
|
52
|
+
this.addContextDependency(sourcesDir);
|
|
50
53
|
this.cacheable?.(true);
|
|
51
54
|
|
|
52
55
|
const result = generate({
|
|
@@ -58,6 +61,7 @@ export default function backmatterLoader(this: BackmatterLoaderContext, source:
|
|
|
58
61
|
mdxDir: options.mdxDir,
|
|
59
62
|
layoutsDir: options.layoutsDir,
|
|
60
63
|
objectsDir: options.objectsDir,
|
|
64
|
+
sourcesDir: options.sourcesDir,
|
|
61
65
|
sdk: options.sdk,
|
|
62
66
|
});
|
|
63
67
|
if (result.configFile) this.addDependency(result.configFile);
|