@capacms/sdk 1.0.0-next.2 → 1.0.0-next.4
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/README.md +140 -7
- package/dist/next/attrs.d.ts +38 -3
- package/dist/next/attrs.js +64 -1
- package/dist/next/client.d.ts +71 -1
- package/dist/next/client.js +81 -8
- package/dist/next/index.d.ts +6 -4
- package/dist/next/index.js +8 -1
- package/dist/next/inflate.d.ts +33 -0
- package/dist/next/inflate.js +229 -0
- package/dist/next/select-types.d.ts +17 -0
- package/dist/nextjs/index.d.ts +176 -1
- package/dist/nextjs/index.js +241 -3
- package/package.json +2 -2
package/dist/nextjs/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_API_VERSION = exports.CAPA_ENV = exports.EDIT_CACHE_CONTROL = exports.DRAFT_COOKIE = exports.EDIT_HEADER = exports.EDIT_PARAM = exports.LAYOUT_PAGE = void 0;
|
|
3
4
|
exports.withCache = withCache;
|
|
4
5
|
exports.tagsFor = tagsFor;
|
|
5
6
|
exports.revalidateFromWebhook = revalidateFromWebhook;
|
|
@@ -7,7 +8,16 @@ exports.draftClient = draftClient;
|
|
|
7
8
|
exports.routeOf = routeOf;
|
|
8
9
|
exports.preview = preview;
|
|
9
10
|
exports.pagesFor = pagesFor;
|
|
11
|
+
exports.editMode = editMode;
|
|
12
|
+
exports.resolveEditRequest = resolveEditRequest;
|
|
13
|
+
exports.getPublishedClient = getPublishedClient;
|
|
14
|
+
exports.getCapaClient = getCapaClient;
|
|
15
|
+
exports.safeSitePath = safeSitePath;
|
|
16
|
+
exports.createPreviewRoute = createPreviewRoute;
|
|
17
|
+
exports.exitPreviewRoute = exitPreviewRoute;
|
|
18
|
+
exports.capaMiddleware = capaMiddleware;
|
|
10
19
|
const next_1 = require("../next");
|
|
20
|
+
Object.defineProperty(exports, "LAYOUT_PAGE", { enumerable: true, get: function () { return next_1.LAYOUT_PAGE; } });
|
|
11
21
|
/** Add Next.js fetch-cache options without importing `next/*`. */
|
|
12
22
|
function withCache(fetchImpl, options) {
|
|
13
23
|
return (async (input, init = {}) => {
|
|
@@ -64,6 +74,16 @@ async function draftClient(input) {
|
|
|
64
74
|
* src/app/(marketing)/pricing/page.tsx -> /pricing
|
|
65
75
|
* pages/blog/[slug].tsx -> /blog/[slug]
|
|
66
76
|
* app/page.tsx -> /
|
|
77
|
+
* app/layout.tsx, app/blog/template.tsx -> (layout)
|
|
78
|
+
*
|
|
79
|
+
* A LAYOUT IS NOT A PAGE. `layout.*` and `template.*` render around every page
|
|
80
|
+
* below them, and a layout is not told which one: Next hands it no pathname.
|
|
81
|
+
* So its reads cannot be charged to the page being rendered, and charging them
|
|
82
|
+
* to the route its own file sits at is wrong: a root layout's Site singleton
|
|
83
|
+
* and nav would all be recorded as reads of `/`. `routeOf` returns
|
|
84
|
+
* `LAYOUT_PAGE` (`"(layout)"`) for these files instead, and a read that names
|
|
85
|
+
* it sends no `Capa-Page` at all. Pass `routeOf(import.meta.url)` from a layout
|
|
86
|
+
* exactly as from a page and it does the right thing.
|
|
67
87
|
*
|
|
68
88
|
* WHAT IS DROPPED, and why each one:
|
|
69
89
|
*
|
|
@@ -74,8 +94,7 @@ async function draftClient(input) {
|
|
|
74
94
|
* parallel slots `@modal` the same
|
|
75
95
|
* the `(.)` intercept marker an intercepting route at the SAME
|
|
76
96
|
* level renders the segment beside it
|
|
77
|
-
* `page.*`, `
|
|
78
|
-
* `default.*`, `template.*`
|
|
97
|
+
* `page.*`, `route.*`, `default.*` leaf files, not segments
|
|
79
98
|
* `index` in the pages router the folder IS the route
|
|
80
99
|
* the extension never in a URL
|
|
81
100
|
*
|
|
@@ -87,7 +106,9 @@ async function draftClient(input) {
|
|
|
87
106
|
* case the message says to pass the page string yourself.
|
|
88
107
|
*/
|
|
89
108
|
const ROUTER_ROOTS = new Set(["app", "pages"]);
|
|
90
|
-
const LEAF_FILES = new Set(["page", "
|
|
109
|
+
const LEAF_FILES = new Set(["page", "route", "default"]);
|
|
110
|
+
/** Files that render around many routes, so they name none. */
|
|
111
|
+
const LAYOUT_FILES = new Set(["layout", "template"]);
|
|
91
112
|
/** `(..)photo`, `(..)(..)photo`, `(...)photo`: the URL is not here. */
|
|
92
113
|
const OUTER_INTERCEPT = /^(\(\.\.\.\)|(\(\.\.\))+)/;
|
|
93
114
|
function routeOf(file) {
|
|
@@ -131,6 +152,9 @@ function routeOf(file) {
|
|
|
131
152
|
const dot = part.lastIndexOf(".");
|
|
132
153
|
if (dot > 0)
|
|
133
154
|
part = part.slice(0, dot);
|
|
155
|
+
// Only in the app router: `pages/layout.tsx` is a page at `/layout`.
|
|
156
|
+
if (parts[rootIndex] === "app" && LAYOUT_FILES.has(part))
|
|
157
|
+
return next_1.LAYOUT_PAGE;
|
|
134
158
|
if (LEAF_FILES.has(part))
|
|
135
159
|
continue;
|
|
136
160
|
// The pages router: `pages/blog/index.tsx` is `/blog`.
|
|
@@ -176,3 +200,217 @@ async function preview(token, client) {
|
|
|
176
200
|
function pagesFor(client) {
|
|
177
201
|
return client.pages;
|
|
178
202
|
}
|
|
203
|
+
// -------------------------------------------------------------- edit mode ---
|
|
204
|
+
/**
|
|
205
|
+
* The query parameter that turns edit mode on for one request without draft
|
|
206
|
+
* content: `?capa-edit=<token>`, where the token is a Capa preview token. The
|
|
207
|
+
* Capa editor's Published view sends it so the published page is still
|
|
208
|
+
* clickable. It never switches the site to draft data.
|
|
209
|
+
*/
|
|
210
|
+
exports.EDIT_PARAM = "capa-edit";
|
|
211
|
+
/**
|
|
212
|
+
* The request header `resolveEditRequest` sets once a `capa-edit` token has
|
|
213
|
+
* been verified, for `editMode()` to read. Any copy a browser sent is removed
|
|
214
|
+
* first, so it cannot be forged from outside.
|
|
215
|
+
*/
|
|
216
|
+
exports.EDIT_HEADER = "x-capa-edit";
|
|
217
|
+
/** The cookie Next's `draftMode().enable()` sets. */
|
|
218
|
+
exports.DRAFT_COOKIE = "__prerender_bypass";
|
|
219
|
+
/** What every edit-mode response must send: never cached, never shared. */
|
|
220
|
+
exports.EDIT_CACHE_CONTROL = "private, no-store";
|
|
221
|
+
/**
|
|
222
|
+
* Whether this request renders in edit mode: Next draft mode is on, or the
|
|
223
|
+
* request carried a verified `capa-edit` token (see `resolveEditRequest`).
|
|
224
|
+
*
|
|
225
|
+
* Pass Next's own functions; this package imports nothing from `next`:
|
|
226
|
+
*
|
|
227
|
+
* import { draftMode, headers } from "next/headers";
|
|
228
|
+
* const edit = await editMode({ draftMode, headers });
|
|
229
|
+
* const client = createClient({ ...config, editMode: edit });
|
|
230
|
+
*
|
|
231
|
+
* A client built with `editMode: edit` marks what it reads, and `capaAttrs`
|
|
232
|
+
* then tags those entries and only those.
|
|
233
|
+
*/
|
|
234
|
+
async function editMode(input) {
|
|
235
|
+
const [draft, headers] = await Promise.all([input.draftMode(), input.headers()]);
|
|
236
|
+
return draft.isEnabled === true || headers.get(exports.EDIT_HEADER) === "1";
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The middleware half of edit mode.
|
|
240
|
+
*
|
|
241
|
+
* export async function middleware(request: NextRequest) {
|
|
242
|
+
* const edit = await resolveEditRequest(request, publishedClient());
|
|
243
|
+
* const response = NextResponse.next({ request: { headers: edit.headers } });
|
|
244
|
+
* if (edit.cacheControl) response.headers.set("Cache-Control", edit.cacheControl);
|
|
245
|
+
* return response;
|
|
246
|
+
* }
|
|
247
|
+
*
|
|
248
|
+
* The token is checked with Capa (`client.preview`), exactly as a preview link
|
|
249
|
+
* is: the site never holds the signing key. A bad, expired or unverifiable
|
|
250
|
+
* token means not in edit mode; it never throws, because a broken edit link
|
|
251
|
+
* must still render the public page.
|
|
252
|
+
*/
|
|
253
|
+
async function resolveEditRequest(request, client) {
|
|
254
|
+
const headers = new Headers(request.headers);
|
|
255
|
+
headers.delete(exports.EDIT_HEADER);
|
|
256
|
+
const token = new URL(String(request.url)).searchParams.get(exports.EDIT_PARAM);
|
|
257
|
+
let verified = false;
|
|
258
|
+
if (token) {
|
|
259
|
+
try {
|
|
260
|
+
verified = (await client.preview(token)) !== null;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
verified = false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (verified)
|
|
267
|
+
headers.set(exports.EDIT_HEADER, "1");
|
|
268
|
+
const draft = request.cookies?.has(exports.DRAFT_COOKIE) ??
|
|
269
|
+
(headers.get("cookie") ?? "").split(/;\s*/).some((c) => c.startsWith(`${exports.DRAFT_COOKIE}=`));
|
|
270
|
+
const edit = verified || draft;
|
|
271
|
+
return { edit, verified, headers, cacheControl: edit ? exports.EDIT_CACHE_CONTROL : null };
|
|
272
|
+
}
|
|
273
|
+
// ------------------------------------------------- five-minute integration ---
|
|
274
|
+
/** Where the env-driven helpers read their settings (M6). */
|
|
275
|
+
exports.CAPA_ENV = {
|
|
276
|
+
baseUrl: "CAPA_API_URL",
|
|
277
|
+
apiKey: "CAPA_KEY",
|
|
278
|
+
draftKey: "CAPA_DRAFT_KEY",
|
|
279
|
+
version: "CAPA_API_VERSION",
|
|
280
|
+
};
|
|
281
|
+
exports.DEFAULT_API_VERSION = "2026-10-01";
|
|
282
|
+
function readEnv(name) {
|
|
283
|
+
const env = globalThis.process?.env;
|
|
284
|
+
const value = env?.[name];
|
|
285
|
+
return value === undefined || value === "" ? undefined : value;
|
|
286
|
+
}
|
|
287
|
+
function requireEnv(name) {
|
|
288
|
+
const value = readEnv(name);
|
|
289
|
+
if (!value)
|
|
290
|
+
throw new Error(`@capacms/sdk/nextjs: ${name} is not set.`);
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
/** The published-key client from env: what verifying a token needs. */
|
|
294
|
+
function getPublishedClient(overrides = {}) {
|
|
295
|
+
return (0, next_1.createClient)({
|
|
296
|
+
baseUrl: requireEnv(exports.CAPA_ENV.baseUrl),
|
|
297
|
+
apiKey: requireEnv(exports.CAPA_ENV.apiKey),
|
|
298
|
+
version: readEnv(exports.CAPA_ENV.version) ?? exports.DEFAULT_API_VERSION,
|
|
299
|
+
...overrides,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The client for this request, from env: the draft key under draft mode,
|
|
304
|
+
* otherwise the published key, and `editMode` worked out for you.
|
|
305
|
+
*
|
|
306
|
+
* import { draftMode, headers } from "next/headers";
|
|
307
|
+
* const capa = await getCapaClient({ draftMode, headers });
|
|
308
|
+
*/
|
|
309
|
+
async function getCapaClient(input) {
|
|
310
|
+
const draft = (await input.draftMode()).isEnabled === true;
|
|
311
|
+
const edit = await editMode({ draftMode: input.draftMode, headers: input.headers });
|
|
312
|
+
return (0, next_1.createClient)({
|
|
313
|
+
baseUrl: requireEnv(exports.CAPA_ENV.baseUrl),
|
|
314
|
+
apiKey: requireEnv(draft ? exports.CAPA_ENV.draftKey : exports.CAPA_ENV.apiKey),
|
|
315
|
+
version: readEnv(exports.CAPA_ENV.version) ?? exports.DEFAULT_API_VERSION,
|
|
316
|
+
editMode: edit,
|
|
317
|
+
...input.config,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/** Only a path on this site: never `//elsewhere.example` or a full URL. */
|
|
321
|
+
function safeSitePath(value) {
|
|
322
|
+
if (!value || !value.startsWith("/") || value.startsWith("//"))
|
|
323
|
+
return "/";
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* `app/api/capa/preview/route.ts`:
|
|
328
|
+
*
|
|
329
|
+
* import { draftMode } from "next/headers";
|
|
330
|
+
* import { redirect } from "next/navigation";
|
|
331
|
+
* export const GET = createPreviewRoute({ draftMode, redirect });
|
|
332
|
+
*
|
|
333
|
+
* Checks the token with Capa (the site never holds the signing key), turns
|
|
334
|
+
* draft mode on and lands on the entry's page. A bad or expired token lands on
|
|
335
|
+
* the page without draft mode and `?preview=expired`; Capa unreachable gives
|
|
336
|
+
* `?preview=unavailable`.
|
|
337
|
+
*/
|
|
338
|
+
function createPreviewRoute(input) {
|
|
339
|
+
return async (request) => {
|
|
340
|
+
const url = new URL(request.url);
|
|
341
|
+
const token = url.searchParams.get("token") ?? url.searchParams.get("capa-preview") ?? "";
|
|
342
|
+
// Reached two ways: directly, or through the middleware's rewrite of a page
|
|
343
|
+
// URL carrying `?capa-preview=`, where Next may hand over the ORIGINAL URL.
|
|
344
|
+
// Then the page itself is the path.
|
|
345
|
+
const path = safeSitePath(url.searchParams.get("path") ?? (url.searchParams.has("capa-preview") ? url.pathname : null));
|
|
346
|
+
let claim = null;
|
|
347
|
+
let failed = false;
|
|
348
|
+
try {
|
|
349
|
+
claim = await (input.client ?? getPublishedClient)().preview(token);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
failed = true;
|
|
353
|
+
}
|
|
354
|
+
const draft = await input.draftMode();
|
|
355
|
+
if (!claim) {
|
|
356
|
+
draft.disable?.();
|
|
357
|
+
return input.redirect(`${path}?preview=${failed ? "unavailable" : "expired"}`);
|
|
358
|
+
}
|
|
359
|
+
draft.enable?.();
|
|
360
|
+
await input.onEnable?.();
|
|
361
|
+
return input.redirect(safeSitePath(claim.path ?? path));
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/** `app/api/capa/exit/route.ts`: `export const GET = exitPreviewRoute({ draftMode, redirect });` */
|
|
365
|
+
function exitPreviewRoute(input) {
|
|
366
|
+
return async (request) => {
|
|
367
|
+
(await input.draftMode()).disable?.();
|
|
368
|
+
return input.redirect(safeSitePath(new URL(request.url).searchParams.get("path")));
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* `middleware.ts` in one line:
|
|
373
|
+
*
|
|
374
|
+
* import { NextResponse } from "next/server";
|
|
375
|
+
* export const middleware = capaMiddleware({ NextResponse });
|
|
376
|
+
*
|
|
377
|
+
* - `?capa-preview=<token>` on any page goes to `previewRoute`, which turns
|
|
378
|
+
* draft mode on and comes back;
|
|
379
|
+
* - `?capa-view=published` renders without the draft cookie, for the editor's
|
|
380
|
+
* Published view;
|
|
381
|
+
* - `?capa-edit=<token>` turns edit mode on (ids and overlay, published data);
|
|
382
|
+
* - every edit-mode response is `private, no-store`.
|
|
383
|
+
*/
|
|
384
|
+
function capaMiddleware(input) {
|
|
385
|
+
const previewRoute = input.previewRoute ?? "/api/capa/preview";
|
|
386
|
+
return async (request) => {
|
|
387
|
+
const token = request.nextUrl.searchParams.get("capa-preview");
|
|
388
|
+
if (token) {
|
|
389
|
+
const target = request.nextUrl.clone();
|
|
390
|
+
target.pathname = previewRoute;
|
|
391
|
+
target.search = "";
|
|
392
|
+
target.searchParams.set("token", token);
|
|
393
|
+
target.searchParams.set("path", request.nextUrl.pathname);
|
|
394
|
+
return input.NextResponse.rewrite(target);
|
|
395
|
+
}
|
|
396
|
+
const headers = new Headers(request.headers);
|
|
397
|
+
if (request.nextUrl.searchParams.get("capa-view") === "published") {
|
|
398
|
+
const cookies = request.cookies
|
|
399
|
+
.getAll()
|
|
400
|
+
.filter((cookie) => cookie.name !== exports.DRAFT_COOKIE)
|
|
401
|
+
.map((cookie) => `${cookie.name}=${encodeURIComponent(cookie.value)}`)
|
|
402
|
+
.join("; ");
|
|
403
|
+
if (cookies)
|
|
404
|
+
headers.set("cookie", cookies);
|
|
405
|
+
else
|
|
406
|
+
headers.delete("cookie");
|
|
407
|
+
}
|
|
408
|
+
// The client is built only when a token needs checking, so a missing env
|
|
409
|
+
// value cannot break every page of the site.
|
|
410
|
+
const edit = await resolveEditRequest({ url: request.url, headers }, { preview: (t) => (input.client ?? getPublishedClient)().preview(t) });
|
|
411
|
+
const response = input.NextResponse.next({ request: { headers: edit.headers } });
|
|
412
|
+
if (edit.cacheControl)
|
|
413
|
+
response.headers.set("Cache-Control", edit.cacheControl);
|
|
414
|
+
return response;
|
|
415
|
+
};
|
|
416
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capacms/sdk",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.4",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -71,6 +71,6 @@
|
|
|
71
71
|
"scripts": {
|
|
72
72
|
"build": "tsc -p tsconfig.json",
|
|
73
73
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
74
|
-
"test": "tsc -p tsconfig.json && node --test test/codegen.test.js test/client.test.js test/next-client.test.js test/nextjs.test.js test/webhooks.test.js test/attrs.test.js test/overlay.test.js && tsc -p test/types/tsconfig.consumer.json --noEmit"
|
|
74
|
+
"test": "tsc -p tsconfig.json && node --test test/codegen.test.js test/client.test.js test/next-client.test.js test/nextjs.test.js test/webhooks.test.js test/attrs.test.js test/overlay.test.js test/inflate.test.js && tsc -p test/types/tsconfig.consumer.json --noEmit"
|
|
75
75
|
}
|
|
76
76
|
}
|