@backmatter/next 0.0.6
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 +405 -0
- package/dist/loader.cjs +349 -0
- package/package.json +60 -0
- package/src/cached-doc.ts +55 -0
- package/src/config.ts +160 -0
- package/src/create-backmatter.ts +15 -0
- package/src/devbar.tsx +280 -0
- package/src/index.ts +9 -0
- package/src/loader.ts +59 -0
- package/src/mdx-client.tsx +29 -0
- package/src/mdx-compile.ts +37 -0
- package/src/mdx.ts +13 -0
- package/src/middleware.ts +4 -0
- package/src/page-render.ts +111 -0
- package/src/preview-live.tsx +108 -0
- package/src/preview-refresh.tsx +17 -0
- package/src/preview-route.ts +138 -0
- package/src/preview-ui.ts +15 -0
- package/src/preview.ts +4 -0
- package/src/proxy.ts +24 -0
- package/src/revalidate.ts +64 -0
- package/src/routes.ts +226 -0
package/dist/loader.cjs
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
function __accessProp(key) {
|
|
8
|
+
return this[key];
|
|
9
|
+
}
|
|
10
|
+
var __toESMCache_node;
|
|
11
|
+
var __toESMCache_esm;
|
|
12
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
+
var canCache = mod != null && typeof mod === "object";
|
|
14
|
+
if (canCache) {
|
|
15
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
+
var cached = cache.get(mod);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
+
if (mod && typeof mod === "object" || typeof mod === "function") {
|
|
23
|
+
for (let key of __getOwnPropNames(mod))
|
|
24
|
+
if (!__hasOwnProp.call(to, key))
|
|
25
|
+
__defProp(to, key, {
|
|
26
|
+
get: __accessProp.bind(mod, key),
|
|
27
|
+
enumerable: true
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (canCache)
|
|
31
|
+
cache.set(mod, to);
|
|
32
|
+
return to;
|
|
33
|
+
};
|
|
34
|
+
var __toCommonJS = (from) => {
|
|
35
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
36
|
+
if (entry)
|
|
37
|
+
return entry;
|
|
38
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
39
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
40
|
+
for (var key of __getOwnPropNames(from))
|
|
41
|
+
if (!__hasOwnProp.call(entry, key))
|
|
42
|
+
__defProp(entry, key, {
|
|
43
|
+
get: __accessProp.bind(from, key),
|
|
44
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
__moduleCache.set(from, entry);
|
|
48
|
+
return entry;
|
|
49
|
+
};
|
|
50
|
+
var __moduleCache;
|
|
51
|
+
var __returnValue = (v) => v;
|
|
52
|
+
function __exportSetter(name, newValue) {
|
|
53
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
54
|
+
}
|
|
55
|
+
var __export = (target, all) => {
|
|
56
|
+
for (var name in all)
|
|
57
|
+
__defProp(target, name, {
|
|
58
|
+
get: all[name],
|
|
59
|
+
enumerable: true,
|
|
60
|
+
configurable: true,
|
|
61
|
+
set: __exportSetter.bind(all, name)
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// src/loader.ts
|
|
66
|
+
var exports_loader = {};
|
|
67
|
+
__export(exports_loader, {
|
|
68
|
+
default: () => backmatterLoader
|
|
69
|
+
});
|
|
70
|
+
module.exports = __toCommonJS(exports_loader);
|
|
71
|
+
var import_node_path3 = __toESM(require("node:path"));
|
|
72
|
+
|
|
73
|
+
// ../core/src/node/generate.ts
|
|
74
|
+
var import_node_fs2 = require("node:fs");
|
|
75
|
+
var import_node_path2 = __toESM(require("node:path"));
|
|
76
|
+
|
|
77
|
+
// ../core/src/config.ts
|
|
78
|
+
var DEFAULT_DIRS = {
|
|
79
|
+
models: "models",
|
|
80
|
+
blocks: "blocks",
|
|
81
|
+
mdx: "mdx",
|
|
82
|
+
layouts: "layouts"
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ../core/src/node/discover.ts
|
|
86
|
+
var import_node_fs = require("node:fs");
|
|
87
|
+
var import_node_path = __toESM(require("node:path"));
|
|
88
|
+
var SOURCE_EXT = /\.(ts|tsx|js|jsx|mjs)$/;
|
|
89
|
+
var IGNORED = /(\.test\.|\.spec\.|\.d\.ts$|\.stories\.)/;
|
|
90
|
+
function toIdentifier(stem) {
|
|
91
|
+
const camel = stem.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part, i) => i === 0 ? part : part[0].toUpperCase() + part.slice(1)).join("");
|
|
92
|
+
const safe = camel.replace(/^[^A-Za-z_$]/, (c) => `_${c}`);
|
|
93
|
+
return safe || "_";
|
|
94
|
+
}
|
|
95
|
+
function listSourceFiles(dir) {
|
|
96
|
+
if (!import_node_fs.existsSync(dir) || !import_node_fs.statSync(dir).isDirectory())
|
|
97
|
+
return [];
|
|
98
|
+
return import_node_fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && SOURCE_EXT.test(e.name) && !IGNORED.test(e.name)).filter((e) => !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => import_node_path.default.join(dir, e.name)).sort();
|
|
99
|
+
}
|
|
100
|
+
function listModelFiles(root, modelsDir = "models") {
|
|
101
|
+
const files = listSourceFiles(import_node_path.default.resolve(root, modelsDir));
|
|
102
|
+
const seen = new Map;
|
|
103
|
+
return files.map((file) => {
|
|
104
|
+
const stem = import_node_path.default.basename(file).replace(SOURCE_EXT, "");
|
|
105
|
+
const importName = toIdentifier(stem);
|
|
106
|
+
const clash = seen.get(importName);
|
|
107
|
+
if (clash) {
|
|
108
|
+
throw new Error(`models/${import_node_path.default.basename(clash)} and models/${import_node_path.default.basename(file)} both map to "${importName}"`);
|
|
109
|
+
}
|
|
110
|
+
seen.set(importName, file);
|
|
111
|
+
return { file, stem, importName };
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
var JSX_TAG = /^[A-Z][A-Za-z0-9]*$/;
|
|
115
|
+
var FALLBACK_STEM = /^([A-Z][A-Za-z0-9]*)\.fallback$/;
|
|
116
|
+
function listPascalCaseFiles(root, dir) {
|
|
117
|
+
const files = [];
|
|
118
|
+
const skipped = [];
|
|
119
|
+
for (const file of listSourceFiles(import_node_path.default.resolve(root, dir))) {
|
|
120
|
+
const name = import_node_path.default.basename(file).replace(SOURCE_EXT, "");
|
|
121
|
+
if (FALLBACK_STEM.test(name))
|
|
122
|
+
continue;
|
|
123
|
+
if (JSX_TAG.test(name))
|
|
124
|
+
files.push({ file, name });
|
|
125
|
+
else
|
|
126
|
+
skipped.push(import_node_path.default.relative(root, file));
|
|
127
|
+
}
|
|
128
|
+
return { files, skipped };
|
|
129
|
+
}
|
|
130
|
+
function listBlockFallbackFiles(root, blocksDir = "blocks") {
|
|
131
|
+
const files = [];
|
|
132
|
+
for (const file of listSourceFiles(import_node_path.default.resolve(root, blocksDir))) {
|
|
133
|
+
const stem = import_node_path.default.basename(file).replace(SOURCE_EXT, "");
|
|
134
|
+
const match = FALLBACK_STEM.exec(stem);
|
|
135
|
+
if (match)
|
|
136
|
+
files.push({ file, name: match[1] });
|
|
137
|
+
}
|
|
138
|
+
return files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
139
|
+
}
|
|
140
|
+
function listLayoutFiles(root, layoutsDir = "layouts") {
|
|
141
|
+
return listModelFiles(root, layoutsDir);
|
|
142
|
+
}
|
|
143
|
+
function listBlockFiles(root, blocksDir = "blocks") {
|
|
144
|
+
const { files, skipped } = listPascalCaseFiles(root, blocksDir);
|
|
145
|
+
return { blocks: files, skipped };
|
|
146
|
+
}
|
|
147
|
+
function listMdxFiles(root, mdxDir = "mdx") {
|
|
148
|
+
const { files, skipped } = listPascalCaseFiles(root, mdxDir);
|
|
149
|
+
return { blocks: files, skipped };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ../core/src/node/generate.ts
|
|
153
|
+
var GENERATED_HEADER = "// Generated by backmatter — do not edit. Re-run `backmatter generate`.";
|
|
154
|
+
var DEFAULT_OUT_DIR = ".backmatter";
|
|
155
|
+
var CONFIG_FILES = ["backmatter.config.ts", "backmatter.config.mts", "backmatter.config.js", "backmatter.config.mjs"];
|
|
156
|
+
function importSpecifier(fromDir, file) {
|
|
157
|
+
const rel = import_node_path2.default.relative(fromDir, file).split(import_node_path2.default.sep).join("/").replace(/\.(ts|tsx|mts|js|jsx|mjs)$/, "");
|
|
158
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
159
|
+
}
|
|
160
|
+
function findConfigFile(root) {
|
|
161
|
+
for (const name of CONFIG_FILES) {
|
|
162
|
+
const full = import_node_path2.default.join(root, name);
|
|
163
|
+
if (import_node_fs2.existsSync(full))
|
|
164
|
+
return full;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function detectSdk(root) {
|
|
169
|
+
try {
|
|
170
|
+
const pkg = JSON.parse(import_node_fs2.readFileSync(import_node_path2.default.join(root, "package.json"), "utf-8"));
|
|
171
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
172
|
+
if (deps["@backmatter/next"])
|
|
173
|
+
return "@backmatter/next";
|
|
174
|
+
} catch {}
|
|
175
|
+
return "@backmatter/core/node";
|
|
176
|
+
}
|
|
177
|
+
function renderModelsBarrel(input) {
|
|
178
|
+
const lines = [GENERATED_HEADER, `import { createBackmatter } from "${input.sdk}";`];
|
|
179
|
+
if (input.configFile) {
|
|
180
|
+
lines.push(`import config from "${importSpecifier(input.outDir, input.configFile)}";`);
|
|
181
|
+
} else {
|
|
182
|
+
lines.push("const config = {};");
|
|
183
|
+
}
|
|
184
|
+
for (const m of input.models) {
|
|
185
|
+
lines.push(`import ${m.importName} from "${importSpecifier(input.outDir, m.file)}";`);
|
|
186
|
+
}
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push(`export const models = [${input.models.map((m) => m.importName).join(", ")}] as const;`);
|
|
189
|
+
lines.push("export type Models = typeof models;");
|
|
190
|
+
lines.push("");
|
|
191
|
+
lines.push("/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */");
|
|
192
|
+
lines.push("export const cms = createBackmatter({ ...config, models });");
|
|
193
|
+
return `${lines.join(`
|
|
194
|
+
`)}
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
function renderSyncBarrel(input) {
|
|
198
|
+
const lines = [GENERATED_HEADER, `import { createBackmatter } from "${input.sdk}";`];
|
|
199
|
+
if (input.configFile) {
|
|
200
|
+
lines.push(`import config from "${importSpecifier(input.outDir, input.configFile)}";`);
|
|
201
|
+
} else {
|
|
202
|
+
lines.push("const config = {};");
|
|
203
|
+
}
|
|
204
|
+
for (const m of input.models) {
|
|
205
|
+
lines.push(`import ${m.importName} from "${importSpecifier(input.outDir, m.file)}";`);
|
|
206
|
+
}
|
|
207
|
+
for (const b of input.blocks) {
|
|
208
|
+
lines.push(`import ${b.name} from "${importSpecifier(input.outDir, b.file)}";`);
|
|
209
|
+
}
|
|
210
|
+
lines.push("");
|
|
211
|
+
lines.push(`export const models = [${input.models.map((m) => m.importName).join(", ")}] as const;`);
|
|
212
|
+
lines.push("export type Models = typeof models;");
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push(`export const blocks = [${input.blocks.map((b) => b.name).join(", ")}] as const;`);
|
|
215
|
+
lines.push("");
|
|
216
|
+
lines.push("/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */");
|
|
217
|
+
lines.push("export const cms = createBackmatter({ ...config, models, blocks });");
|
|
218
|
+
return `${lines.join(`
|
|
219
|
+
`)}
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
function renderMdxBarrel(input) {
|
|
223
|
+
const lines = [GENERATED_HEADER];
|
|
224
|
+
for (const b of input.mdx) {
|
|
225
|
+
lines.push(`import ${b.name} from "${importSpecifier(input.outDir, b.file)}";`);
|
|
226
|
+
}
|
|
227
|
+
lines.push("");
|
|
228
|
+
lines.push("/** MDX component map: <Hero /> in content resolves to mdx/Hero.tsx. */");
|
|
229
|
+
const names = input.mdx.map((b) => b.name).join(", ");
|
|
230
|
+
lines.push(`export const components = ${names ? `{ ${names} }` : "{}"};`);
|
|
231
|
+
return `${lines.join(`
|
|
232
|
+
`)}
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
function renderBlocksBarrel(input) {
|
|
236
|
+
const lines = [GENERATED_HEADER, ""];
|
|
237
|
+
lines.push('/** Composer loaders: JSON `{ type: "Hero" }` → dynamic import of blocks/Hero.tsx. */');
|
|
238
|
+
const entries = [...input.blocks].sort((a, b) => a.name.localeCompare(b.name)).map((b) => `${b.name}: Object.assign(() => import("${importSpecifier(input.outDir, b.file)}"), { $$backmatterLoader: true })`);
|
|
239
|
+
lines.push(`export const blockLoaders = ${entries.length ? `{ ${entries.join(", ")} }` : "{}"};`);
|
|
240
|
+
return `${lines.join(`
|
|
241
|
+
`)}
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
function renderBlockFallbacksBarrel(input) {
|
|
245
|
+
const lines = [GENERATED_HEADER];
|
|
246
|
+
for (const b of input.fallbacks) {
|
|
247
|
+
lines.push(`import ${b.name} from "${importSpecifier(input.outDir, b.file)}";`);
|
|
248
|
+
}
|
|
249
|
+
lines.push("");
|
|
250
|
+
const names = input.fallbacks.map((b) => b.name).join(", ");
|
|
251
|
+
lines.push(`export const blockFallbacks = ${names ? `{ ${names} }` : "{}"};`);
|
|
252
|
+
return `${lines.join(`
|
|
253
|
+
`)}
|
|
254
|
+
`;
|
|
255
|
+
}
|
|
256
|
+
function renderLayoutsBarrel(input) {
|
|
257
|
+
const lines = [GENERATED_HEADER];
|
|
258
|
+
for (const layout of input.layouts) {
|
|
259
|
+
lines.push(`import ${layout.importName} from "${importSpecifier(input.outDir, layout.file)}";`);
|
|
260
|
+
}
|
|
261
|
+
lines.push("");
|
|
262
|
+
lines.push("/** Per-type wrappers: layouts/{type}.tsx wraps that model's catch-all body. */");
|
|
263
|
+
const entries = input.layouts.map((l) => l.importName === l.stem ? l.stem : `${JSON.stringify(l.stem)}: ${l.importName}`);
|
|
264
|
+
lines.push(`export const layouts = ${entries.length ? `{ ${entries.join(", ")} }` : "{}"};`);
|
|
265
|
+
return `${lines.join(`
|
|
266
|
+
`)}
|
|
267
|
+
`;
|
|
268
|
+
}
|
|
269
|
+
function differsOnDisk(file, content) {
|
|
270
|
+
return !import_node_fs2.existsSync(file) || import_node_fs2.readFileSync(file, "utf-8") !== content;
|
|
271
|
+
}
|
|
272
|
+
var REGISTRY_FILES = [
|
|
273
|
+
"index.ts",
|
|
274
|
+
"blocks.ts",
|
|
275
|
+
"block-fallbacks.ts",
|
|
276
|
+
"mdx.ts",
|
|
277
|
+
"layouts.ts",
|
|
278
|
+
"sync.ts"
|
|
279
|
+
];
|
|
280
|
+
function generate(options) {
|
|
281
|
+
const root = import_node_path2.default.resolve(options.root);
|
|
282
|
+
const outDir = import_node_path2.default.resolve(root, options.outDir ?? DEFAULT_OUT_DIR);
|
|
283
|
+
const models = listModelFiles(root, options.modelsDir ?? DEFAULT_DIRS.models);
|
|
284
|
+
const { blocks, skipped: skippedBlocks } = listBlockFiles(root, options.blocksDir ?? DEFAULT_DIRS.blocks);
|
|
285
|
+
const fallbacks = listBlockFallbackFiles(root, options.blocksDir ?? DEFAULT_DIRS.blocks);
|
|
286
|
+
const { blocks: mdx, skipped: skippedMdx } = listMdxFiles(root, options.mdxDir ?? DEFAULT_DIRS.mdx);
|
|
287
|
+
const layouts = listLayoutFiles(root, options.layoutsDir ?? DEFAULT_DIRS.layouts);
|
|
288
|
+
const sdk = options.sdk ?? detectSdk(root);
|
|
289
|
+
const configFile = findConfigFile(root);
|
|
290
|
+
const write = options.write ?? true;
|
|
291
|
+
const rendered = {
|
|
292
|
+
"index.ts": renderModelsBarrel({ outDir, models, configFile, sdk }),
|
|
293
|
+
"blocks.ts": renderBlocksBarrel({ outDir, blocks }),
|
|
294
|
+
"block-fallbacks.ts": renderBlockFallbacksBarrel({ outDir, fallbacks }),
|
|
295
|
+
"mdx.ts": renderMdxBarrel({ outDir, mdx }),
|
|
296
|
+
"layouts.ts": renderLayoutsBarrel({ outDir, layouts }),
|
|
297
|
+
"sync.ts": renderSyncBarrel({ outDir, models, blocks, configFile, sdk })
|
|
298
|
+
};
|
|
299
|
+
const outputs = REGISTRY_FILES.map((name) => {
|
|
300
|
+
const file = import_node_path2.default.join(outDir, name);
|
|
301
|
+
const content = rendered[name];
|
|
302
|
+
const changed = differsOnDisk(file, content);
|
|
303
|
+
if (changed && write) {
|
|
304
|
+
import_node_fs2.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
|
|
305
|
+
import_node_fs2.writeFileSync(file, content);
|
|
306
|
+
}
|
|
307
|
+
return { file, rel: import_node_path2.default.relative(root, file).split(import_node_path2.default.sep).join("/"), content, changed };
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
outputs,
|
|
311
|
+
files: outputs.map((o) => o.rel),
|
|
312
|
+
changed: outputs.filter((o) => o.changed).map((o) => o.rel),
|
|
313
|
+
models,
|
|
314
|
+
blocks,
|
|
315
|
+
mdx,
|
|
316
|
+
layouts,
|
|
317
|
+
skippedBlocks,
|
|
318
|
+
skippedMdx,
|
|
319
|
+
configFile
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/loader.ts
|
|
324
|
+
function backmatterLoader(source) {
|
|
325
|
+
const options = this.getOptions();
|
|
326
|
+
const root = import_node_path3.default.resolve(options.root);
|
|
327
|
+
const modelsDir = import_node_path3.default.resolve(root, options.modelsDir);
|
|
328
|
+
const blocksDir = import_node_path3.default.resolve(root, options.blocksDir);
|
|
329
|
+
const mdxDir = import_node_path3.default.resolve(root, options.mdxDir);
|
|
330
|
+
const layoutsDir = import_node_path3.default.resolve(root, options.layoutsDir);
|
|
331
|
+
this.addContextDependency(modelsDir);
|
|
332
|
+
this.addContextDependency(blocksDir);
|
|
333
|
+
this.addContextDependency(mdxDir);
|
|
334
|
+
this.addContextDependency(layoutsDir);
|
|
335
|
+
this.cacheable?.(true);
|
|
336
|
+
const result = generate({
|
|
337
|
+
root,
|
|
338
|
+
outDir: options.outDir,
|
|
339
|
+
modelsDir: options.modelsDir,
|
|
340
|
+
blocksDir: options.blocksDir,
|
|
341
|
+
mdxDir: options.mdxDir,
|
|
342
|
+
layoutsDir: options.layoutsDir,
|
|
343
|
+
sdk: options.sdk
|
|
344
|
+
});
|
|
345
|
+
if (result.configFile)
|
|
346
|
+
this.addDependency(result.configFile);
|
|
347
|
+
const output = result.outputs.find((o) => import_node_path3.default.resolve(o.file) === import_node_path3.default.resolve(this.resourcePath));
|
|
348
|
+
return output ? output.content : source;
|
|
349
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@backmatter/next",
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "Experimental Next.js adapter for Backmatter. APIs will break. Not ready for production.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.ts",
|
|
11
|
+
"./preview": "./src/preview.ts",
|
|
12
|
+
"./devbar": "./src/devbar.tsx",
|
|
13
|
+
"./mdx": "./src/mdx.ts",
|
|
14
|
+
"./mdx/client": "./src/mdx-client.tsx",
|
|
15
|
+
"./config": {
|
|
16
|
+
"types": "./src/config.ts",
|
|
17
|
+
"default": "./dist/config.cjs"
|
|
18
|
+
},
|
|
19
|
+
"./loader": {
|
|
20
|
+
"types": "./src/loader.ts",
|
|
21
|
+
"default": "./dist/loader.cjs"
|
|
22
|
+
},
|
|
23
|
+
"./routes": "./src/routes.ts",
|
|
24
|
+
"./revalidate": "./src/revalidate.ts",
|
|
25
|
+
"./preview-route": "./src/preview-route.ts",
|
|
26
|
+
"./proxy": "./src/proxy.ts",
|
|
27
|
+
"./middleware": "./src/middleware.ts",
|
|
28
|
+
"./page": "./src/page-render.ts"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"dist",
|
|
33
|
+
"!src/**/*.test.ts",
|
|
34
|
+
"!src/**/*.test.tsx"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "bun build src/config.ts src/loader.ts --outdir dist --target node --format cjs --entry-naming \"[name].cjs\"",
|
|
38
|
+
"test": "bun test"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@backmatter/core": "^0.0.6",
|
|
42
|
+
"@mdx-js/mdx": "^3.1.1",
|
|
43
|
+
"remark-gfm": "^4.0.1"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"next": ">=16.3.0",
|
|
47
|
+
"react": ">=19",
|
|
48
|
+
"react-dom": ">=19"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"next": { "optional": true }
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/bun": "^1.3.14",
|
|
55
|
+
"@types/react": "^19.3.0",
|
|
56
|
+
"@types/react-dom": "^19.3.0",
|
|
57
|
+
"react": "^19.3.0",
|
|
58
|
+
"react-dom": "^19.3.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// packages/next/src/cached-doc.ts
|
|
2
|
+
import { contentCacheTags, type BackmatterDoc } from "@backmatter/core";
|
|
3
|
+
|
|
4
|
+
export type CacheFns = {
|
|
5
|
+
cacheLife: (profile: string) => void;
|
|
6
|
+
cacheTag: (...tags: string[]) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type GetDocFn = (
|
|
10
|
+
locale: string,
|
|
11
|
+
type: string,
|
|
12
|
+
slug: string,
|
|
13
|
+
) => Promise<BackmatterDoc | null>;
|
|
14
|
+
|
|
15
|
+
let registeredGetDoc: GetDocFn | null = null;
|
|
16
|
+
|
|
17
|
+
/** Called from `createCatchAll` so `"use cache"` only closes over serializable args. */
|
|
18
|
+
export function registerCachedDocGet(getDoc: GetDocFn): void {
|
|
19
|
+
registeredGetDoc = getDoc;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function loadCacheFns(): Promise<CacheFns> {
|
|
23
|
+
try {
|
|
24
|
+
const mod = (await import("next/cache" as string)) as CacheFns;
|
|
25
|
+
return {
|
|
26
|
+
cacheLife: (profile) => {
|
|
27
|
+
if (!process.env.__NEXT_USE_CACHE) return;
|
|
28
|
+
mod.cacheLife(profile);
|
|
29
|
+
},
|
|
30
|
+
cacheTag: (...tags) => {
|
|
31
|
+
if (!process.env.__NEXT_USE_CACHE) return;
|
|
32
|
+
mod.cacheTag(...tags);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
return { cacheLife: () => {}, cacheTag: () => {} };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function getCachedDoc(
|
|
41
|
+
locale: string,
|
|
42
|
+
type: string,
|
|
43
|
+
slug: string,
|
|
44
|
+
cache?: CacheFns,
|
|
45
|
+
): Promise<BackmatterDoc | null> {
|
|
46
|
+
"use cache";
|
|
47
|
+
const fns = cache ?? (await loadCacheFns());
|
|
48
|
+
fns.cacheLife("hours");
|
|
49
|
+
fns.cacheTag(...contentCacheTags(locale, type, slug));
|
|
50
|
+
const getDoc = registeredGetDoc;
|
|
51
|
+
if (!getDoc) {
|
|
52
|
+
throw new Error("[backmatter] getCachedDoc called before createCatchAll registered getDoc");
|
|
53
|
+
}
|
|
54
|
+
return getDoc(locale, type, slug);
|
|
55
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// packages/next/src/config.ts
|
|
2
|
+
// `withBackmatter(nextConfig)` for next.config.{js,mjs,ts}.
|
|
3
|
+
//
|
|
4
|
+
// 1. Generates `.backmatter/` once when the config loads, so the first compile can
|
|
5
|
+
// resolve `import { cms } from ".backmatter"`.
|
|
6
|
+
// 2. Registers `@backmatter/next/loader` for the registry files with Turbopack
|
|
7
|
+
// (Next 16 default) and webpack (`next build --webpack`). The loader declares
|
|
8
|
+
// `models/`, `blocks/`, and `mdx/` as context dependencies, so the bundler's
|
|
9
|
+
// own file watcher keeps the registry fresh — no polling, no second watcher process.
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { DEFAULT_DIRS } from "@backmatter/core/generate";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_OUT_DIR,
|
|
15
|
+
detectSdk,
|
|
16
|
+
generate,
|
|
17
|
+
type GenerateResult,
|
|
18
|
+
} from "@backmatter/core/generate";
|
|
19
|
+
import type { BackmatterLoaderOptions } from "./loader";
|
|
20
|
+
|
|
21
|
+
export type WithBackmatterOptions = {
|
|
22
|
+
/** Customer repo root. Default: process.cwd(). */
|
|
23
|
+
root?: string;
|
|
24
|
+
outDir?: string;
|
|
25
|
+
modelsDir?: string;
|
|
26
|
+
blocksDir?: string;
|
|
27
|
+
mdxDir?: string;
|
|
28
|
+
layoutsDir?: string;
|
|
29
|
+
/** Called after the boot-time generation. Default: log what changed. */
|
|
30
|
+
onGenerate?: (result: GenerateResult) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const LOADER = "@backmatter/next/loader";
|
|
34
|
+
const SDK = "@backmatter/next";
|
|
35
|
+
|
|
36
|
+
type TurbopackRule = { loaders: Array<{ loader: string; options: Record<string, unknown> }> };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The slice of NextConfig we touch; kept local so `next` stays optional.
|
|
40
|
+
* Deliberately loose so the real `NextConfig` (whose `TurbopackOptions` has no
|
|
41
|
+
* index signature and whose `webpack` context is a concrete type) is assignable.
|
|
42
|
+
*/
|
|
43
|
+
export type NextConfigLike = {
|
|
44
|
+
cacheComponents?: boolean;
|
|
45
|
+
partialPrefetching?: boolean | "unstable_eager";
|
|
46
|
+
turbopack?: Record<string, unknown>;
|
|
47
|
+
webpack?: ((config: WebpackConfigLike, context: any) => WebpackConfigLike) | null;
|
|
48
|
+
} & Record<string, unknown>;
|
|
49
|
+
|
|
50
|
+
export type WebpackConfigLike = {
|
|
51
|
+
module?: { rules?: unknown[] } & Record<string, unknown>;
|
|
52
|
+
} & Record<string, unknown>;
|
|
53
|
+
|
|
54
|
+
export function warnMissingInstrumentation(root: string): void {
|
|
55
|
+
if (detectSdk(root) !== "@backmatter/next") return;
|
|
56
|
+
const hasRoot = existsSync(path.join(root, "instrumentation.ts"));
|
|
57
|
+
const hasSrc = existsSync(path.join(root, "src", "instrumentation.ts"));
|
|
58
|
+
if (!hasRoot && !hasSrc) {
|
|
59
|
+
console.warn(
|
|
60
|
+
"backmatter: add instrumentation.ts that imports .backmatter/sync — block schema sync will not run without it",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function defaultLog(result: GenerateResult, root: string): void {
|
|
66
|
+
for (const file of result.changed) console.log(`backmatter: wrote ${file}`);
|
|
67
|
+
for (const skipped of result.skippedBlocks) {
|
|
68
|
+
console.warn(`backmatter: skipped ${skipped} (block file names must be PascalCase)`);
|
|
69
|
+
}
|
|
70
|
+
for (const skipped of result.skippedMdx) {
|
|
71
|
+
console.warn(`backmatter: skipped ${skipped} (MDX file names must be PascalCase)`);
|
|
72
|
+
}
|
|
73
|
+
warnMissingInstrumentation(root);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveLoaderOptions(options: WithBackmatterOptions = {}): BackmatterLoaderOptions {
|
|
77
|
+
return {
|
|
78
|
+
root: path.resolve(options.root ?? process.cwd()),
|
|
79
|
+
outDir: options.outDir ?? DEFAULT_OUT_DIR,
|
|
80
|
+
modelsDir: options.modelsDir ?? DEFAULT_DIRS.models,
|
|
81
|
+
blocksDir: options.blocksDir ?? DEFAULT_DIRS.blocks,
|
|
82
|
+
mdxDir: options.mdxDir ?? DEFAULT_DIRS.mdx,
|
|
83
|
+
layoutsDir: options.layoutsDir ?? DEFAULT_DIRS.layouts,
|
|
84
|
+
sdk: SDK,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Generate once. Safe to call repeatedly; only changed files are rewritten. */
|
|
89
|
+
export function generateRegistry(options: WithBackmatterOptions = {}): GenerateResult {
|
|
90
|
+
const loaderOptions = resolveLoaderOptions(options);
|
|
91
|
+
const result = generate(loaderOptions);
|
|
92
|
+
const root = loaderOptions.root;
|
|
93
|
+
(options.onGenerate ?? ((r) => defaultLog(r, root)))(result);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Glob (Turbopack) and RegExp (webpack) matching the registry files. */
|
|
98
|
+
export function registryMatchers(loaderOptions: BackmatterLoaderOptions): { glob: string; test: RegExp } {
|
|
99
|
+
const rel = path
|
|
100
|
+
.relative(loaderOptions.root, path.resolve(loaderOptions.root, loaderOptions.outDir))
|
|
101
|
+
.split(path.sep)
|
|
102
|
+
.join("/");
|
|
103
|
+
const escaped = rel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
104
|
+
return {
|
|
105
|
+
glob: `**/${rel}/*.ts`,
|
|
106
|
+
test: new RegExp(`(^|[\\\\/])${escaped.replace(/\//g, "[\\\\/]")}[\\\\/](index|blocks|block-fallbacks|mdx|layouts|sync)\\.ts$`),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Wrap your Next config:
|
|
112
|
+
*
|
|
113
|
+
* ```ts
|
|
114
|
+
* // next.config.ts
|
|
115
|
+
* import { withBackmatter } from "@backmatter/next/config";
|
|
116
|
+
* export default withBackmatter({ reactStrictMode: true });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export type WithBackmatterResult<T extends object> = Omit<T, "turbopack" | "webpack"> & {
|
|
120
|
+
cacheComponents: boolean;
|
|
121
|
+
partialPrefetching: boolean | "unstable_eager";
|
|
122
|
+
turbopack: Record<string, unknown>;
|
|
123
|
+
webpack: NonNullable<NextConfigLike["webpack"]>;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export function withBackmatter<T extends object>(
|
|
127
|
+
nextConfig: T = {} as T,
|
|
128
|
+
options: WithBackmatterOptions = {},
|
|
129
|
+
): WithBackmatterResult<T> {
|
|
130
|
+
const loaderOptions = resolveLoaderOptions(options);
|
|
131
|
+
generateRegistry(options);
|
|
132
|
+
const { glob, test } = registryMatchers(loaderOptions);
|
|
133
|
+
const use = { loader: LOADER, options: loaderOptions as unknown as Record<string, unknown> };
|
|
134
|
+
|
|
135
|
+
const cfg = nextConfig as T & NextConfigLike;
|
|
136
|
+
const existingTurbo = (cfg.turbopack ?? {}) as Record<string, unknown>;
|
|
137
|
+
const existingRules = (existingTurbo.rules ?? {}) as Record<string, unknown>;
|
|
138
|
+
const turbopackRule: TurbopackRule = { loaders: [use] };
|
|
139
|
+
const turbopack = {
|
|
140
|
+
...existingTurbo,
|
|
141
|
+
rules: { ...existingRules, [glob]: turbopackRule },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const webpack: NonNullable<NextConfigLike["webpack"]> = (config, context) => {
|
|
145
|
+
config.module ??= {};
|
|
146
|
+
config.module.rules ??= [];
|
|
147
|
+
// `enforce: "pre"` runs before Next's SWC loader; we return TypeScript, SWC compiles it.
|
|
148
|
+
config.module.rules.push({ test, enforce: "pre", use: [use] });
|
|
149
|
+
return cfg.webpack ? cfg.webpack(config, context) : config;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const cacheComponents = cfg.cacheComponents ?? true;
|
|
153
|
+
return {
|
|
154
|
+
...nextConfig,
|
|
155
|
+
cacheComponents,
|
|
156
|
+
partialPrefetching: cfg.partialPrefetching ?? cacheComponents,
|
|
157
|
+
turbopack,
|
|
158
|
+
webpack,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// packages/next/src/create-backmatter.ts
|
|
2
|
+
// Next hosts cache API fetches with tags so `content.committed` can
|
|
3
|
+
// `revalidateTag("backmatter")`. Core/CLI keep `cache: "no-store"`.
|
|
4
|
+
import {
|
|
5
|
+
createBackmatter as createCore,
|
|
6
|
+
type CreateBackmatterOptions,
|
|
7
|
+
type BackmatterClient,
|
|
8
|
+
} from "@backmatter/core/node/create-backmatter";
|
|
9
|
+
import type { ModelDef } from "@backmatter/core";
|
|
10
|
+
|
|
11
|
+
export function createBackmatter<const Models extends readonly ModelDef[]>(
|
|
12
|
+
options: CreateBackmatterOptions<Models>,
|
|
13
|
+
): BackmatterClient<Models> {
|
|
14
|
+
return createCore({ ...options, taggedApiFetch: true });
|
|
15
|
+
}
|