@backmatter/next 0.0.10 → 0.0.12
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 +2 -2
- package/src/cached-doc.ts +40 -3
- package/src/config.ts +6 -0
- package/src/create-backmatter.ts +16 -2
- package/src/routes.ts +11 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backmatter/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
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.11",
|
|
42
42
|
"@mdx-js/mdx": "^3.1.1",
|
|
43
43
|
"remark-gfm": "^4.0.1"
|
|
44
44
|
},
|
package/src/cached-doc.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// packages/next/src/cached-doc.ts
|
|
2
|
-
import { contentCacheTags, type BackmatterDoc } from "@backmatter/core";
|
|
2
|
+
import { contentCacheTags, listCacheTags, type BackmatterDoc, type BackmatterDocSummary } from "@backmatter/core";
|
|
3
3
|
|
|
4
4
|
export type CacheFns = {
|
|
5
5
|
cacheLife: (profile: string) => void;
|
|
@@ -12,13 +12,23 @@ export type GetDocFn = (
|
|
|
12
12
|
slug: string,
|
|
13
13
|
) => Promise<BackmatterDoc | null>;
|
|
14
14
|
|
|
15
|
+
export type ListDocsFn = (filter?: {
|
|
16
|
+
type?: string;
|
|
17
|
+
locale?: string;
|
|
18
|
+
}) => Promise<BackmatterDocSummary[]>;
|
|
19
|
+
|
|
15
20
|
let registeredGetDoc: GetDocFn | null = null;
|
|
21
|
+
let registeredListDocs: ListDocsFn | null = null;
|
|
16
22
|
|
|
17
|
-
/** Called from `
|
|
23
|
+
/** Called from `createBackmatter` so `"use cache"` only closes over serializable args. */
|
|
18
24
|
export function registerCachedDocGet(getDoc: GetDocFn): void {
|
|
19
25
|
registeredGetDoc = getDoc;
|
|
20
26
|
}
|
|
21
27
|
|
|
28
|
+
export function registerCachedListDocs(listDocs: ListDocsFn): void {
|
|
29
|
+
registeredListDocs = listDocs;
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
async function loadCacheFns(): Promise<CacheFns> {
|
|
23
33
|
try {
|
|
24
34
|
const mod = (await import("next/cache" as string)) as CacheFns;
|
|
@@ -49,7 +59,34 @@ export async function getCachedDoc(
|
|
|
49
59
|
fns.cacheTag(...contentCacheTags(locale, type, slug));
|
|
50
60
|
const getDoc = registeredGetDoc;
|
|
51
61
|
if (!getDoc) {
|
|
52
|
-
throw new Error("[backmatter] getCachedDoc called before
|
|
62
|
+
throw new Error("[backmatter] getCachedDoc called before createBackmatter registered getDoc");
|
|
53
63
|
}
|
|
54
64
|
return getDoc(locale, type, slug);
|
|
55
65
|
}
|
|
66
|
+
|
|
67
|
+
export async function getCachedDocs(
|
|
68
|
+
type?: string,
|
|
69
|
+
locale?: string,
|
|
70
|
+
cache?: CacheFns,
|
|
71
|
+
): Promise<BackmatterDocSummary[]> {
|
|
72
|
+
"use cache";
|
|
73
|
+
const fns = cache ?? (await loadCacheFns());
|
|
74
|
+
fns.cacheLife("hours");
|
|
75
|
+
fns.cacheTag(...listCacheTags(type));
|
|
76
|
+
const listDocs = registeredListDocs;
|
|
77
|
+
if (!listDocs) {
|
|
78
|
+
throw new Error("[backmatter] getCachedDocs called before createBackmatter registered listDocs");
|
|
79
|
+
}
|
|
80
|
+
return listDocs({ type, locale });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Unwrap Next `params` inside `"use cache"` so Cache Components can prerender. */
|
|
84
|
+
export async function cachedRouteParams<T extends object>(
|
|
85
|
+
params: Promise<T> | T,
|
|
86
|
+
cache?: CacheFns,
|
|
87
|
+
): Promise<T> {
|
|
88
|
+
"use cache";
|
|
89
|
+
const fns = cache ?? (await loadCacheFns());
|
|
90
|
+
fns.cacheLife("hours");
|
|
91
|
+
return await params;
|
|
92
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -43,6 +43,7 @@ type TurbopackRule = { loaders: Array<{ loader: string; options: Record<string,
|
|
|
43
43
|
export type NextConfigLike = {
|
|
44
44
|
cacheComponents?: boolean;
|
|
45
45
|
partialPrefetching?: boolean | "unstable_eager";
|
|
46
|
+
transpilePackages?: string[];
|
|
46
47
|
turbopack?: Record<string, unknown>;
|
|
47
48
|
webpack?: ((config: WebpackConfigLike, context: any) => WebpackConfigLike) | null;
|
|
48
49
|
} & Record<string, unknown>;
|
|
@@ -119,6 +120,7 @@ export function registryMatchers(loaderOptions: BackmatterLoaderOptions): { glob
|
|
|
119
120
|
export type WithBackmatterResult<T extends object> = Omit<T, "turbopack" | "webpack"> & {
|
|
120
121
|
cacheComponents: boolean;
|
|
121
122
|
partialPrefetching: boolean | "unstable_eager";
|
|
123
|
+
transpilePackages: string[];
|
|
122
124
|
turbopack: Record<string, unknown>;
|
|
123
125
|
webpack: NonNullable<NextConfigLike["webpack"]>;
|
|
124
126
|
};
|
|
@@ -150,10 +152,14 @@ export function withBackmatter<T extends object>(
|
|
|
150
152
|
};
|
|
151
153
|
|
|
152
154
|
const cacheComponents = cfg.cacheComponents ?? true;
|
|
155
|
+
const transpilePackages = [
|
|
156
|
+
...new Set([...(cfg.transpilePackages ?? []), "@backmatter/next", "@backmatter/core"]),
|
|
157
|
+
];
|
|
153
158
|
return {
|
|
154
159
|
...nextConfig,
|
|
155
160
|
cacheComponents,
|
|
156
161
|
partialPrefetching: cfg.partialPrefetching ?? cacheComponents,
|
|
162
|
+
transpilePackages,
|
|
157
163
|
turbopack,
|
|
158
164
|
webpack,
|
|
159
165
|
};
|
package/src/create-backmatter.ts
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
// packages/next/src/create-backmatter.ts
|
|
2
2
|
// Next hosts cache API fetches with tags so `content.committed` can
|
|
3
3
|
// `revalidateTag("backmatter")`. Core/CLI keep `cache: "no-store"`.
|
|
4
|
+
// getDoc/listDocs also go through `"use cache"` so Cache Components can prerender.
|
|
5
|
+
import { createQuery, type ModelDef } from "@backmatter/core";
|
|
4
6
|
import {
|
|
5
7
|
createBackmatter as createCore,
|
|
6
8
|
type CreateBackmatterOptions,
|
|
7
9
|
type BackmatterClient,
|
|
8
10
|
} from "@backmatter/core/node/create-backmatter";
|
|
9
|
-
import
|
|
11
|
+
import { getCachedDoc, getCachedDocs, registerCachedDocGet, registerCachedListDocs } from "./cached-doc";
|
|
10
12
|
|
|
11
13
|
export function createBackmatter<const Models extends readonly ModelDef[]>(
|
|
12
14
|
options: CreateBackmatterOptions<Models>,
|
|
13
15
|
): BackmatterClient<Models> {
|
|
14
|
-
|
|
16
|
+
const core = createCore({ ...options, taggedApiFetch: true });
|
|
17
|
+
registerCachedDocGet(core.getDoc);
|
|
18
|
+
registerCachedListDocs(core.listDocs);
|
|
19
|
+
const getDoc: BackmatterClient<Models>["getDoc"] = (locale, type, slug) =>
|
|
20
|
+
getCachedDoc(locale, type, slug);
|
|
21
|
+
const listDocs: BackmatterClient<Models>["listDocs"] = (filter) =>
|
|
22
|
+
getCachedDocs(filter?.type, filter?.locale);
|
|
23
|
+
return {
|
|
24
|
+
...core,
|
|
25
|
+
getDoc,
|
|
26
|
+
listDocs,
|
|
27
|
+
query: createQuery(options.models, { getDoc, listDocs }),
|
|
28
|
+
};
|
|
15
29
|
}
|
package/src/routes.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { matchRoute, modelFormat, PREVIEW_CHECKOUT_QUERY, PREVIEW_COOKIE, readPr
|
|
|
8
8
|
import type { BackmatterClient } from "@backmatter/core/node/create-backmatter";
|
|
9
9
|
import { fetchPreviewDoc } from "@backmatter/core/preview";
|
|
10
10
|
import { renderDoc, type MdxComponents } from "./mdx-compile";
|
|
11
|
-
import {
|
|
11
|
+
import { cachedRouteParams } from "./cached-doc";
|
|
12
12
|
import { renderPage, type BlockComponents, type BlockFallbacks } from "./page-render";
|
|
13
13
|
import { previewUnavailable } from "./preview-ui";
|
|
14
14
|
|
|
@@ -62,7 +62,7 @@ export type CatchAllRoute = {
|
|
|
62
62
|
Page: (props: {
|
|
63
63
|
params: Promise<CatchAllParams>;
|
|
64
64
|
searchParams?: Promise<CatchAllSearchParams>;
|
|
65
|
-
}) => ReactNode
|
|
65
|
+
}) => Promise<ReactNode>;
|
|
66
66
|
CmsRoute: (props: {
|
|
67
67
|
params: Promise<CatchAllParams> | CatchAllParams;
|
|
68
68
|
searchParams?: Promise<CatchAllSearchParams> | CatchAllSearchParams;
|
|
@@ -133,7 +133,6 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
133
133
|
cms: BackmatterClient<Models>,
|
|
134
134
|
options: CatchAllOptions = {},
|
|
135
135
|
): CatchAllRoute {
|
|
136
|
-
registerCachedDocGet((locale, type, slug) => cms.getDoc(locale, type, slug));
|
|
137
136
|
const models = cms.models as readonly ModelDef[];
|
|
138
137
|
const byType = new Map(models.map((m) => [m.type, m]));
|
|
139
138
|
const matchOptions = {
|
|
@@ -221,12 +220,12 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
const CmsRoute: CatchAllRoute["CmsRoute"] = async ({ params, searchParams }) => {
|
|
224
|
-
const resolved = await params;
|
|
223
|
+
const resolved = await cachedRouteParams(params);
|
|
225
224
|
const hit = resolve(resolved[CATCH_ALL_PARAM]);
|
|
226
225
|
if (!hit) return failFor();
|
|
227
226
|
const preview = await resolvePreviewState(cms, options.preview, searchParams);
|
|
228
227
|
if (preview.enabled) return loadPreviewDoc(hit, preview);
|
|
229
|
-
const doc = await
|
|
228
|
+
const doc = await cms.getDoc(hit.locale, hit.type, hit.slug);
|
|
230
229
|
if (!doc) return failFor();
|
|
231
230
|
return layoutDoc(doc, false);
|
|
232
231
|
};
|
|
@@ -237,10 +236,10 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
237
236
|
params: Promise<CatchAllParams> | CatchAllParams;
|
|
238
237
|
searchParams?: Promise<CatchAllSearchParams> | CatchAllSearchParams;
|
|
239
238
|
}) => {
|
|
240
|
-
const resolved = await params;
|
|
239
|
+
const resolved = await cachedRouteParams(params);
|
|
241
240
|
const hit = resolve(resolved[CATCH_ALL_PARAM]);
|
|
242
241
|
if (!hit) return failFor();
|
|
243
|
-
const doc = await
|
|
242
|
+
const doc = await cms.getDoc(hit.locale, hit.type, hit.slug);
|
|
244
243
|
if (!doc) return failFor();
|
|
245
244
|
return createElement(
|
|
246
245
|
"div",
|
|
@@ -256,7 +255,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
256
255
|
params: Promise<CatchAllParams> | CatchAllParams;
|
|
257
256
|
searchParams?: Promise<CatchAllSearchParams> | CatchAllSearchParams;
|
|
258
257
|
}) => {
|
|
259
|
-
const resolved = await params;
|
|
258
|
+
const resolved = await cachedRouteParams(params);
|
|
260
259
|
const hit = resolve(resolved[CATCH_ALL_PARAM]);
|
|
261
260
|
if (!hit) return null;
|
|
262
261
|
const preview = await resolvePreviewState(cms, options.preview, searchParams);
|
|
@@ -273,19 +272,19 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
273
272
|
);
|
|
274
273
|
};
|
|
275
274
|
|
|
276
|
-
const Page: CatchAllRoute["Page"] = (props) =>
|
|
275
|
+
const Page: CatchAllRoute["Page"] = async (props) =>
|
|
277
276
|
createElement(
|
|
278
277
|
Fragment,
|
|
279
278
|
null,
|
|
280
|
-
|
|
279
|
+
await PublishedRoute(props),
|
|
281
280
|
createElement(Suspense, { fallback: null }, createElement(PreviewBranch, props)),
|
|
282
281
|
);
|
|
283
282
|
|
|
284
283
|
const generateMetadata: CatchAllRoute["generateMetadata"] = async ({ params }) => {
|
|
285
|
-
const resolved = await params;
|
|
284
|
+
const resolved = await cachedRouteParams(params);
|
|
286
285
|
const hit = resolve(resolved[CATCH_ALL_PARAM]);
|
|
287
286
|
if (!hit) return {};
|
|
288
|
-
const doc = await
|
|
287
|
+
const doc = await cms.getDoc(hit.locale, hit.type, hit.slug);
|
|
289
288
|
if (!doc) return {};
|
|
290
289
|
const title = doc.frontmatter.title;
|
|
291
290
|
return { title: typeof title === "string" ? title : hit.slug };
|