@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backmatter/next",
3
- "version": "0.0.10",
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.10",
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 `createCatchAll` so `"use cache"` only closes over serializable args. */
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 createCatchAll registered getDoc");
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
  };
@@ -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 type { ModelDef } from "@backmatter/core";
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
- return createCore({ ...options, taggedApiFetch: true });
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 { getCachedDoc, registerCachedDocGet } from "./cached-doc";
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 getCachedDoc(hit.locale, hit.type, hit.slug);
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 getCachedDoc(hit.locale, hit.type, hit.slug);
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
- createElement(PublishedRoute, props),
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 getCachedDoc(hit.locale, hit.type, hit.slug);
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 };