@notion-headless-cms/core 0.1.2 → 0.2.0

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 (45) hide show
  1. package/README.md +69 -175
  2. package/dist/cache/memory.d.mts +52 -0
  3. package/dist/cache/memory.mjs +115 -0
  4. package/dist/cache/memory.mjs.map +1 -0
  5. package/dist/cache/{noop.d.ts → noop.d.mts} +5 -3
  6. package/dist/cache/noop.mjs +44 -0
  7. package/dist/cache/noop.mjs.map +1 -0
  8. package/dist/cache-Av7HRw_s.d.mts +205 -0
  9. package/dist/content-BrwEY2_p.d.mts +53 -0
  10. package/dist/errors.d.mts +32 -0
  11. package/dist/errors.mjs +24 -0
  12. package/dist/errors.mjs.map +1 -0
  13. package/dist/hooks-DCSAQkST.d.mts +60 -0
  14. package/dist/hooks.d.mts +2 -0
  15. package/dist/hooks.mjs +75 -0
  16. package/dist/hooks.mjs.map +1 -0
  17. package/dist/index.d.mts +349 -0
  18. package/dist/index.mjs +672 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/package.json +17 -16
  21. package/dist/cache/memory.d.ts +0 -54
  22. package/dist/cache/memory.js +0 -15
  23. package/dist/cache/memory.js.map +0 -1
  24. package/dist/cache/noop.js +0 -9
  25. package/dist/cache/noop.js.map +0 -1
  26. package/dist/cache-DvbyemBK.d.ts +0 -33
  27. package/dist/chunk-4KGKWKKI.js +0 -80
  28. package/dist/chunk-4KGKWKKI.js.map +0 -1
  29. package/dist/chunk-6DG63XUF.js +0 -42
  30. package/dist/chunk-6DG63XUF.js.map +0 -1
  31. package/dist/chunk-6LHROEPI.js +0 -104
  32. package/dist/chunk-6LHROEPI.js.map +0 -1
  33. package/dist/chunk-V6ML4QE5.js +0 -26
  34. package/dist/chunk-V6ML4QE5.js.map +0 -1
  35. package/dist/content-Biqf0l_o.d.ts +0 -51
  36. package/dist/errors.d.ts +0 -30
  37. package/dist/errors.js +0 -11
  38. package/dist/errors.js.map +0 -1
  39. package/dist/hooks-B83RUclt.d.ts +0 -41
  40. package/dist/hooks.d.ts +0 -2
  41. package/dist/hooks.js +0 -9
  42. package/dist/hooks.js.map +0 -1
  43. package/dist/index.d.ts +0 -278
  44. package/dist/index.js +0 -598
  45. package/dist/index.js.map +0 -1
