@capacms/sdk 1.0.0-next.3 → 1.0.0-next.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +323 -0
  2. package/README.md +1163 -186
  3. package/bin/capa-codegen.js +192 -5
  4. package/bin/capa.js +208 -0
  5. package/bin/graphql-project.js +142 -0
  6. package/bin/project-env.js +58 -0
  7. package/dist/client.d.ts +5 -0
  8. package/dist/client.js +17 -0
  9. package/dist/codegen.d.ts +55 -0
  10. package/dist/codegen.js +320 -39
  11. package/dist/config.d.ts +5 -1
  12. package/dist/graphql-codegen.d.ts +117 -0
  13. package/dist/graphql-codegen.js +705 -0
  14. package/dist/http.js +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +2 -1
  17. package/dist/next/attrs.d.ts +84 -10
  18. package/dist/next/attrs.js +119 -2
  19. package/dist/next/client.d.ts +160 -31
  20. package/dist/next/client.js +178 -89
  21. package/dist/next/entry-fields.d.ts +162 -0
  22. package/dist/next/entry-fields.js +2 -0
  23. package/dist/next/errors.d.ts +136 -0
  24. package/dist/next/errors.js +214 -0
  25. package/dist/next/field-names.d.ts +37 -0
  26. package/dist/next/field-names.js +145 -0
  27. package/dist/next/graphql/build.d.ts +27 -0
  28. package/dist/next/graphql/build.js +98 -0
  29. package/dist/next/graphql/documents.d.ts +67 -0
  30. package/dist/next/graphql/documents.js +35 -0
  31. package/dist/next/graphql/edit-mode.d.ts +16 -0
  32. package/dist/next/graphql/edit-mode.js +93 -0
  33. package/dist/next/graphql/filter-values.d.ts +34 -0
  34. package/dist/next/graphql/filter-values.js +96 -0
  35. package/dist/next/graphql/introspection.d.ts +89 -0
  36. package/dist/next/graphql/introspection.js +102 -0
  37. package/dist/next/graphql/plan.d.ts +115 -0
  38. package/dist/next/graphql/plan.js +531 -0
  39. package/dist/next/graphql/request.d.ts +228 -0
  40. package/dist/next/graphql/request.js +283 -0
  41. package/dist/next/graphql/rest.d.ts +66 -0
  42. package/dist/next/graphql/rest.js +502 -0
  43. package/dist/next/graphql/selection.d.ts +55 -0
  44. package/dist/next/graphql/selection.js +212 -0
  45. package/dist/next/graphql/sha256.d.ts +13 -0
  46. package/dist/next/graphql/sha256.js +86 -0
  47. package/dist/next/graphql/summary.d.ts +83 -0
  48. package/dist/next/graphql/summary.js +151 -0
  49. package/dist/next/graphql/tree-layout.d.ts +36 -0
  50. package/dist/next/graphql/tree-layout.js +20 -0
  51. package/dist/next/graphql/tree.d.ts +171 -0
  52. package/dist/next/graphql/tree.js +249 -0
  53. package/dist/next/graphql/typed.d.ts +261 -0
  54. package/dist/next/graphql/typed.js +146 -0
  55. package/dist/next/index.d.ts +29 -4
  56. package/dist/next/index.js +31 -1
  57. package/dist/next/inflate.d.ts +51 -0
  58. package/dist/next/inflate.js +243 -0
  59. package/dist/next/key-family.d.ts +34 -0
  60. package/dist/next/key-family.js +74 -0
  61. package/dist/next/select-types.d.ts +58 -5
  62. package/dist/next/system-keys.d.ts +27 -0
  63. package/dist/next/system-keys.js +42 -0
  64. package/dist/nextjs/index.d.ts +333 -6
  65. package/dist/nextjs/index.js +450 -4
  66. package/dist/nextjs/overlay.d.ts +5 -0
  67. package/dist/nextjs/overlay.js +35 -0
  68. package/package.json +35 -13
@@ -1,15 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LAYOUT_PAGE = void 0;
3
+ exports.DEFAULT_API_VERSION = exports.CAPA_ENV_ALIASES = exports.CAPA_ENV = exports.EDIT_CACHE_CONTROL = exports.DRAFT_COOKIE = exports.EDIT_HEADER = exports.EDIT_PARAM = exports.LAYOUT_PAGE = exports.gql = exports.MEDIA_TAG = exports.GRAPHQL_TAG = void 0;
4
4
  exports.withCache = withCache;
