@notion-headless-cms/core 0.1.2 → 0.1.3

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 (43) hide show
  1. package/dist/cache/memory.d.mts +52 -0
  2. package/dist/cache/memory.mjs +112 -0
  3. package/dist/cache/memory.mjs.map +1 -0
  4. package/dist/cache/{noop.d.ts → noop.d.mts} +5 -3
  5. package/dist/cache/noop.mjs +44 -0
  6. package/dist/cache/noop.mjs.map +1 -0
  7. package/dist/cache-B-MG4yyg.d.mts +45 -0
  8. package/dist/content-BrwEY2_p.d.mts +53 -0
  9. package/dist/{errors.d.ts → errors.d.mts} +18 -16
  10. package/dist/errors.mjs +24 -0
  11. package/dist/errors.mjs.map +1 -0
  12. package/dist/hooks-DCSAQkST.d.mts +60 -0
  13. package/dist/hooks.d.mts +2 -0
  14. package/dist/hooks.mjs +75 -0
  15. package/dist/hooks.mjs.map +1 -0
  16. package/dist/index.d.mts +449 -0
  17. package/dist/index.mjs +616 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/package.json +17 -16
  20. package/dist/cache/memory.d.ts +0 -54
  21. package/dist/cache/memory.js +0 -15
  22. package/dist/cache/memory.js.map +0 -1
  23. package/dist/cache/noop.js +0 -9
  24. package/dist/cache/noop.js.map +0 -1
  25. package/dist/cache-DvbyemBK.d.ts +0 -33
  26. package/dist/chunk-4KGKWKKI.js +0 -80
  27. package/dist/chunk-4KGKWKKI.js.map +0 -1
  28. package/dist/chunk-6DG63XUF.js +0 -42
  29. package/dist/chunk-6DG63XUF.js.map +0 -1
  30. package/dist/chunk-6LHROEPI.js +0 -104
  31. package/dist/chunk-6LHROEPI.js.map +0 -1
  32. package/dist/chunk-V6ML4QE5.js +0 -26
  33. package/dist/chunk-V6ML4QE5.js.map +0 -1
  34. package/dist/content-Biqf0l_o.d.ts +0 -51
  35. package/dist/errors.js +0 -11
  36. package/dist/errors.js.map +0 -1
  37. package/dist/hooks-B83RUclt.d.ts +0 -41
  38. package/dist/hooks.d.ts +0 -2
  39. package/dist/hooks.js +0 -9
  40. package/dist/hooks.js.map +0 -1
  41. package/dist/index.d.ts +0 -278
  42. package/dist/index.js +0 -598
  43. package/dist/index.js.map +0 -1