package/dist/index.mjs ADDED
@@ -0,0 +1,672 @@
1
+ import { memoryCache, memoryDocumentCache, memoryImageCache } from "./cache/memory.mjs";
2
+ import { noopDocumentCache, noopImageCache } from "./cache/noop.mjs";
3
+ import { CMSError, isCMSError, isCMSErrorInNamespace } from "./errors.mjs";
4
+ import { mergeHooks, mergeLoggers } from "./hooks.mjs";
5
+ //#region src/cache.ts
6
+ /** 文字列をSHA-256でハッシュ化し、16進数文字列として返す。画像キーの生成に使用。 */
7
+ async function sha256Hex(input) {
8
+ const data = new TextEncoder().encode(input);
9
+ const hash = await crypto.subtle.digest("SHA-256", data);
10
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
11
+ }
12
+ /**
13
+ * キャッシュが有効期限切れかどうかを判定する。
14
+ * ttlMs が未指定の場合は常に false(無期限有効)を返す。
15
+ */
16
+ function isStale(cachedAt, ttlMs) {
17
+ if (ttlMs === void 0) return false;
18
+ return Date.now() - cachedAt > ttlMs;
19
+ }
20
+ //#endregion
21
+ //#region src/image.ts
22
+ /** レスポンスヘッダまたはURLの拡張子からContent-Typeを推測する。 */
23
+ function inferContentType(url, responseContentType) {
24
+ if (responseContentType?.startsWith("image/")) return responseContentType.split(";")[0].trim();
25
+ if (url.includes(".png")) return "image/png";
26
+ if (url.includes(".gif")) return "image/gif";
27
+ if (url.includes(".webp")) return "image/webp";
28
+ return "image/jpeg";
29
+ }
30
+ /**
31
+ * Notion画像URLをfetchしてImageCacheAdapterにキャッシュし、プロキシURL を返す。
32
+ * 既存キャッシュがあれば再fetchしない。
33
+ */
34
+ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase) {
35
+ const hash = await sha256Hex(notionUrl);
36
+ const proxyUrl = `${imageProxyBase}/${hash}`;
37
+ if (await cache.get(hash)) return proxyUrl;
38
+ try {
39
+ const response = await fetch(notionUrl, { signal: AbortSignal.timeout(1e4) });
40
+ if (!response.ok) throw new CMSError({
41
+ code: "cache/image_fetch_failed",
42
+ message: `Failed to fetch Notion image: HTTP ${response.status}`,
43
+ context: {
44
+ operation: "fetchAndCacheImage",
45
+ notionUrl,
46
+ httpStatus: response.status
47
+ }
48
+ });
49
+ const data = await response.arrayBuffer();
50
+ const contentType = inferContentType(notionUrl, response.headers.get("content-type"));
51
+ await cache.set(hash, data, contentType);
52
+ } catch (err) {
53
+ if (isCMSError(err)) throw err;
54
+ throw new CMSError({
55
+ code: "cache/io_failed",
56
+ message: "Failed to fetch or cache Notion image.",
57
+ cause: err,
58
+ context: {
59
+ operation: "fetchAndCacheImage",
60
+ notionUrl
61
+ }
62
+ });
63
+ }
64
+ return proxyUrl;
65
+ }
66
+ /** ImageCacheAdapter と imageProxyBase から cacheImage 関数を構築するファクトリ。 */
67
+ function buildCacheImageFn(cache, imageProxyBase) {
68
+ return (notionUrl) => fetchAndCacheImage(cache, notionUrl, imageProxyBase);
69
+ }
70
+ //#endregion
71
+ //#region src/rendering.ts
72
+ /**
73
+ * コンテンツアイテムをソースから Markdown ロード → blocks 生成 → HTML レンダリング
74
+ * → フック適用まで実行し、キャッシュ保存用の `CachedItem` を返す。
75
+ */
76
+ async function buildCachedItem(item, ctx) {
77
+ const start = Date.now();
78
+ ctx.logger?.info?.("コンテンツのレンダリング開始", {
79
+ slug: item.slug,
80
+ pageId: item.id
81
+ });
82
+ ctx.hooks.onRenderStart?.(item.slug);
83
+ let markdown;
84
+ try {
85
+ markdown = await ctx.source.loadMarkdown(item);
86
+ } catch (err) {
87
+ if (isCMSError(err)) throw err;
88
+ throw new CMSError({
89
+ code: "source/load_markdown_failed",
90
+ message: "Failed to load markdown from source.",
91
+ cause: err,
92
+ context: {
93
+ operation: "buildCachedItem:loadMarkdown",
94
+ pageId: item.id,
95
+ slug: item.slug
96
+ }
97
+ });
98
+ }
99
+ let blocks = [];
100
+ try {
101
+ blocks = await ctx.source.loadBlocks(item);
102
+ } catch (err) {
103
+ ctx.logger?.warn?.("loadBlocks に失敗したため raw フォールバック", {
104
+ slug: item.slug,
105
+ error: err instanceof Error ? err.message : String(err)
106
+ });
107
+ blocks = [];
108
+ }
109
+ const cacheImage = ctx.hasImageCache ? buildCacheImageFn(ctx.imgCache, ctx.imageProxyBase) : void 0;
110
+ let html;
111
+ const rendererFn = ctx.rendererFn ?? await loadDefaultRenderer();
112
+ try {
113
+ html = await rendererFn(markdown, {
114
+ imageProxyBase: ctx.imageProxyBase,
115
+ cacheImage,
116
+ remarkPlugins: ctx.contentConfig?.remarkPlugins,
117
+ rehypePlugins: ctx.contentConfig?.rehypePlugins
118
+ });
119
+ } catch (err) {
120
+ if (isCMSError(err)) throw err;
121
+ throw new CMSError({
122
+ code: "renderer/failed",
123
+ message: "Failed to render markdown.",
124
+ cause: err,
125
+ context: {
126
+ operation: "buildCachedItem:renderMarkdown",
127
+ pageId: item.id,
128
+ slug: item.slug
129
+ }
130
+ });
131
+ }
132
+ if (ctx.hooks.afterRender) html = await ctx.hooks.afterRender(html, item);
133
+ let result = {
134
+ html,
135
+ blocks,
136
+ markdown,
137
+ item,
138
+ notionUpdatedAt: ctx.source.getLastModified(item),
139
+ cachedAt: Date.now()
140
+ };
141
+ if (ctx.hooks.beforeCache) result = await ctx.hooks.beforeCache(result);
142
+ const durationMs = Date.now() - start;
143
+ ctx.logger?.info?.("コンテンツのレンダリング完了", {
144
+ slug: item.slug,
145
+ durationMs
146
+ });
147
+ ctx.hooks.onRenderEnd?.(item.slug, durationMs);
148
+ return result;
149
+ }
150
+ /**
151
+ * renderer オプション未指定時のフォールバック。
152
+ * @notion-headless-cms/renderer を動的 import する。
153
+ * adapter-cloudflare / adapter-node は renderer を明示注入するためこのパスは通らない。
154
+ */
155
+ async function loadDefaultRenderer() {
156
+ try {
157
+ return (await import("@notion-headless-cms/renderer")).renderMarkdown;
158
+ } catch (err) {
159
+ throw new CMSError({
160
+ code: "renderer/failed",
161
+ message: "renderer オプションが未指定で @notion-headless-cms/renderer が見つかりません。 createCMS({ renderer }) でレンダラーを注入するか、@notion-headless-cms/renderer をインストールしてください。",
162
+ cause: err,
163
+ context: { operation: "loadDefaultRenderer" }
164
+ });
165
+ }
166
+ }
167
+ //#endregion
168
+ //#region src/retry.ts
169
+ const DEFAULT_RETRY_CONFIG = {
170
+ retryOn: [
171
+ 429,
172
+ 502,
173
+ 503
174
+ ],
175
+ maxRetries: 4,
176
+ baseDelayMs: 1e3,
177
+ jitter: true
178
+ };
179
+ /** 指数バックオフ(オプションでジッター付き)でリトライする。retryOn に含まれる HTTP エラーのみ対象。 */
180
+ async function withRetry(fn, config) {
181
+ let lastError;
182
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) try {
183
+ return await fn();
184
+ } catch (err) {
185
+ const status = err.status;
186
+ if (status === void 0 || !config.retryOn.includes(status)) throw err;
187
+ lastError = err;
188
+ if (attempt < config.maxRetries) {
189
+ config.onRetry?.(attempt + 1, status);
190
+ const jitterFactor = config.jitter !== false ? .5 + Math.random() * .5 : 1;
191
+ const delay = config.baseDelayMs * 2 ** attempt * jitterFactor;
192
+ await new Promise((resolve) => setTimeout(resolve, delay));
193
+ }
194
+ }
195
+ throw lastError;
196
+ }
197
+ //#endregion
198
+ //#region src/collection.ts
199
+ /**
200
+ * コレクション別キャッシュキーを生成する。
201
+ * item: `{collection}:{slug}` / list: `{collection}`
202
+ */
203
+ function collectionKey(collection, slug) {
204
+ return slug ? `${collection}:${slug}` : collection;
205
+ }
206
+ /** CollectionClient の実装。ユーザーは `createCMS` 経由でインスタンスを受け取る。 */
207
+ var CollectionClientImpl = class {
208
+ constructor(ctx) {
209
+ this.ctx = ctx;
210
+ }
211
+ async getItem(slug) {
212
+ const cached = await this.ctx.docCache.getItem(slug);
213
+ if (cached && !isStale(cached.cachedAt, this.ctx.ttlMs)) {
214
+ this.ctx.hooks.onCacheHit?.(slug, cached);
215
+ return this.attachContent(cached.item, cached);
216
+ }
217
+ this.ctx.hooks.onCacheMiss?.(slug);
218
+ const item = await this.findRaw(slug);
219
+ if (!item) return null;
220
+ const entry = await buildCachedItem(item, this.ctx.render);
221
+ const save = this.ctx.docCache.setItem(slug, entry);
222
+ if (this.ctx.waitUntil) this.ctx.waitUntil(save);
223
+ else await save;
224
+ return this.attachContent(entry.item, entry);
225
+ }
226
+ async getList(opts) {
227
+ return applyGetListOptions(await this.fetchList(), opts);
228
+ }
229
+ async getStaticParams() {
230
+ return (await this.fetchList()).map((item) => ({ slug: item.slug }));
231
+ }
232
+ async getStaticPaths() {
233
+ return (await this.fetchList()).map((item) => item.slug);
234
+ }
235
+ async adjacent(slug, opts) {
236
+ const items = applyGetListOptions(await this.fetchList(), { sort: opts?.sort });
237
+ const index = items.findIndex((it) => it.slug === slug);
238
+ if (index === -1) return {
239
+ prev: null,
240
+ next: null
241
+ };
242
+ return {
243
+ prev: index > 0 ? items[index - 1] ?? null : null,
244
+ next: index < items.length - 1 ? items[index + 1] ?? null : null
245
+ };
246
+ }
247
+ async revalidate(scope) {
248
+ if (!this.ctx.docCache.invalidate) return;
249
+ if (scope === void 0 || scope === "all") await this.ctx.docCache.invalidate({ collection: this.ctx.collection });
250
+ else await this.ctx.docCache.invalidate({
251
+ collection: this.ctx.collection,
252
+ slug: scope.slug
253
+ });
254
+ }
255
+ async prefetch(opts) {
256
+ const items = await this.fetchListRaw();
257
+ const concurrency = opts?.concurrency ?? this.ctx.maxConcurrent;
258
+ let ok = 0;
259
+ let failed = 0;
260
+ for (let i = 0; i < items.length; i += concurrency) {
261
+ const chunk = items.slice(i, i + concurrency);
262
+ await Promise.all(chunk.map(async (item) => {
263
+ try {
264
+ const rendered = await buildCachedItem(item, this.ctx.render);
265
+ await this.ctx.docCache.setItem(item.slug, rendered);
266
+ ok++;
267
+ } catch (err) {
268
+ failed++;
269
+ this.ctx.logger?.warn?.("prefetch: アイテムの事前レンダリングに失敗", {
270
+ slug: item.slug,
271
+ pageId: item.id,
272
+ error: err instanceof Error ? err.message : String(err)
273
+ });
274
+ }
275
+ }));
276
+ opts?.onProgress?.(Math.min(i + concurrency, items.length), items.length);
277
+ }
278
+ await this.ctx.docCache.setList({
279
+ items,
280
+ cachedAt: Date.now()
281
+ });
282
+ return {
283
+ ok,
284
+ failed
285
+ };
286
+ }
287
+ attachContent(item, cached) {
288
+ const ctx = this.ctx;
289
+ let blocksCache;
290
+ let htmlCache = cached.html;
291
+ let markdownCache;
292
+ const content = {
293
+ get blocks() {
294
+ if (!blocksCache) blocksCache = [{
295
+ type: "raw",
296
+ html: cached.html
297
+ }];
298
+ return blocksCache;
299
+ },
300
+ async html() {
301
+ if (htmlCache !== void 0) return htmlCache;
302
+ htmlCache = cached.html;
303
+ return htmlCache;
304
+ },
305
+ async markdown() {
306
+ if (markdownCache !== void 0) return markdownCache;
307
+ markdownCache = await ctx.source.loadMarkdown(item);
308
+ return markdownCache;
309
+ }
310
+ };
311
+ const maybeBlocks = cached.blocks;
312
+ if (maybeBlocks) blocksCache = maybeBlocks;
313
+ return Object.assign(Object.create(null), item, { content });
314
+ }
315
+ async fetchList() {
316
+ const cached = await this.ctx.docCache.getList();
317
+ if (cached && !isStale(cached.cachedAt, this.ctx.ttlMs)) {
318
+ this.ctx.hooks.onListCacheHit?.(cached.items, cached.cachedAt);
319
+ return cached.items;
320
+ }
321
+ this.ctx.hooks.onListCacheMiss?.();
322
+ const items = await this.fetchListRaw();
323
+ const cachedAt = Date.now();
324
+ const save = this.ctx.docCache.setList({
325
+ items,
326
+ cachedAt
327
+ });
328
+ if (this.ctx.waitUntil) this.ctx.waitUntil(save);
329
+ else await save;
330
+ return items;
331
+ }
332
+ fetchListRaw() {
333
+ return withRetry(() => this.ctx.source.list({ publishedStatuses: this.ctx.publishedStatuses.length > 0 ? this.ctx.publishedStatuses : void 0 }), {
334
+ ...this.ctx.retryConfig,
335
+ onRetry: (attempt, status) => {
336
+ this.ctx.logger?.warn?.("getList() リトライ中", {
337
+ attempt,
338
+ status
339
+ });
340
+ }
341
+ });
342
+ }
343
+ async findRaw(slug) {
344
+ const item = await withRetry(() => this.ctx.source.findBySlug(slug), {
345
+ ...this.ctx.retryConfig,
346
+ onRetry: (attempt, status) => {
347
+ this.ctx.logger?.warn?.("getItem() リトライ中", {
348
+ attempt,
349
+ status,
350
+ slug
351
+ });
352
+ }
353
+ });
354
+ if (!item) return null;
355
+ if (this.ctx.accessibleStatuses.length > 0 && (!item.status || !this.ctx.accessibleStatuses.includes(item.status))) return null;
356
+ return item;
357
+ }
358
+ };
359
+ function applyGetListOptions(items, opts) {
360
+ if (!opts) return items;
361
+ let result = items;
362
+ if (opts.statuses && opts.statuses.length > 0) {
363
+ const allow = new Set(opts.statuses);
364
+ result = result.filter((it) => it.status !== void 0 && allow.has(it.status));
365
+ }
366
+ if (opts.tag) {
367
+ const tag = opts.tag;
368
+ result = result.filter((it) => {
369
+ const tags = it.tags;
370
+ return Array.isArray(tags) && tags.includes(tag);
371
+ });
372
+ }
373
+ if (opts.where) {
374
+ const where = opts.where;
375
+ result = result.filter((it) => Object.entries(where).every(([key, value]) => it[key] === value));
376
+ }
377
+ if (opts.sort) result = [...result].sort(makeComparator(opts.sort));
378
+ const skip = opts.skip ?? 0;
379
+ const limit = opts.limit;
380
+ if (skip > 0 || limit !== void 0) result = result.slice(skip, limit !== void 0 ? skip + limit : void 0);
381
+ return result;
382
+ }
383
+ function makeComparator(sort) {
384
+ const by = sort.by;
385
+ const dir = sort.direction === "asc" ? 1 : -1;
386
+ return (a, b) => {
387
+ const av = a[by];
388
+ const bv = b[by];
389
+ if (av === bv) return 0;
390
+ if (av === void 0) return 1;
391
+ if (bv === void 0) return -1;
392
+ return av > bv ? dir : -dir;
393
+ };
394
+ }
395
+ //#endregion
396
+ //#region src/handler.ts
397
+ const DEFAULT_OPTS = {
398
+ basePath: "/api/cms",
399
+ imagesPath: "/images",
400
+ revalidatePath: "/revalidate"
401
+ };
402
+ /**
403
+ * Web Standard な Request → Response ルーター。
404
+ * Next.js / React Router / Hono / Cloudflare Workers いずれでも使える。
405
+ *
406
+ * ルート:
407
+ * - GET `{basePath}/images/:hash` — 画像プロキシ
408
+ * - POST `{basePath}/revalidate` — Webhook 受信 + $revalidate()
409
+ */
410
+ function createHandler(adapter, opts = {}) {
411
+ const basePath = trimTrailingSlash(opts.basePath ?? DEFAULT_OPTS.basePath);
412
+ const imagesPath = opts.imagesPath ?? DEFAULT_OPTS.imagesPath;
413
+ const revalidatePath = opts.revalidatePath ?? DEFAULT_OPTS.revalidatePath;
414
+ return async (req) => {
415
+ const path = new URL(req.url).pathname;
416
+ if (!path.startsWith(basePath)) return new Response("Not Found", { status: 404 });
417
+ const rel = path.slice(basePath.length) || "/";
418
+ if (req.method === "GET" && rel.startsWith(`${imagesPath}/`)) {
419
+ const hash = rel.slice(imagesPath.length + 1);
420
+ if (!hash) return new Response("Bad Request", { status: 400 });
421
+ const object = await adapter.imageCache.get(hash);
422
+ if (!object) return new Response("Not Found", { status: 404 });
423
+ const headers = new Headers();
424
+ if (object.contentType) headers.set("content-type", object.contentType);
425
+ headers.set("cache-control", "public, max-age=31536000, immutable");
426
+ return new Response(object.data, { headers });
427
+ }
428
+ if (req.method === "POST" && rel === revalidatePath) {
429
+ const scope = await adapter.parseWebhook(req, opts.webhookSecret);
430
+ if (!scope) return new Response(JSON.stringify({
431
+ ok: false,
432
+ reason: "invalid"
433
+ }), {
434
+ status: 400,
435
+ headers: { "content-type": "application/json" }
436
+ });
437
+ await adapter.revalidate(scope);
438
+ return new Response(JSON.stringify({
439
+ ok: true,
440
+ scope
441
+ }), {
442
+ status: 200,
443
+ headers: { "content-type": "application/json" }
444
+ });
445
+ }
446
+ return new Response("Not Found", { status: 404 });
447
+ };
448
+ }
449
+ function trimTrailingSlash(s) {
450
+ return s.endsWith("/") ? s.slice(0, -1) : s;
451
+ }
452
+ //#endregion
453
+ //#region src/preset-node.ts
454
+ /**
455
+ * Node.js ランタイム向けの `createCMS` オプションプリセット。
456
+ * メモリキャッシュをデフォルト有効にした `{ cache, renderer }` を返す。
457
+ *
458
+ * @example
459
+ * import { createCMS, nodePreset } from "@notion-headless-cms/core";
460
+ * import { cmsDataSources } from "./generated/cms-schema";
461
+ *
462
+ * const cms = createCMS({
463
+ * ...nodePreset({ ttlMs: 5 * 60_000 }),
464
+ * dataSources: cmsDataSources,
465
+ * });
466
+ */
467
+ function nodePreset(opts = {}) {
468
+ if (opts.cache === "disabled") return {
469
+ cache: void 0,
470
+ renderer: opts.renderer
471
+ };
472
+ return {
473
+ cache: opts.cache ?? {
474
+ document: memoryDocumentCache(),
475
+ image: memoryImageCache(),
476
+ ttlMs: opts.ttlMs
477
+ },
478
+ renderer: opts.renderer
479
+ };
480
+ }
481
+ //#endregion
482
+ //#region src/cms.ts
483
+ const DEFAULT_IMAGE_PROXY_BASE = "/api/images";
484
+ function resolveDocumentCache(cache) {
485
+ if (!cache || cache === "disabled" || !cache.document) return noopDocumentCache();
486
+ return cache.document;
487
+ }
488
+ function resolveImageCache(cache) {
489
+ if (!cache || cache === "disabled" || !cache.image) return noopImageCache();
490
+ return cache.image;
491
+ }
492
+ function resolveTtl(cache) {
493
+ if (!cache || cache === "disabled") return void 0;
494
+ return cache.ttlMs;
495
+ }
496
+ function hasImageCacheConfigured(cache) {
497
+ if (!cache || cache === "disabled") return false;
498
+ return !!cache.image;
499
+ }
500
+ /**
501
+ * `{collection}:{slug}` キー空間で動作するコレクション別キャッシュビューを生成する。
502
+ * 単一の `DocumentCacheAdapter` に複数コレクションを同居させるためのアダプタ。
503
+ */
504
+ function scopeDocumentCache(base, collection) {
505
+ const itemKey = (slug) => `${collection}:${slug}`;
506
+ const listKey = collection;
507
+ return {
508
+ name: `${base.name}@${collection}`,
509
+ async getList() {
510
+ const anyBase = base;
511
+ if (typeof anyBase.getListByKey === "function") return anyBase.getListByKey(listKey);
512
+ return base.getList();
513
+ },
514
+ async setList(data) {
515
+ const anyBase = base;
516
+ if (typeof anyBase.setListByKey === "function") return anyBase.setListByKey(listKey, data);
517
+ return base.setList(data);
518
+ },
519
+ async getItem(slug) {
520
+ const anyBase = base;
521
+ if (typeof anyBase.getItemByKey === "function") return anyBase.getItemByKey(itemKey(slug));
522
+ return base.getItem(slug);
523
+ },
524
+ async setItem(slug, data) {
525
+ const anyBase = base;
526
+ if (typeof anyBase.setItemByKey === "function") return anyBase.setItemByKey(itemKey(slug), data);
527
+ return base.setItem(slug, data);
528
+ },
529
+ async invalidate(scope) {
530
+ if (!base.invalidate) return;
531
+ if (scope === "all") return base.invalidate({ collection });
532
+ return base.invalidate(scope);
533
+ }
534
+ };
535
+ }
536
+ /**
537
+ * `preset` オプションを解決して `cache` / `renderer` のデフォルトを補完する内部関数。
538
+ * 明示的な `cache` / `renderer` がある場合はそちらが優先される。
539
+ * `preset` 未指定時は opts をそのまま返す(後方互換)。
540
+ */
541
+ function resolvePreset(opts) {
542
+ if (opts.preset === "disabled") return {
543
+ ...opts,
544
+ cache: void 0
545
+ };
546
+ if (opts.preset === "node") {
547
+ const presetResult = nodePreset({ ttlMs: opts.ttlMs });
548
+ return {
549
+ ...opts,
550
+ cache: opts.cache ?? presetResult.cache,
551
+ renderer: opts.renderer ?? presetResult.renderer
552
+ };
553
+ }
554
+ return opts;
555
+ }
556
+ /**
557
+ * 複数の DataSource を束ねた CMS クライアントを生成する。
558
+ *
559
+ * @example
560
+ * // Node.js(preset を使った簡潔な記法)
561
+ * const cms = createCMS({ dataSources: cmsDataSources, preset: "node", ttlMs: 5 * 60_000 });
562
+ *
563
+ * @example
564
+ * // 従来の spread パターン(引き続き動作する)
565
+ * const cms = createCMS({ ...nodePreset({ ttlMs: 5 * 60_000 }), dataSources: cmsDataSources });
566
+ *
567
+ * @example
568
+ * // キャッシュを細かく指定する場合
569
+ * const cms = createCMS({
570
+ * dataSources,
571
+ * cache: { document, image, ttlMs: 60_000 },
572
+ * });
573
+ */
574
+ function createCMS(opts) {
575
+ if (!opts.dataSources || Object.keys(opts.dataSources).length === 0) throw new CMSError({
576
+ code: "core/config_invalid",
577
+ message: "createCMS: dataSources に少なくとも1つのコレクションを指定してください。",
578
+ context: { operation: "createCMS" }
579
+ });
580
+ const resolved = resolvePreset(opts);
581
+ const baseDocCache = resolveDocumentCache(resolved.cache);
582
+ const imgCache = resolveImageCache(resolved.cache);
583
+ const hasImageCache = hasImageCacheConfigured(resolved.cache);
584
+ const ttlMs = resolveTtl(resolved.cache);
585
+ const imageProxyBase = opts.content?.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
586
+ const contentConfig = opts.content;
587
+ const rendererFn = resolved.renderer;
588
+ const waitUntil = opts.waitUntil;
589
+ const logger = mergeLoggers(opts.plugins ?? [], opts.logger);
590
+ const hooks = mergeHooks(opts.plugins ?? [], opts.hooks, logger);
591
+ const maxConcurrent = opts.rateLimiter?.maxConcurrent ?? 3;
592
+ const retryConfig = {
593
+ ...DEFAULT_RETRY_CONFIG,
594
+ ...opts.rateLimiter ?? {}
595
+ };
596
+ const collectionNames = Object.keys(opts.dataSources);
597
+ const collections = {};
598
+ for (const name of collectionNames) {
599
+ const source = opts.dataSources[name];
600
+ collections[name] = new CollectionClientImpl({
601
+ collection: name,
602
+ source,
603
+ docCache: scopeDocumentCache(baseDocCache, name),
604
+ render: {
605
+ source,
606
+ rendererFn,
607
+ imgCache,
608
+ hasImageCache,
609
+ imageProxyBase,
610
+ contentConfig,
611
+ hooks,
612
+ logger
613
+ },
614
+ hooks,
615
+ logger,
616
+ ttlMs,
617
+ publishedStatuses: source.publishedStatuses ? [...source.publishedStatuses] : [],
618
+ accessibleStatuses: source.accessibleStatuses ? [...source.accessibleStatuses] : [],
619
+ retryConfig,
620
+ maxConcurrent,
621
+ waitUntil
622
+ });
623
+ }
624
+ const globalOps = {
625
+ $collections: collectionNames,
626
+ async $revalidate(scope) {
627
+ if (!baseDocCache.invalidate) return;
628
+ await baseDocCache.invalidate(scope ?? "all");
629
+ },
630
+ $handler(handlerOpts) {
631
+ return createHandler({
632
+ imageCache: imgCache,
633
+ parseWebhook: async (req, webhookSecret) => {
634
+ for (const name of collectionNames) {
635
+ const ds = opts.dataSources[name];
636
+ if (ds.parseWebhook) try {
637
+ return await ds.parseWebhook(req.clone(), { secret: webhookSecret });
638
+ } catch (err) {
639
+ logger?.warn?.("parseWebhook 失敗", {
640
+ collection: name,
641
+ error: err instanceof Error ? err.message : String(err)
642
+ });
643
+ }
644
+ }
645
+ try {
646
+ const body = await req.json();
647
+ if (body.slug && body.collection) return {
648
+ collection: body.collection,
649
+ slug: body.slug
650
+ };
651
+ if (body.collection) return { collection: body.collection };
652
+ } catch {}
653
+ return null;
654
+ },
655
+ revalidate: (scope) => globalOps.$revalidate(scope)
656
+ }, handlerOpts);
657
+ },
658
+ $getCachedImage(hash) {
659
+ return imgCache.get(hash);
660
+ }
661
+ };
662
+ return Object.assign(Object.create(null), collections, globalOps);
663
+ }
664
+ //#endregion
665
+ //#region src/types/plugin.ts
666
+ function definePlugin(plugin) {
667
+ return plugin;
668
+ }
669
+ //#endregion
670
+ export { CMSError, CollectionClientImpl, DEFAULT_RETRY_CONFIG, collectionKey, createCMS, createHandler, definePlugin, isCMSError, isCMSErrorInNamespace, isStale, memoryCache, memoryDocumentCache, memoryImageCache, mergeHooks, mergeLoggers, nodePreset, noopDocumentCache, noopImageCache, sha256Hex, withRetry };
671
+
672
+ //# sourceMappingURL=index.mjs.map