5
+ exports.modelTag = modelTag;
5
6
  exports.tagsFor = tagsFor;
6
7
  exports.revalidateFromWebhook = revalidateFromWebhook;
7
8
  exports.draftClient = draftClient;
8
9
  exports.routeOf = routeOf;
9
10
  exports.preview = preview;
10
11
  exports.pagesFor = pagesFor;
12
+ exports.editMode = editMode;
13
+ exports.resolveEditRequest = resolveEditRequest;
14
+ exports.getPublishedClient = getPublishedClient;
15
+ exports.getCapaClient = getCapaClient;
16
+ exports.graphql = graphql;
17
+ exports.safeSitePath = safeSitePath;
18
+ exports.createPreviewRoute = createPreviewRoute;
19
+ exports.exitPreviewRoute = exitPreviewRoute;
20
+ exports.capaMiddleware = capaMiddleware;
11
21
  const next_1 = require("../next");
22
+ Object.defineProperty(exports, "gql", { enumerable: true, get: function () { return next_1.gql; } });
12
23
  Object.defineProperty(exports, "LAYOUT_PAGE", { enumerable: true, get: function () { return next_1.LAYOUT_PAGE; } });
24
+ const client_1 = require("../next/client");
25
+ const sha256_1 = require("../next/graphql/sha256");
26
+ const key_family_1 = require("../next/key-family");
13
27
  /** Add Next.js fetch-cache options without importing `next/*`. */