package/dist/index.mjs ADDED
@@ -0,0 +1,616 @@
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/cms.ts
454
+ const DEFAULT_IMAGE_PROXY_BASE = "/api/images";
455
+ function resolveDocumentCache(cache) {
456
+ if (!cache || cache === "disabled" || !cache.document) return noopDocumentCache();
457
+ return cache.document;
458
+ }
459
+ function resolveImageCache(cache) {
460
+ if (!cache || cache === "disabled" || !cache.image) return noopImageCache();
461
+ return cache.image;
462
+ }
463
+ function resolveTtl(cache) {
464
+ if (!cache || cache === "disabled") return void 0;
465
+ return cache.ttlMs;
466
+ }
467
+ function hasImageCacheConfigured(cache) {
468
+ if (!cache || cache === "disabled") return false;
469
+ return !!cache.image;
470
+ }
471
+ /**
472
+ * `{collection}:{slug}` キー空間で動作するコレクション別キャッシュビューを生成する。
473
+ * 単一の `DocumentCacheAdapter` に複数コレクションを同居させるためのアダプタ。
474
+ */
475
+ function scopeDocumentCache(base, collection) {
476
+ const itemKey = (slug) => `${collection}:${slug}`;
477
+ const listKey = collection;
478
+ return {
479
+ name: `${base.name}@${collection}`,
480
+ async getList() {
481
+ const anyBase = base;
482
+ if (typeof anyBase.getListByKey === "function") return anyBase.getListByKey(listKey);
483
+ return base.getList();
484
+ },
485
+ async setList(data) {
486
+ const anyBase = base;
487
+ if (typeof anyBase.setListByKey === "function") return anyBase.setListByKey(listKey, data);
488
+ return base.setList(data);
489
+ },
490
+ async getItem(slug) {
491
+ const anyBase = base;
492
+ if (typeof anyBase.getItemByKey === "function") return anyBase.getItemByKey(itemKey(slug));
493
+ return base.getItem(slug);
494
+ },
495
+ async setItem(slug, data) {
496
+ const anyBase = base;
497
+ if (typeof anyBase.setItemByKey === "function") return anyBase.setItemByKey(itemKey(slug), data);
498
+ return base.setItem(slug, data);
499
+ },
500
+ async invalidate(scope) {
501
+ if (!base.invalidate) return;
502
+ if (scope === "all") return base.invalidate({ collection });
503
+ if ("slug" in scope && !("collection" in scope)) return base.invalidate({
504
+ collection,
505
+ slug: scope.slug
506
+ });
507
+ return base.invalidate(scope);
508
+ }
509
+ };
510
+ }
511
+ /**
512
+ * 複数の DataSource を束ねた CMS クライアントを生成する。
513
+ *
514
+ * @example
515
+ * const cms = createCMS({
516
+ * dataSources: {
517
+ * posts: createNotionCollection({ token, databaseId, schema }),
518
+ * },
519
+ * cache: { document, image, ttlMs: 60_000 },
520
+ * });
521
+ * const post = await cms.posts.getItem("my-slug");
522
+ */
523
+ function createCMS(opts) {
524
+ if (!opts.dataSources || Object.keys(opts.dataSources).length === 0) throw new Error("createCMS: dataSources に少なくとも1つのコレクションを指定してください。");
525
+ const baseDocCache = resolveDocumentCache(opts.cache);
526
+ const imgCache = resolveImageCache(opts.cache);
527
+ const hasImageCache = hasImageCacheConfigured(opts.cache);
528
+ const ttlMs = resolveTtl(opts.cache);
529
+ const imageProxyBase = opts.content?.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
530
+ const contentConfig = opts.content;
531
+ const rendererFn = opts.renderer;
532
+ const waitUntil = opts.waitUntil;
533
+ const logger = mergeLoggers(opts.plugins ?? [], opts.logger);
534
+ const hooks = mergeHooks(opts.plugins ?? [], opts.hooks, logger);
535
+ const maxConcurrent = opts.rateLimiter?.maxConcurrent ?? 3;
536
+ const retryConfig = {
537
+ ...DEFAULT_RETRY_CONFIG,
538
+ ...opts.rateLimiter ?? {}
539
+ };
540
+ const collectionNames = Object.keys(opts.dataSources);
541
+ const collections = {};
542
+ for (const name of collectionNames) {
543
+ const source = opts.dataSources[name];
544
+ collections[name] = new CollectionClientImpl({
545
+ collection: name,
546
+ source,
547
+ docCache: scopeDocumentCache(baseDocCache, name),
548
+ render: {
549
+ source,
550
+ rendererFn,
551
+ imgCache,
552
+ hasImageCache,
553
+ imageProxyBase,
554
+ contentConfig,
555
+ hooks,
556
+ logger
557
+ },
558
+ hooks,
559
+ logger,
560
+ ttlMs,
561
+ publishedStatuses: source.publishedStatuses ? [...source.publishedStatuses] : [],
562
+ accessibleStatuses: source.accessibleStatuses ? [...source.accessibleStatuses] : [],
563
+ retryConfig,
564
+ maxConcurrent,
565
+ waitUntil
566
+ });
567
+ }
568
+ const globalOps = {
569
+ $collections: collectionNames,
570
+ async $revalidate(scope) {
571
+ if (!baseDocCache.invalidate) return;
572
+ await baseDocCache.invalidate(scope ?? "all");
573
+ },
574
+ $handler(handlerOpts) {
575
+ return createHandler({
576
+ imageCache: imgCache,
577
+ parseWebhook: async (req, webhookSecret) => {
578
+ for (const name of collectionNames) {
579
+ const ds = opts.dataSources[name];
580
+ if (ds.parseWebhook) try {
581
+ return await ds.parseWebhook(req.clone(), { secret: webhookSecret });
582
+ } catch (err) {
583
+ logger?.warn?.("parseWebhook 失敗", {
584
+ collection: name,
585
+ error: err instanceof Error ? err.message : String(err)
586
+ });
587
+ }
588
+ }
589
+ try {
590
+ const body = await req.json();
591
+ if (body.slug && body.collection) return {
592
+ collection: body.collection,
593
+ slug: body.slug
594
+ };
595
+ if (body.collection) return { collection: body.collection };
596
+ } catch {}
597
+ return null;
598
+ },
599
+ revalidate: (scope) => globalOps.$revalidate(scope)
600
+ }, handlerOpts);
601
+ },
602
+ $getCachedImage(hash) {
603
+ return imgCache.get(hash);
604
+ }
605
+ };
606
+ return Object.assign(Object.create(null), collections, globalOps);
607
+ }
608
+ //#endregion
609
+ //#region src/types/plugin.ts
610
+ function definePlugin(plugin) {
611
+ return plugin;
612
+ }
613
+ //#endregion
614
+ export { CMSError, CollectionClientImpl, DEFAULT_RETRY_CONFIG, collectionKey, createCMS, createHandler, definePlugin, isCMSError, isCMSErrorInNamespace, isStale, memoryCache, memoryDocumentCache, memoryImageCache, mergeHooks, mergeLoggers, noopDocumentCache, noopImageCache, sha256Hex, withRetry };
615
+
616
+ //# sourceMappingURL=index.mjs.map