@backmatter/next 0.0.12 → 0.0.13
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 +42 -2
- package/dist/loader.cjs +38 -2
- package/package.json +2 -2
- package/src/devbar-client.tsx +20 -11
- package/src/devbar.tsx +1 -0
- package/src/preview-route.ts +16 -5
- package/src/routes.ts +15 -3
package/dist/config.cjs
CHANGED
|
@@ -180,8 +180,40 @@ function detectSdk(root) {
|
|
|
180
180
|
} catch {}
|
|
181
181
|
return "@backmatter/core/node";
|
|
182
182
|
}
|
|
183
|
+
function renderCachedCmsClient() {
|
|
184
|
+
return [
|
|
185
|
+
"const raw = createBackmatter({ ...config, models });",
|
|
186
|
+
"",
|
|
187
|
+
"async function getDoc(locale: string, type: string, slug: string) {",
|
|
188
|
+
' "use cache";',
|
|
189
|
+
' cacheLife("hours");',
|
|
190
|
+
" cacheTag(...contentCacheTags(locale, type, slug));",
|
|
191
|
+
" return raw.getDoc(locale, type, slug);",
|
|
192
|
+
"}",
|
|
193
|
+
"",
|
|
194
|
+
"async function listDocs(filter?: { type?: string; locale?: string }) {",
|
|
195
|
+
' "use cache";',
|
|
196
|
+
' cacheLife("hours");',
|
|
197
|
+
" cacheTag(...listCacheTags(filter?.type));",
|
|
198
|
+
" return raw.listDocs(filter);",
|
|
199
|
+
"}",
|
|
200
|
+
"",
|
|
201
|
+
"/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */",
|
|
202
|
+
"export const cms = {",
|
|
203
|
+
" ...raw,",
|
|
204
|
+
" getDoc,",
|
|
205
|
+
" listDocs,",
|
|
206
|
+
` query: createQuery(models, { getDoc, listDocs }),`,
|
|
207
|
+
"};"
|
|
208
|
+
];
|
|
209
|
+
}
|
|
183
210
|
function renderModelsBarrel(input) {
|
|
211
|
+
const nextHost = input.sdk === "@backmatter/next";
|
|
184
212
|
const lines = [GENERATED_HEADER, `import { createBackmatter } from "${input.sdk}";`];
|
|
213
|
+
if (nextHost) {
|
|
214
|
+
lines.push('import { contentCacheTags, createQuery, listCacheTags } from "@backmatter/core";');
|
|
215
|
+
lines.push('import { cacheLife, cacheTag } from "next/cache";');
|
|
216
|
+
}
|
|
185
217
|
if (input.configFile) {
|
|
186
218
|
lines.push(`import config from "${importSpecifier(input.outDir, input.configFile)}";`);
|
|
187
219
|
} else {
|
|
@@ -194,8 +226,12 @@ function renderModelsBarrel(input) {
|
|
|
194
226
|
lines.push(`export const models = [${input.models.map((m) => m.importName).join(", ")}] as const;`);
|
|
195
227
|
lines.push("export type Models = typeof models;");
|
|
196
228
|
lines.push("");
|
|
197
|
-
|
|
198
|
-
|
|
229
|
+
if (nextHost) {
|
|
230
|
+
lines.push(...renderCachedCmsClient());
|
|
231
|
+
} else {
|
|
232
|
+
lines.push("/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */");
|
|
233
|
+
lines.push("export const cms = createBackmatter({ ...config, models });");
|
|
234
|
+
}
|
|
199
235
|
return `${lines.join(`
|
|
200
236
|
`)}
|
|
201
237
|
`;
|
|
@@ -395,10 +431,14 @@ function withBackmatter(nextConfig = {}, options = {}) {
|
|
|
395
431
|
return cfg.webpack ? cfg.webpack(config, context) : config;
|
|
396
432
|
};
|
|
397
433
|
const cacheComponents = cfg.cacheComponents ?? true;
|
|
434
|
+
const transpilePackages = [
|
|
435
|
+
...new Set([...cfg.transpilePackages ?? [], "@backmatter/next", "@backmatter/core"])
|
|
436
|
+
];
|
|
398
437
|
return {
|
|
399
438
|
...nextConfig,
|
|
400
439
|
cacheComponents,
|
|
401
440
|
partialPrefetching: cfg.partialPrefetching ?? cacheComponents,
|
|
441
|
+
transpilePackages,
|
|
402
442
|
turbopack,
|
|
403
443
|
webpack
|
|
404
444
|
};
|
package/dist/loader.cjs
CHANGED
|
@@ -174,8 +174,40 @@ function detectSdk(root) {
|
|
|
174
174
|
} catch {}
|
|
175
175
|
return "@backmatter/core/node";
|
|
176
176
|
}
|
|
177
|
+
function renderCachedCmsClient() {
|
|
178
|
+
return [
|
|
179
|
+
"const raw = createBackmatter({ ...config, models });",
|
|
180
|
+
"",
|
|
181
|
+
"async function getDoc(locale: string, type: string, slug: string) {",
|
|
182
|
+
' "use cache";',
|
|
183
|
+
' cacheLife("hours");',
|
|
184
|
+
" cacheTag(...contentCacheTags(locale, type, slug));",
|
|
185
|
+
" return raw.getDoc(locale, type, slug);",
|
|
186
|
+
"}",
|
|
187
|
+
"",
|
|
188
|
+
"async function listDocs(filter?: { type?: string; locale?: string }) {",
|
|
189
|
+
' "use cache";',
|
|
190
|
+
' cacheLife("hours");',
|
|
191
|
+
" cacheTag(...listCacheTags(filter?.type));",
|
|
192
|
+
" return raw.listDocs(filter);",
|
|
193
|
+
"}",
|
|
194
|
+
"",
|
|
195
|
+
"/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */",
|
|
196
|
+
"export const cms = {",
|
|
197
|
+
" ...raw,",
|
|
198
|
+
" getDoc,",
|
|
199
|
+
" listDocs,",
|
|
200
|
+
` query: createQuery(models, { getDoc, listDocs }),`,
|
|
201
|
+
"};"
|
|
202
|
+
];
|
|
203
|
+
}
|
|
177
204
|
function renderModelsBarrel(input) {
|
|
205
|
+
const nextHost = input.sdk === "@backmatter/next";
|
|
178
206
|
const lines = [GENERATED_HEADER, `import { createBackmatter } from "${input.sdk}";`];
|
|
207
|
+
if (nextHost) {
|
|
208
|
+
lines.push('import { contentCacheTags, createQuery, listCacheTags } from "@backmatter/core";');
|
|
209
|
+
lines.push('import { cacheLife, cacheTag } from "next/cache";');
|
|
210
|
+
}
|
|
179
211
|
if (input.configFile) {
|
|
180
212
|
lines.push(`import config from "${importSpecifier(input.outDir, input.configFile)}";`);
|
|
181
213
|
} else {
|
|
@@ -188,8 +220,12 @@ function renderModelsBarrel(input) {
|
|
|
188
220
|
lines.push(`export const models = [${input.models.map((m) => m.importName).join(", ")}] as const;`);
|
|
189
221
|
lines.push("export type Models = typeof models;");
|
|
190
222
|
lines.push("");
|
|
191
|
-
|
|
192
|
-
|
|
223
|
+
if (nextHost) {
|
|
224
|
+
lines.push(...renderCachedCmsClient());
|
|
225
|
+
} else {
|
|
226
|
+
lines.push("/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */");
|
|
227
|
+
lines.push("export const cms = createBackmatter({ ...config, models });");
|
|
228
|
+
}
|
|
193
229
|
return `${lines.join(`
|
|
194
230
|
`)}
|
|
195
231
|
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backmatter/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "Experimental Next.js adapter for Backmatter. APIs will break. Not ready for production.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"test": "bun test"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@backmatter/core": "^0.0.
|
|
41
|
+
"@backmatter/core": "^0.0.12",
|
|
42
42
|
"@mdx-js/mdx": "^3.1.1",
|
|
43
43
|
"remark-gfm": "^4.0.1"
|
|
44
44
|
},
|
package/src/devbar-client.tsx
CHANGED
|
@@ -16,7 +16,7 @@ export type DevModeState =
|
|
|
16
16
|
| { status: "hidden" }
|
|
17
17
|
| {
|
|
18
18
|
status: "ready";
|
|
19
|
-
user
|
|
19
|
+
user?: CmsSessionUser;
|
|
20
20
|
mode: "preview" | "live";
|
|
21
21
|
branch?: string;
|
|
22
22
|
tree?: string;
|
|
@@ -26,28 +26,37 @@ export function devModeState(
|
|
|
26
26
|
user: CmsSessionUser | null,
|
|
27
27
|
session: { enabled: boolean; branch?: string; tree?: string },
|
|
28
28
|
): DevModeState {
|
|
29
|
-
if (!user) return { status: "hidden" };
|
|
29
|
+
if (!user && !session.enabled) return { status: "hidden" };
|
|
30
30
|
const resolved = resolveDevMode(session);
|
|
31
31
|
return {
|
|
32
32
|
status: "ready",
|
|
33
|
-
user,
|
|
33
|
+
...(user ? { user } : {}),
|
|
34
34
|
...resolved,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export async function loadDevModeState(
|
|
39
|
+
apiUrl: string,
|
|
40
|
+
fetchUser: typeof fetchCmsSession = fetchCmsSession,
|
|
41
|
+
fetchSession: typeof fetchHostPreviewSession = fetchHostPreviewSession,
|
|
42
|
+
): Promise<DevModeState> {
|
|
43
|
+
const trimmed = apiUrl.trim();
|
|
44
|
+
const [user, session] = await Promise.all([
|
|
45
|
+
trimmed ? fetchUser(trimmed) : Promise.resolve(null),
|
|
46
|
+
fetchSession(""),
|
|
47
|
+
]);
|
|
48
|
+
return devModeState(user, session);
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
export function useDevMode(options: { apiUrl: string }): DevModeState {
|
|
39
52
|
const [state, setState] = useState<DevModeState>({ status: "hidden" });
|
|
40
53
|
|
|
41
54
|
useEffect(() => {
|
|
42
|
-
const apiUrl = options.apiUrl.trim();
|
|
43
|
-
if (!apiUrl) return;
|
|
44
55
|
let cancelled = false;
|
|
45
|
-
void
|
|
46
|
-
(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
},
|
|
50
|
-
);
|
|
56
|
+
void loadDevModeState(options.apiUrl).then((next) => {
|
|
57
|
+
if (cancelled) return;
|
|
58
|
+
setState(next);
|
|
59
|
+
});
|
|
51
60
|
return () => {
|
|
52
61
|
cancelled = true;
|
|
53
62
|
};
|
package/src/devbar.tsx
CHANGED
package/src/preview-route.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { BackmatterClient } from "@backmatter/core/node/create-backmatter";
|
|
|
15
15
|
export type PreviewRouteDeps = {
|
|
16
16
|
enableDraft?: () => void | Promise<void>;
|
|
17
17
|
disableDraft?: () => void | Promise<void>;
|
|
18
|
+
isDraftEnabled?: () => boolean | Promise<boolean>;
|
|
18
19
|
now?: () => number;
|
|
19
20
|
};
|
|
20
21
|
|
|
@@ -45,6 +46,13 @@ async function nextDraft(enable: boolean): Promise<void> {
|
|
|
45
46
|
else draft.disable();
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
async function nextDraftEnabled(): Promise<boolean> {
|
|
50
|
+
const mod = (await import("next/headers" as string)) as {
|
|
51
|
+
draftMode: () => Promise<{ isEnabled: boolean }>;
|
|
52
|
+
};
|
|
53
|
+
return (await mod.draftMode()).isEnabled;
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
function cookieFrom(req: Request): string | undefined {
|
|
49
57
|
const header = req.headers.get("cookie");
|
|
50
58
|
if (!header) return undefined;
|
|
@@ -75,11 +83,14 @@ export function createPreviewRoute(
|
|
|
75
83
|
|
|
76
84
|
if (url.searchParams.get("session") === "1") {
|
|
77
85
|
const checkout = secret ? await checkoutFrom(req, secret, url) : null;
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
if (checkout) {
|
|
87
|
+
return Response.json({
|
|
88
|
+
mode: "preview",
|
|
89
|
+
...checkout,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const draftOn = await (deps.isDraftEnabled ?? nextDraftEnabled)();
|
|
93
|
+
return Response.json({ mode: draftOn ? "preview" : "live" });
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
if (url.searchParams.get("stream") === "1") {
|
package/src/routes.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type CatchAllOptions = {
|
|
|
44
44
|
preview?: {
|
|
45
45
|
enabled: boolean;
|
|
46
46
|
checkout?: { branch?: string; tree?: string };
|
|
47
|
+
error?: string;
|
|
47
48
|
getDoc?: (ref: {
|
|
48
49
|
locale: string;
|
|
49
50
|
type: string;
|
|
@@ -87,6 +88,13 @@ type PreviewState =
|
|
|
87
88
|
| { enabled: false }
|
|
88
89
|
| { enabled: true; checkout: { branch?: string; tree?: string }; error?: string };
|
|
89
90
|
|
|
91
|
+
/** Draft without a readable checkout stays on the published page. */
|
|
92
|
+
export function previewReplacesPublished(
|
|
93
|
+
preview: PreviewState,
|
|
94
|
+
): preview is Extract<PreviewState, { enabled: true }> {
|
|
95
|
+
return preview.enabled && preview.error !== "invalid_session";
|
|
96
|
+
}
|
|
97
|
+
|
|
90
98
|
type RouteHit = { locale: string; type: string; slug: string };
|
|
91
99
|
|
|
92
100
|
async function checkoutQueryToken(
|
|
@@ -107,7 +115,11 @@ async function resolvePreviewState(
|
|
|
107
115
|
): Promise<PreviewState> {
|
|
108
116
|
if (injected) {
|
|
109
117
|
if (!injected.enabled) return { enabled: false };
|
|
110
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
enabled: true,
|
|
120
|
+
checkout: injected.checkout ?? {},
|
|
121
|
+
...(injected.error ? { error: injected.error } : {}),
|
|
122
|
+
};
|
|
111
123
|
}
|
|
112
124
|
try {
|
|
113
125
|
const mod = (await import("next/headers" as string)) as {
|
|
@@ -224,7 +236,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
224
236
|
const hit = resolve(resolved[CATCH_ALL_PARAM]);
|
|
225
237
|
if (!hit) return failFor();
|
|
226
238
|
const preview = await resolvePreviewState(cms, options.preview, searchParams);
|
|
227
|
-
if (preview
|
|
239
|
+
if (previewReplacesPublished(preview)) return loadPreviewDoc(hit, preview);
|
|
228
240
|
const doc = await cms.getDoc(hit.locale, hit.type, hit.slug);
|
|
229
241
|
if (!doc) return failFor();
|
|
230
242
|
return layoutDoc(doc, false);
|
|
@@ -259,7 +271,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
259
271
|
const hit = resolve(resolved[CATCH_ALL_PARAM]);
|
|
260
272
|
if (!hit) return null;
|
|
261
273
|
const preview = await resolvePreviewState(cms, options.preview, searchParams);
|
|
262
|
-
if (!preview
|
|
274
|
+
if (!previewReplacesPublished(preview)) return null;
|
|
263
275
|
return createElement(
|
|
264
276
|
Fragment,
|
|
265
277
|
null,
|