14
28
  function withCache(fetchImpl, options) {
15
29
  return (async (input, init = {}) => {
@@ -23,6 +37,38 @@ function withCache(fetchImpl, options) {
23
37
  return fetchImpl(input, { ...init, next });
24
38
  });
25
39
  }
40
+ /**
41
+ * The Next.js cache tag for every read of a model, by its namespace. A GraphQL
42
+ * query names its models by namespace (`articles`), not by id, so this is the
43
+ * tag a GraphQL read is cached under, and `revalidateFromWebhook` revalidates
44
+ * it when an entry of that model changes.
45
+ */
46
+ function modelTag(namespace) {
47
+ return `capa:model:${namespace}`;
48
+ }
49
+ /**
50
+ * The tag `graphql()` gives a read that names no `tags`, and that
51
+ * `revalidateFromWebhook` revalidates on every content change: such a page is
52
+ * never stale after a publish, at the price of refreshing on any publish.
53
+ * Name the models it reads with `tagsFor({ namespace })` to refresh it only
54
+ * when one of those changes.
55
+ */
56
+ exports.GRAPHQL_TAG = "capa:graphql";
57
+ /**
58
+ * The tag `tagsFor({ namespace })` adds beside its model tags, and that
59
+ * `revalidateFromWebhook` revalidates on a media event: a GraphQL read shows
60
+ * a file's URL and alt text whichever models it names, so editing a file in
61
+ * the media library refreshes it.
62
+ */
63
+ exports.MEDIA_TAG = "capa:media";
64
+ /**
65
+ * Next.js cache tags for a read. `model`, `entry`, `key` and `tenant` are the
66
+ * API's surrogate keys (`m:`, `e:`, `k:`, `t:`), by id; `namespace` is one
67
+ * `capa:model:<namespace>` tag per model a GraphQL query reads, which
68
+ * `capa-codegen --graphql` lists as `<Name>Models`, and `MEDIA_TAG`, since
69
+ * the query may show a file from the media library. Each pairs with
70
+ * `revalidateFromWebhook`.
71
+ */
26
72
  function tagsFor(input) {
27
73
  const tags = [];
28
74
  if (input.model)
@@ -33,9 +79,26 @@ function tagsFor(input) {
33
79
  tags.push(`k:${input.key}`);
34
80
  if (input.tenant)
35
81
  tags.push(`t:${input.tenant}`);
82
+ const namespaces = typeof input.namespace === "string" ? [input.namespace] : (input.namespace ?? []);
83
+ for (const namespace of namespaces)
84
+ if (namespace)
85
+ tags.push(modelTag(namespace));
86
+ if (namespaces.some(Boolean))
87
+ tags.push(exports.MEDIA_TAG);
36
88
  return tags;
37
89
  }
38
- /** Revalidate the concrete entry and model identities carried by a webhook. */
90
+ /**
91
+ * Revalidate every tag a webhook's entry and model can be cached under: the
92
+ * entry (`e:`), the model by id (`m:`) and by namespace (`capa:model:`, the
93
+ * tag a GraphQL read uses). The namespace is `data.modelNamespace` on
94
+ * `instance.published`, `instance.unpublished` and `model.published`, and
95
+ * `data.namespace` on the other events (docs/WEBHOOKS.md). Any of those, and
96
+ * a `media.` event, also revalidates `GRAPHQL_TAG`, the tag of a `graphql()`
97
+ * read that named no tags. A `media.` event (a file's alt text, name or
98
+ * visibility changed, or the file went) revalidates the file's own key
99
+ * (`f:<fileId>`, as REST's `Surrogate-Key` names it) and `MEDIA_TAG`, which
100
+ * every read tagged by namespace carries, since it may show that file.
101
+ */
39
102
  async function revalidateFromWebhook(input) {
40
103
  const data = input.payload?.data ?? input.payload;
41
104
  const tags = [];
@@ -44,14 +107,34 @@ async function revalidateFromWebhook(input) {
44
107
  tags.push(`e:${entryId}`);
45
108
  if (typeof data?.modelId === "string" && data.modelId)
46
109
  tags.push(`m:${data.modelId}`);
110
+ const namespace = data?.modelNamespace ?? data?.namespace;
111
+ if (typeof namespace === "string" && namespace)
112
+ tags.push(modelTag(namespace));
113
+ const media = typeof input.payload?.type === "string" && input.payload.type.startsWith("media.");
114
+ if (media) {
115
+ if (typeof data?.fileId === "string" && data.fileId)
116
+ tags.push(`f:${data.fileId}`);
117
+ tags.push(exports.MEDIA_TAG);
118
+ }
119
+ if (tags.length > 0 || media)
120
+ tags.push(exports.GRAPHQL_TAG);
47
121
  const unique = [...new Set(tags)];
48
122
  for (const tag of unique)
49
123
  await input.revalidateTag(tag);
50
124
  return unique;
51
125
  }
52
- /** Select a client using server-only draft state supplied by the caller. */
126
+ /**
127
+ * Select a client using server-only draft state supplied by the caller. Pass
128
+ * codegen's `CapaQuery` to type the builder, as with `createClient`:
129
+ * `draftClient<CapaQuery>({ ... })`. `production` may hold the legacy key a
130
+ * site already has; `draft` needs a `cap_` key, and a legacy one throws a
131
+ * `TypeError` when draft mode selects it.
132
+ */
53
133
  async function draftClient(input) {
54
- return (0, next_1.createClient)((await input.isDraft()) ? input.draft : input.production);
134
+ if (!(await input.isDraft()))
135
+ return (0, next_1.createClient)(input.production);
136
+ (0, key_family_1.requireDraftKey)(input.draft?.apiKey, "the draft config");
137
+ return (0, next_1.createClient)(input.draft);
55
138
  }
56
139
  // ------------------------------------------------------------------ pages ---
57
140
  /**
@@ -192,3 +275,366 @@ async function preview(token, client) {
192
275
  function pagesFor(client) {
193
276
  return client.pages;
194
277
  }
278
+ // -------------------------------------------------------------- edit mode ---
279
+ /**
280
+ * The query parameter that turns edit mode on for one request without draft
281
+ * content: `?capa-edit=<token>`, where the token is a Capa preview token. The
282
+ * Capa editor's Published view sends it so the published page is still
283
+ * clickable. It never switches the site to draft data.
284
+ */
285
+ exports.EDIT_PARAM = "capa-edit";
286
+ /**
287
+ * The request header `resolveEditRequest` sets once a `capa-edit` token has
288
+ * been verified, for `editMode()` to read. Any copy a browser sent is removed
289
+ * first, so it cannot be forged from outside.
290
+ */
291
+ exports.EDIT_HEADER = "x-capa-edit";
292
+ /** The cookie Next's `draftMode().enable()` sets. */
293
+ exports.DRAFT_COOKIE = "__prerender_bypass";
294
+ /** What every edit-mode response must send: never cached, never shared. */
295
+ exports.EDIT_CACHE_CONTROL = "private, no-store";
296
+ /**
297
+ * Whether this request renders in edit mode: Next draft mode is on, or the
298
+ * request carried a verified `capa-edit` token (see `resolveEditRequest`).
299
+ *
300
+ * Pass Next's own functions; this package imports nothing from `next`:
301
+ *
302
+ * import { draftMode, headers } from "next/headers";
303
+ * const edit = await editMode({ draftMode, headers });
304
+ * const client = createClient({ ...config, editMode: edit });
305
+ *
306
+ * A client built with `editMode: edit` marks what it reads, and `capaAttrs`
307
+ * then tags those entries and only those.
308
+ */
309
+ async function editMode(input) {
310
+ const [draft, headers] = await Promise.all([input.draftMode(), input.headers()]);
311
+ return draft.isEnabled === true || verifiedEdit(headers);
312
+ }
313
+ /** Whether `resolveEditRequest` verified a `capa-edit` token for this request. */
314
+ function verifiedEdit(headers) {
315
+ return headers.get(exports.EDIT_HEADER) === "1";
316
+ }
317
+ /**
318
+ * The middleware half of edit mode.
319
+ *
320
+ * export async function middleware(request: NextRequest) {
321
+ * const edit = await resolveEditRequest(request, publishedClient());
322
+ * const response = NextResponse.next({ request: { headers: edit.headers } });
323
+ * if (edit.cacheControl) response.headers.set("Cache-Control", edit.cacheControl);
324
+ * return response;
325
+ * }
326
+ *
327
+ * The token is checked with Capa (`client.preview`), exactly as a preview link
328
+ * is: the site never holds the signing key. A bad, expired or unverifiable
329
+ * token means not in edit mode; it never throws, because a broken edit link
330
+ * must still render the public page.
331
+ */
332
+ async function resolveEditRequest(request, client) {
333
+ const headers = new Headers(request.headers);
334
+ headers.delete(exports.EDIT_HEADER);
335
+ const token = new URL(String(request.url)).searchParams.get(exports.EDIT_PARAM);
336
+ let verified = false;
337
+ if (token) {
338
+ try {
339
+ verified = (await client.preview(token)) !== null;
340
+ }
341
+ catch {
342
+ verified = false;
343
+ }
344
+ }
345
+ if (verified)
346
+ headers.set(exports.EDIT_HEADER, "1");
347
+ const draft = request.cookies?.has(exports.DRAFT_COOKIE) ??
348
+ (headers.get("cookie") ?? "").split(/;\s*/).some((c) => c.startsWith(`${exports.DRAFT_COOKIE}=`));
349
+ const edit = verified || draft;
350
+ return { edit, verified, headers, cacheControl: edit ? exports.EDIT_CACHE_CONTROL : null };
351
+ }
352
+ // ------------------------------------------------- five-minute integration ---
353
+ /**
354
+ * Where the env-driven helpers read their settings (M6): the one pair of
355
+ * names the whole product uses (the MCP server, `capa-codegen`, `capa
356
+ * persist`, every curl example in the API docs).
357
+ */
358
+ exports.CAPA_ENV = {
359
+ baseUrl: "CAPA_API_URL",
360
+ apiKey: "CAPA_KEY",
361
+ draftKey: "CAPA_DRAFT_KEY",
362
+ version: "CAPA_API_VERSION",
363
+ };
364
+ /** Older names that still work, read only when the name above is unset. */
365
+ exports.CAPA_ENV_ALIASES = {
366
+ CAPA_API_URL: "CAPA_BASE_URL",
367
+ CAPA_KEY: "CAPA_API_KEY",
368
+ };
369
+ exports.DEFAULT_API_VERSION = "2026-10-01";
370
+ function readOne(name) {
371
+ const env = globalThis.process?.env;
372
+ const value = env?.[name];
373
+ return value === undefined || value === "" ? undefined : value;
374
+ }
375
+ function readEnv(name) {
376
+ const alias = exports.CAPA_ENV_ALIASES[name];
377
+ return readOne(name) ?? (alias ? readOne(alias) : undefined);
378
+ }
379
+ function requireEnv(name) {
380
+ const value = readEnv(name);
381
+ if (value)
382
+ return value;
383
+ const alias = exports.CAPA_ENV_ALIASES[name];
384
+ throw new Error(`@capacms/sdk/nextjs: ${name} is not set${alias ? ` (${alias} also works)` : ""}.`);
385
+ }
386
+ /**
387
+ * A client config from env, `key` naming the env var for the key. A setting
388
+ * `config` gives is used as given and its env var is never read, so a full
389
+ * `config` needs no env at all.
390
+ */
391
+ function envConfig(config, key) {
392
+ return {
393
+ ...config,
394
+ baseUrl: config?.baseUrl ?? requireEnv(exports.CAPA_ENV.baseUrl),
395
+ apiKey: config?.apiKey ?? requireEnv(key),
396
+ version: config?.version ?? readEnv(exports.CAPA_ENV.version) ?? exports.DEFAULT_API_VERSION,
397
+ };
398
+ }
399
+ /** The published or the draft config from env; the draft key must be a `cap_` key. */
400
+ function modeConfig(config, draft) {
401
+ if (!draft)
402
+ return envConfig(config, exports.CAPA_ENV.apiKey);
403
+ const settings = envConfig(config, exports.CAPA_ENV.draftKey);
404
+ (0, key_family_1.requireDraftKey)(settings.apiKey, config?.apiKey !== undefined ? "config.apiKey" : exports.CAPA_ENV.draftKey);
405
+ return settings;
406
+ }
407
+ /**
408
+ * The published-key client from env: what verifying a token needs. `overrides`
409
+ * win over env, and pass codegen's `CapaQuery` to type the builder:
410
+ * `getPublishedClient<CapaQuery>()`.
411
+ */
412
+ function getPublishedClient(overrides = {}) {
413
+ return (0, next_1.createClient)(envConfig(overrides, exports.CAPA_ENV.apiKey));
414
+ }
415
+ /**
416
+ * The client for this request, from env: the draft key under draft mode,
417
+ * otherwise the published key, and `editMode` worked out for you.
418
+ *
419
+ * import { draftMode, headers } from "next/headers";
420
+ * const capa = await getCapaClient<CapaQuery>({ draftMode, headers });
421
+ * const { data } = await capa.graphql.query(selection, { tags: tagsFor({ namespace: "articles" }) });
422
+ *
423
+ * `CapaQuery` (from `capa-codegen --graphql`) types the builder, as with
424
+ * `createClient`; leave it out for an untyped client. `config` wins over env.
425
+ *
426
+ * Its GraphQL reads, a document or the builder, are kept in Next's data cache
427
+ * exactly as `graphql()` keeps them, each under the `tags` and `revalidate`
428
+ * it is called with: a published read with no errors, never a draft or an
429
+ * edit-mode page. Its REST reads are sent as `createClient` sends them.
430
+ */
431
+ async function getCapaClient(input) {
432
+ const draft = (await input.draftMode()).isEnabled === true;
433
+ const edit = await editMode({ draftMode: input.draftMode, headers: input.headers });
434
+ const settings = modeConfig(input.config, draft);
435
+ const client = (0, next_1.createClient)({ editMode: edit, ...settings });
436
+ const read = { draft, edit, settings, given: input.unstable_cache };
437
+ const graphql = (0, client_1.graphqlClient)((document, variables, options = {}) => {
438
+ const { tags, revalidate, ...call } = options;
439
+ return readInNext(read, document, variables, call, { tags, revalidate });
440
+ });
441
+ return { ...client, graphql };
442
+ }
443
+ let loadedUnstableCache;
444
+ /**
445
+ * Next's own `unstable_cache`, loaded on first use, or null where `next/cache`
446
+ * cannot be loaded (Next not installed). The package never imports `next/*`
447
+ * at the top, so a Node script can import this file without Next.
448
+ */
449
+ function nextUnstableCache() {
450
+ if (loadedUnstableCache === undefined) {
451
+ try {
452
+ loadedUnstableCache = require("next/cache").unstable_cache ?? null;
453
+ }
454
+ catch {
455
+ loadedUnstableCache = null;
456
+ }
457
+ }
458
+ return loadedUnstableCache;
459
+ }
460
+ /** Thrown inside `unstable_cache` so that a result with errors is never stored; caught at once. */
461
+ class NotCached extends Error {
462
+ result;
463
+ constructor(result) {
464
+ super(`@capacms/sdk/nextjs: this GraphQL read answered with errors, so it was not cached: ${result.errors[0]?.message ?? "unknown error"}`);
465
+ this.result = result;
466
+ this.name = "NotCached";
467
+ }
468
+ }
469
+ /**
470
+ * What a cached read is keyed by: everything that changes its answer. The key
471
+ * is hashed, so the API key never reaches the cache's index.
472
+ */
473
+ async function cacheKeyOf(settings, document, variables, call) {
474
+ return [
475
+ "@capacms/sdk/nextjs graphql",
476
+ settings.baseUrl,
477
+ await (0, sha256_1.sha256Hex)(settings.apiKey),
478
+ settings.version ?? "",
479
+ String(settings.contract ?? ""),
480
+ settings.schemaChecksum ?? "",
481
+ document,
482
+ JSON.stringify(variables ?? null),
483
+ call.operationName ?? "",
484
+ call.persisted ? "persisted" : "",
485
+ call.method ?? "",
486
+ ];
487
+ }
488
+ async function graphql(document, variables, options = {}) {
489
+ const { tags, revalidate, draft: draftOption, draftMode, headers, config, unstable_cache: given, ...call } = options;
490
+ const draft = draftOption ?? (draftMode ? (await draftMode()).isEnabled === true : false);
491
+ const edit = draft || (headers ? verifiedEdit(await headers()) : false);
492
+ const read = { draft, edit, settings: modeConfig(config, draft), given };
493
+ return readInNext(read, document, variables, call, { tags, revalidate });
494
+ }
495
+ /**
496
+ * One GraphQL read in Next, for `graphql()` and `getCapaClient`. Next's data
497
+ * cache holds only a published read with no errors, and the fetch under it is
498
+ * never cached. A draft, an edit-mode read (its entries carry marks a cached
499
+ * copy would lose) and `revalidate: 0` are not cached.
500
+ */
501
+ async function readInNext(read, document, variables, call, { tags, revalidate }) {
502
+ const { draft, edit, settings, given } = read;
503
+ const baseFetch = settings.fetch ?? globalThis.fetch;
504
+ const cache = given ?? nextUnstableCache();
505
+ if (cache && !draft) {
506
+ if (edit || revalidate === 0) {
507
+ return (0, next_1.createClient)({ editMode: edit, ...settings, fetch: withCache(baseFetch, { revalidate: 0 }) }).graphql(document, variables, call);
508
+ }
509
+ const client = (0, next_1.createClient)({ editMode: false, ...settings, fetch: baseFetch });
510
+ let sent = false;
511
+ const send = async () => {
512
+ sent = true;
513
+ const result = await client.graphql(document, variables, call);
514
+ if (result.errors.length > 0)
515
+ throw new NotCached(result);
516
+ return result;
517
+ };
518
+ const keyParts = await cacheKeyOf(settings, document, variables, call);
519
+ const cacheOptions = { tags: tags ?? [exports.GRAPHQL_TAG] };
520
+ if (revalidate !== undefined)
521
+ cacheOptions.revalidate = revalidate;
522
+ try {
523
+ return await cache(send, keyParts, cacheOptions)();
524
+ }
525
+ catch (error) {
526
+ if (error instanceof NotCached)
527
+ return error.result;
528
+ // Next's own `unstable_cache` throws before it reads anything outside a
529
+ // Next request (a script, a test, a build step with no cache): the read
530
+ // is then sent as below. One passed in throws as it did.
531
+ if (given || sent)
532
+ throw error;
533
+ }
534
+ }
535
+ const client = (0, next_1.createClient)({
536
+ editMode: edit,
537
+ ...settings,
538
+ fetch: draft
539
+ ? withCache(baseFetch, { revalidate: 0 })
540
+ : withCache(baseFetch, { tags: tags ?? [exports.GRAPHQL_TAG], revalidate }),
541
+ });
542
+ return client.graphql(document, variables, call);
543
+ }
544
+ /** Only a path on this site: never `//elsewhere.example` or a full URL. */
545
+ function safeSitePath(value) {
546
+ if (!value || !value.startsWith("/") || value.startsWith("//"))
547
+ return "/";
548
+ return value;
549
+ }
550
+ /**
551
+ * `app/api/capa/preview/route.ts`:
552
+ *
553
+ * import { draftMode } from "next/headers";
554
+ * import { redirect } from "next/navigation";
555
+ * export const GET = createPreviewRoute({ draftMode, redirect });
556
+ *
557
+ * Checks the token with Capa (the site never holds the signing key), turns
558
+ * draft mode on and lands on the entry's page. A bad or expired token lands on
559
+ * the page without draft mode and `?preview=expired`; Capa unreachable gives
560
+ * `?preview=unavailable`.
561
+ */
562
+ function createPreviewRoute(input) {
563
+ return async (request) => {
564
+ const url = new URL(request.url);
565
+ const token = url.searchParams.get("token") ?? url.searchParams.get("capa-preview") ?? "";
566
+ // Reached two ways: directly, or through the middleware's rewrite of a page
567
+ // URL carrying `?capa-preview=`, where Next may hand over the ORIGINAL URL.
568
+ // Then the page itself is the path.
569
+ const path = safeSitePath(url.searchParams.get("path") ?? (url.searchParams.has("capa-preview") ? url.pathname : null));
570
+ let claim = null;
571
+ let failed = false;
572
+ try {
573
+ claim = await (input.client ?? getPublishedClient)().preview(token);
574
+ }
575
+ catch {
576
+ failed = true;
577
+ }
578
+ const draft = await input.draftMode();
579
+ if (!claim) {
580
+ draft.disable?.();
581
+ return input.redirect(`${path}?preview=${failed ? "unavailable" : "expired"}`);
582
+ }
583
+ draft.enable?.();
584
+ await input.onEnable?.();
585
+ return input.redirect(safeSitePath(claim.path ?? path));
586
+ };
587
+ }
588
+ /** `app/api/capa/exit/route.ts`: `export const GET = exitPreviewRoute({ draftMode, redirect });` */
589
+ function exitPreviewRoute(input) {
590
+ return async (request) => {
591
+ (await input.draftMode()).disable?.();
592
+ return input.redirect(safeSitePath(new URL(request.url).searchParams.get("path")));
593
+ };
594
+ }
595
+ /**
596
+ * `middleware.ts` in one line:
597
+ *
598
+ * import { NextResponse } from "next/server";
599
+ * export const middleware = capaMiddleware({ NextResponse });
600
+ *
601
+ * - `?capa-preview=<token>` on any page goes to `previewRoute`, which turns
602
+ * draft mode on and comes back;
603
+ * - `?capa-view=published` renders without the draft cookie, for the editor's
604
+ * Published view;
605
+ * - `?capa-edit=<token>` turns edit mode on (ids and overlay, published data);
606
+ * - every edit-mode response is `private, no-store`.
607
+ */
608
+ function capaMiddleware(input) {
609
+ const previewRoute = input.previewRoute ?? "/api/capa/preview";
610
+ return async (request) => {
611
+ const token = request.nextUrl.searchParams.get("capa-preview");
612
+ if (token) {
613
+ const target = request.nextUrl.clone();
614
+ target.pathname = previewRoute;
615
+ target.search = "";
616
+ target.searchParams.set("token", token);
617
+ target.searchParams.set("path", request.nextUrl.pathname);
618
+ return input.NextResponse.rewrite(target);
619
+ }
620
+ const headers = new Headers(request.headers);
621
+ if (request.nextUrl.searchParams.get("capa-view") === "published") {
622
+ const cookies = request.cookies
623
+ .getAll()
624
+ .filter((cookie) => cookie.name !== exports.DRAFT_COOKIE)
625
+ .map((cookie) => `${cookie.name}=${encodeURIComponent(cookie.value)}`)
626
+ .join("; ");
627
+ if (cookies)
628
+ headers.set("cookie", cookies);
629
+ else
630
+ headers.delete("cookie");
631
+ }
632
+ // The client is built only when a token needs checking, so a missing env
633
+ // value cannot break every page of the site.
634
+ const edit = await resolveEditRequest({ url: request.url, headers }, { preview: (t) => (input.client ?? getPublishedClient)().preview(t) });
635
+ const response = input.NextResponse.next({ request: { headers: edit.headers } });
636
+ if (edit.cacheControl)
637
+ response.headers.set("Cache-Control", edit.cacheControl);
638
+ return response;
639
+ };
640
+ }
@@ -0,0 +1,5 @@
1
+ export interface CapaOverlayProps {
2
+ /** The Capa admin origins allowed to drive the overlay. */
3
+ adminOrigins: string[];
4
+ }
5
+ export declare function CapaOverlay({ adminOrigins }: CapaOverlayProps): null;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.CapaOverlay = CapaOverlay;
5
+ /**
6
+ * `<CapaOverlay />`: Capa's live preview overlay as one Next.js client
7
+ * component (M6).
8
+ *
9
+ * // app/layout.tsx (a server component)
10
+ * import { CapaOverlay } from "@capacms/sdk/nextjs/overlay";
11
+ * {edit ? <CapaOverlay adminOrigins={["https://app.capacms.com"]} /> : null}
12
+ *
13
+ * Render it only in edit mode (`editMode()` from `@capacms/sdk/nextjs`), so a
14
+ * visitor never downloads it. Inside the Capa editor it outlines the focused
15
+ * field, reports clicks back, and on a save re-renders the page with
16
+ * `router.refresh()`, keeping the scroll position. Outside a Capa frame it does
17
+ * nothing at all.
18
+ *
19
+ * Its own entry point, apart from `@capacms/sdk/nextjs`, because it imports
20
+ * `react` and `next/navigation` and is a client module: the server helpers must
21
+ * stay free of both.
22
+ */
23
+ const react_1 = require("react");
24
+ const navigation_1 = require("next/navigation");
25
+ const overlay_1 = require("../overlay");
26
+ function CapaOverlay({ adminOrigins }) {
27
+ const router = (0, navigation_1.useRouter)();
28
+ // A string, so a new array with the same origins does not restart it.
29
+ const origins = adminOrigins.join(",");
30
+ (0, react_1.useEffect)(() => (0, overlay_1.startOverlay)({
31
+ adminOrigins: origins.split(",").filter(Boolean),
32
+ onRefresh: () => router.refresh(),
33
+ }), [origins, router]);
34
+ return null;
35
+ }
package/package.json CHANGED
@@ -1,16 +1,9 @@
1
1
  {
2
2
  "name": "@capacms/sdk",
3
- "version": "1.0.0-next.3",
3
+ "version": "1.0.0-next.6",
4
+ "description": "The TypeScript SDK for Capa's content API: REST and GraphQL reads typed from your models, Next.js caching and live preview, and codegen.",
4
5
  "license": "UNLICENSED",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/ZVN-DEV/capa-cms-future.git",
8
- "directory": "packages/sdk"
9
- },
10
- "homepage": "https://github.com/ZVN-DEV/capa-cms-future/tree/main/packages/sdk#readme",
11
- "bugs": {
12
- "url": "https://github.com/ZVN-DEV/capa-cms-future/issues"
13
- },
6
+ "homepage": "https://docs.capacms.com/api",
14
7
  "engines": {
15
8
  "node": ">=18"
16
9
  },
@@ -18,7 +11,8 @@
18
11
  "files": [
19
12
  "dist",
20
13
  "bin",
21
- "README.md"
14
+ "README.md",
15
+ "CHANGELOG.md"
22
16
  ],
23
17
  "publishConfig": {
24
18
  "access": "public",
@@ -43,7 +37,11 @@
43
37
  "types": "./dist/overlay/index.d.ts",
44
38
  "default": "./dist/overlay/index.js"
45
39
  },
46
- "./package.json": "./package.json"
40
+ "./package.json": "./package.json",
41
+ "./nextjs/overlay": {
42
+ "types": "./dist/nextjs/overlay.d.ts",
43
+ "default": "./dist/nextjs/overlay.js"
44
+ }
47
45
  },
48
46
  "typesVersions": {
49
47
  "*": {
@@ -56,21 +54,45 @@
56
54
  "overlay": [
57
55
  "dist/overlay/index.d.ts"
58
56
  ],
57
+ "nextjs/overlay": [
58
+ "dist/nextjs/overlay.d.ts"
59
+ ],
59
60
  "*": [
60
61
  "dist/index.d.ts"
61
62
  ]
62
63
  }
63
64
  },
64
65
  "bin": {
66
+ "capa": "bin/capa.js",
65
67
  "capa-codegen": "bin/capa-codegen.js"
66
68
  },
69
+ "peerDependencies": {
70
+ "graphql": "^16.9.0",
71
+ "next": ">=14",
72
+ "react": ">=18"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "graphql": {
76
+ "optional": true
77
+ },
78
+ "next": {
79
+ "optional": true
80
+ },
81
+ "react": {
82
+ "optional": true
83
+ }
84
+ },
67
85
  "devDependencies": {
86
+ "graphql": "^16.9.0",
68
87
  "typescript": "^5.5.0",
88
+ "@types/react": "19.0.1",
89
+ "next": "15.5.9",
90
+ "react": "19.2.8",
69
91
  "@capa/shared": "0.1.0"
70
92
  },
71
93
  "scripts": {
72
94
  "build": "tsc -p tsconfig.json",
73
95
  "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"
96
+ "test": "tsc -p tsconfig.json && node --test test/comments.test.js 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 test/field-names.test.js test/next-graphql.test.js test/graphql-codegen.test.js test/graphql-contract.test.js test/readme-snippets.test.js test/nextjs-app.test.js test/package-json.test.js test/changelog.test.js test/published-docs.test.js test/builder-messages.test.js && tsc -p test/types/tsconfig.consumer.json --noEmit && tsc -p test/types/tsconfig.nonstrict.json --noEmit"
75
97
  }
76
98
  }