@notion-headless-cms/core 0.3.25 → 0.5.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.
package/README.md CHANGED
@@ -127,12 +127,12 @@ import { memoryCache } from "@notion-headless-cms/core/cache/memory";
127
127
 
128
128
  ## ランタイム別レシピ
129
129
 
130
- - [Node.js スクリプト](../../docs/recipes/nodejs-script.md)
131
- - [Cloudflare Workers + R2 + KV](../../docs/recipes/cloudflare-workers.md)
132
- - [Next.js App Router](../../docs/recipes/nextjs-app-router.md)
133
- - [カスタムデータソース](../../docs/recipes/custom-source.md)
130
+ - [Node.js スクリプト](../../docs/ja/recipes/nodejs-script.md)
131
+ - [Cloudflare Workers + R2 + KV](../../docs/ja/recipes/cloudflare-workers.md)
132
+ - [Next.js App Router](../../docs/ja/recipes/nextjs-app-router.md)
133
+ - [カスタムデータソース](../../docs/ja/recipes/custom-source.md)
134
134
 
135
135
  ## 詳細ドキュメント
136
136
 
137
- - [クイックスタート](../../docs/quickstart.md)
138
- - [CMS メソッド一覧](../../docs/api/cms-methods.md)
137
+ - [クイックスタート](../../docs/ja/quickstart.md)
138
+ - [CMS メソッド一覧](../../docs/ja/api/cms-methods.md)
@@ -1,4 +1,4 @@
1
- import { t as CacheAdapter } from "../cache-DS81aOcC.mjs";
1
+ import { t as CacheAdapter } from "../cache-v9jTMnYd.mjs";
2
2
 
3
3
  //#region src/cache/memory.d.ts
4
4
  interface MemoryDocumentOptions {
@@ -16,6 +16,8 @@ interface MemoryCacheOptions extends MemoryDocumentOptions, MemoryImageOptions {
16
16
  * インメモリのキャッシュアダプタ。document + image 両方を担当する。
17
17
  * プロセス再起動でクリアされるため、ローカル開発・SSG ビルド・テスト用途。
18
18
  *
19
+ * `stats()` でヒット率・エントリ数・画像合計バイト数を返す。
20
+ *
19
21
  * @example
20
22
  * cache: [memoryCache({ maxItems: 1000 })]
21
23
  */
@@ -16,11 +16,21 @@ var MemoryDocumentOps = class {
16
16
  metas = /* @__PURE__ */ new Map();
17
17
  contents = /* @__PURE__ */ new Map();
18
18
  maxItems;
19
+ /** `cms.stats()` 用のヒット/ミス集計 (list+meta+content の合計)。 */
20
+ hits = 0;
21
+ misses = 0;
19
22
  constructor(options) {
20
23
  this.maxItems = options?.maxItems;
21
24
  }
25
+ /** 保持エントリ数 (list / meta / content の合計)。stats() で使う。 */
26
+ totalEntries() {
27
+ return this.lists.size + this.metas.size + this.contents.size;
28
+ }
22
29
  getList(collection) {
23
- return Promise.resolve(this.lists.get(collection) ?? null);
30
+ const entry = this.lists.get(collection);
31
+ if (entry) this.hits++;
32
+ else this.misses++;
33
+ return Promise.resolve(entry ?? null);
24
34
  }
25
35
  setList(collection, data) {
26
36
  this.lists.set(collection, data);
@@ -29,7 +39,10 @@ var MemoryDocumentOps = class {
29
39
  getMeta(collection, slug) {
30
40
  const key = itemKey(collection, slug);
31
41
  const entry = this.metas.get(key);
32
- if (entry) touch(this.metas, key);
42
+ if (entry) {
43
+ touch(this.metas, key);
44
+ this.hits++;
45
+ } else this.misses++;
33
46
  return Promise.resolve(entry ?? null);
34
47
  }
35
48
  setMeta(collection, slug, data) {
@@ -42,7 +55,10 @@ var MemoryDocumentOps = class {
42
55
  getContent(collection, slug) {
43
56
  const key = itemKey(collection, slug);
44
57
  const entry = this.contents.get(key);
45
- if (entry) touch(this.contents, key);
58
+ if (entry) {
59
+ touch(this.contents, key);
60
+ this.hits++;
61
+ } else this.misses++;
46
62
  return Promise.resolve(entry ?? null);
47
63
  }
48
64
  setContent(collection, slug, data) {
@@ -98,15 +114,28 @@ var MemoryImageOps = class {
98
114
  totalBytes = 0;
99
115
  maxItems;
100
116
  maxSizeBytes;
117
+ /** `cms.stats()` 用のヒット/ミス集計。 */
118
+ hits = 0;
119
+ misses = 0;
101
120
  constructor(options) {
102
121
  this.maxItems = options?.maxItems;
103
122
  this.maxSizeBytes = options?.maxSizeBytes;
104
123
  }
105
124
  get(hash) {
106
125
  const entry = this.store.get(hash);
107
- if (entry) touch(this.store, hash);
126
+ if (entry) {
127
+ touch(this.store, hash);
128
+ this.hits++;
129
+ } else this.misses++;
108
130
  return Promise.resolve(entry ?? null);
109
131
  }
132
+ /** stats() 用に、現在の保持エントリ数と合計バイト数を返す。 */
133
+ snapshot() {
134
+ return {
135
+ entries: this.store.size,
136
+ sizeBytes: this.totalBytes
137
+ };
138
+ }
110
139
  set(hash, data, contentType) {
111
140
  const existing = this.store.get(hash);
112
141
  if (existing) {
@@ -135,15 +164,36 @@ var MemoryImageOps = class {
135
164
  * インメモリのキャッシュアダプタ。document + image 両方を担当する。
136
165
  * プロセス再起動でクリアされるため、ローカル開発・SSG ビルド・テスト用途。
137
166
  *
167
+ * `stats()` でヒット率・エントリ数・画像合計バイト数を返す。
168
+ *
138
169
  * @example
139
170
  * cache: [memoryCache({ maxItems: 1000 })]
140
171
  */
141
172
  function memoryCache(options) {
173
+ const doc = new MemoryDocumentOps(options);
174
+ const img = new MemoryImageOps(options);
142
175
  return {
143
176
  name: "memory",
144
177
  handles: ["document", "image"],
145
- doc: new MemoryDocumentOps(options),
146
- img: new MemoryImageOps(options)
178
+ doc,
179
+ img,
180
+ async stats() {
181
+ const imgSnap = img.snapshot();
182
+ return {
183
+ name: "memory",
184
+ doc: {
185
+ hits: doc.hits,
186
+ misses: doc.misses,
187
+ entries: doc.totalEntries()
188
+ },
189
+ img: {
190
+ hits: img.hits,
191
+ misses: img.misses,
192
+ entries: imgSnap.entries,
193
+ sizeBytes: imgSnap.sizeBytes
194
+ }
195
+ };
196
+ }
147
197
  };
148
198
  }
149
199
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"memory.mjs","names":[],"sources":["../../src/cache/memory.ts"],"sourcesContent":["import type {\n BaseContentItem,\n CacheAdapter,\n CachedItemContent,\n CachedItemList,\n CachedItemMeta,\n DocumentCacheOps,\n ImageCacheOps,\n InvalidateScope,\n StorageBinary,\n} from \"../types/index\";\n\nexport interface MemoryDocumentOptions {\n /** アイテム保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n}\n\nexport interface MemoryImageOptions {\n /** エントリ保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n /** 合計保持サイズ上限(バイト)。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxSizeBytes?: number;\n}\n\nexport interface MemoryCacheOptions\n extends MemoryDocumentOptions,\n MemoryImageOptions {}\n\n/**\n * Map の挿入順を LRU として扱う軽量実装。\n * `touch` で既存キーを末尾に移動、`enforceLimit` で古いキー(先頭)から削除する。\n */\nfunction touch<K, V>(map: Map<K, V>, key: K): void {\n const v = map.get(key);\n if (v === undefined) return;\n map.delete(key);\n map.set(key, v);\n}\n\nconst itemKey = (collection: string, slug: string): string =>\n `${collection}:${slug}`;\n\n/** インメモリのドキュメントオペレーション実装。プロセス再起動でクリアされる。 */\nclass MemoryDocumentOps implements DocumentCacheOps {\n private lists = new Map<string, CachedItemList<BaseContentItem>>();\n private metas = new Map<string, CachedItemMeta<BaseContentItem>>();\n private contents = new Map<string, CachedItemContent>();\n private readonly maxItems: number | undefined;\n\n constructor(options?: MemoryDocumentOptions) {\n this.maxItems = options?.maxItems;\n }\n\n getList<T extends BaseContentItem>(\n collection: string,\n ): Promise<CachedItemList<T> | null> {\n return Promise.resolve(\n (this.lists.get(collection) as CachedItemList<T> | undefined) ?? null,\n );\n }\n\n setList<T extends BaseContentItem>(\n collection: string,\n data: CachedItemList<T>,\n ): Promise<void> {\n this.lists.set(collection, data);\n return Promise.resolve();\n }\n\n getMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n ): Promise<CachedItemMeta<T> | null> {\n const key = itemKey(collection, slug);\n const entry = this.metas.get(key) as CachedItemMeta<T> | undefined;\n if (entry) touch(this.metas, key);\n return Promise.resolve(entry ?? null);\n }\n\n setMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n data: CachedItemMeta<T>,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.metas.has(key)) this.metas.delete(key);\n this.metas.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n getContent(\n collection: string,\n slug: string,\n ): Promise<CachedItemContent | null> {\n const key = itemKey(collection, slug);\n const entry = this.contents.get(key);\n if (entry) touch(this.contents, key);\n return Promise.resolve(entry ?? null);\n }\n\n setContent(\n collection: string,\n slug: string,\n data: CachedItemContent,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.contents.has(key)) this.contents.delete(key);\n this.contents.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n invalidate(scope: InvalidateScope): Promise<void> {\n if (scope === \"all\") {\n this.lists.clear();\n this.metas.clear();\n this.contents.clear();\n return Promise.resolve();\n }\n const kind = scope.kind ?? \"all\";\n const collection = scope.collection;\n if (\"slug\" in scope) {\n const key = itemKey(collection, scope.slug);\n if (kind === \"all\" || kind === \"meta\") this.metas.delete(key);\n if (kind === \"all\" || kind === \"content\") this.contents.delete(key);\n // 単一スラッグ無効化ではリストは触らない(リスト全体の整合は別管理)\n return Promise.resolve();\n }\n // コレクション全体\n if (kind === \"all\" || kind === \"meta\") {\n this.lists.delete(collection);\n const prefix = `${collection}:`;\n for (const key of [...this.metas.keys()]) {\n if (key.startsWith(prefix)) this.metas.delete(key);\n }\n }\n if (kind === \"all\" || kind === \"content\") {\n const prefix = `${collection}:`;\n for (const key of [...this.contents.keys()]) {\n if (key.startsWith(prefix)) this.contents.delete(key);\n }\n }\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n if (this.maxItems === undefined) return;\n while (this.metas.size > this.maxItems) {\n const firstKey = this.metas.keys().next().value;\n if (firstKey === undefined) break;\n this.metas.delete(firstKey);\n }\n while (this.contents.size > this.maxItems) {\n const firstKey = this.contents.keys().next().value;\n if (firstKey === undefined) break;\n this.contents.delete(firstKey);\n }\n }\n}\n\n/** インメモリの画像オペレーション実装。 */\nclass MemoryImageOps implements ImageCacheOps {\n private store = new Map<string, StorageBinary>();\n private totalBytes = 0;\n private readonly maxItems: number | undefined;\n private readonly maxSizeBytes: number | undefined;\n\n constructor(options?: MemoryImageOptions) {\n this.maxItems = options?.maxItems;\n this.maxSizeBytes = options?.maxSizeBytes;\n }\n\n get(hash: string): Promise<StorageBinary | null> {\n const entry = this.store.get(hash);\n if (entry) touch(this.store, hash);\n return Promise.resolve(entry ?? null);\n }\n\n set(hash: string, data: ArrayBuffer, contentType: string): Promise<void> {\n const existing = this.store.get(hash);\n if (existing) {\n this.totalBytes -= existing.data.byteLength;\n this.store.delete(hash);\n }\n this.store.set(hash, { data, contentType });\n this.totalBytes += data.byteLength;\n this.enforceLimit();\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n while (\n (this.maxItems !== undefined && this.store.size > this.maxItems) ||\n (this.maxSizeBytes !== undefined && this.totalBytes > this.maxSizeBytes)\n ) {\n const firstKey = this.store.keys().next().value;\n if (firstKey === undefined) break;\n const victim = this.store.get(firstKey);\n if (victim) this.totalBytes -= victim.data.byteLength;\n this.store.delete(firstKey);\n }\n }\n}\n\n/**\n * インメモリのキャッシュアダプタ。document + image 両方を担当する。\n * プロセス再起動でクリアされるため、ローカル開発・SSG ビルド・テスト用途。\n *\n * @example\n * cache: [memoryCache({ maxItems: 1000 })]\n */\nexport function memoryCache(options?: MemoryCacheOptions): CacheAdapter {\n return {\n name: \"memory\",\n handles: [\"document\", \"image\"] as const,\n doc: new MemoryDocumentOps(options),\n img: new MemoryImageOps(options),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"memory.mjs","names":[],"sources":["../../src/cache/memory.ts"],"sourcesContent":["import type {\n BaseContentItem,\n CacheAdapter,\n CacheAdapterStats,\n CacheAreaStats,\n CachedItemContent,\n CachedItemList,\n CachedItemMeta,\n DocumentCacheOps,\n ImageCacheOps,\n InvalidateScope,\n StorageBinary,\n} from \"../types/index\";\n\nexport interface MemoryDocumentOptions {\n /** アイテム保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n}\n\nexport interface MemoryImageOptions {\n /** エントリ保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n /** 合計保持サイズ上限(バイト)。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxSizeBytes?: number;\n}\n\nexport interface MemoryCacheOptions\n extends MemoryDocumentOptions,\n MemoryImageOptions {}\n\n/**\n * Map の挿入順を LRU として扱う軽量実装。\n * `touch` で既存キーを末尾に移動、`enforceLimit` で古いキー(先頭)から削除する。\n */\nfunction touch<K, V>(map: Map<K, V>, key: K): void {\n const v = map.get(key);\n if (v === undefined) return;\n map.delete(key);\n map.set(key, v);\n}\n\nconst itemKey = (collection: string, slug: string): string =>\n `${collection}:${slug}`;\n\n/** インメモリのドキュメントオペレーション実装。プロセス再起動でクリアされる。 */\nclass MemoryDocumentOps implements DocumentCacheOps {\n private lists = new Map<string, CachedItemList<BaseContentItem>>();\n private metas = new Map<string, CachedItemMeta<BaseContentItem>>();\n private contents = new Map<string, CachedItemContent>();\n private readonly maxItems: number | undefined;\n /** `cms.stats()` 用のヒット/ミス集計 (list+meta+content の合計)。 */\n hits = 0;\n misses = 0;\n\n constructor(options?: MemoryDocumentOptions) {\n this.maxItems = options?.maxItems;\n }\n\n /** 保持エントリ数 (list / meta / content の合計)。stats() で使う。 */\n totalEntries(): number {\n return this.lists.size + this.metas.size + this.contents.size;\n }\n\n getList<T extends BaseContentItem>(\n collection: string,\n ): Promise<CachedItemList<T> | null> {\n const entry = this.lists.get(collection) as CachedItemList<T> | undefined;\n if (entry) this.hits++;\n else this.misses++;\n return Promise.resolve(entry ?? null);\n }\n\n setList<T extends BaseContentItem>(\n collection: string,\n data: CachedItemList<T>,\n ): Promise<void> {\n this.lists.set(collection, data);\n return Promise.resolve();\n }\n\n getMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n ): Promise<CachedItemMeta<T> | null> {\n const key = itemKey(collection, slug);\n const entry = this.metas.get(key) as CachedItemMeta<T> | undefined;\n if (entry) {\n touch(this.metas, key);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n setMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n data: CachedItemMeta<T>,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.metas.has(key)) this.metas.delete(key);\n this.metas.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n getContent(\n collection: string,\n slug: string,\n ): Promise<CachedItemContent | null> {\n const key = itemKey(collection, slug);\n const entry = this.contents.get(key);\n if (entry) {\n touch(this.contents, key);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n setContent(\n collection: string,\n slug: string,\n data: CachedItemContent,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.contents.has(key)) this.contents.delete(key);\n this.contents.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n invalidate(scope: InvalidateScope): Promise<void> {\n if (scope === \"all\") {\n this.lists.clear();\n this.metas.clear();\n this.contents.clear();\n return Promise.resolve();\n }\n const kind = scope.kind ?? \"all\";\n const collection = scope.collection;\n if (\"slug\" in scope) {\n const key = itemKey(collection, scope.slug);\n if (kind === \"all\" || kind === \"meta\") this.metas.delete(key);\n if (kind === \"all\" || kind === \"content\") this.contents.delete(key);\n // 単一スラッグ無効化ではリストは触らない(リスト全体の整合は別管理)\n return Promise.resolve();\n }\n // コレクション全体\n if (kind === \"all\" || kind === \"meta\") {\n this.lists.delete(collection);\n const prefix = `${collection}:`;\n for (const key of [...this.metas.keys()]) {\n if (key.startsWith(prefix)) this.metas.delete(key);\n }\n }\n if (kind === \"all\" || kind === \"content\") {\n const prefix = `${collection}:`;\n for (const key of [...this.contents.keys()]) {\n if (key.startsWith(prefix)) this.contents.delete(key);\n }\n }\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n if (this.maxItems === undefined) return;\n while (this.metas.size > this.maxItems) {\n const firstKey = this.metas.keys().next().value;\n if (firstKey === undefined) break;\n this.metas.delete(firstKey);\n }\n while (this.contents.size > this.maxItems) {\n const firstKey = this.contents.keys().next().value;\n if (firstKey === undefined) break;\n this.contents.delete(firstKey);\n }\n }\n}\n\n/** インメモリの画像オペレーション実装。 */\nclass MemoryImageOps implements ImageCacheOps {\n private store = new Map<string, StorageBinary>();\n private totalBytes = 0;\n private readonly maxItems: number | undefined;\n private readonly maxSizeBytes: number | undefined;\n /** `cms.stats()` 用のヒット/ミス集計。 */\n hits = 0;\n misses = 0;\n\n constructor(options?: MemoryImageOptions) {\n this.maxItems = options?.maxItems;\n this.maxSizeBytes = options?.maxSizeBytes;\n }\n\n get(hash: string): Promise<StorageBinary | null> {\n const entry = this.store.get(hash);\n if (entry) {\n touch(this.store, hash);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n /** stats() 用に、現在の保持エントリ数と合計バイト数を返す。 */\n snapshot(): { entries: number; sizeBytes: number } {\n return { entries: this.store.size, sizeBytes: this.totalBytes };\n }\n\n set(hash: string, data: ArrayBuffer, contentType: string): Promise<void> {\n const existing = this.store.get(hash);\n if (existing) {\n this.totalBytes -= existing.data.byteLength;\n this.store.delete(hash);\n }\n this.store.set(hash, { data, contentType });\n this.totalBytes += data.byteLength;\n this.enforceLimit();\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n while (\n (this.maxItems !== undefined && this.store.size > this.maxItems) ||\n (this.maxSizeBytes !== undefined && this.totalBytes > this.maxSizeBytes)\n ) {\n const firstKey = this.store.keys().next().value;\n if (firstKey === undefined) break;\n const victim = this.store.get(firstKey);\n if (victim) this.totalBytes -= victim.data.byteLength;\n this.store.delete(firstKey);\n }\n }\n}\n\n/**\n * インメモリのキャッシュアダプタ。document + image 両方を担当する。\n * プロセス再起動でクリアされるため、ローカル開発・SSG ビルド・テスト用途。\n *\n * `stats()` でヒット率・エントリ数・画像合計バイト数を返す。\n *\n * @example\n * cache: [memoryCache({ maxItems: 1000 })]\n */\nexport function memoryCache(options?: MemoryCacheOptions): CacheAdapter {\n const doc = new MemoryDocumentOps(options);\n const img = new MemoryImageOps(options);\n return {\n name: \"memory\",\n handles: [\"document\", \"image\"] as const,\n doc,\n img,\n async stats(): Promise<CacheAdapterStats> {\n const imgSnap = img.snapshot();\n const docArea: CacheAreaStats = {\n hits: doc.hits,\n misses: doc.misses,\n entries: doc.totalEntries(),\n };\n const imgArea: CacheAreaStats = {\n hits: img.hits,\n misses: img.misses,\n entries: imgSnap.entries,\n sizeBytes: imgSnap.sizeBytes,\n };\n return { name: \"memory\", doc: docArea, img: imgArea };\n },\n };\n}\n"],"mappings":";;;;;AAkCA,SAAS,MAAY,KAAgB,KAAc;CACjD,MAAM,IAAI,IAAI,IAAI,GAAG;CACrB,IAAI,MAAM,KAAA,GAAW;CACrB,IAAI,OAAO,GAAG;CACd,IAAI,IAAI,KAAK,CAAC;AAChB;AAEA,MAAM,WAAW,YAAoB,SACnC,GAAG,WAAW,GAAG;;AAGnB,IAAM,oBAAN,MAAoD;CAClD,wBAAgB,IAAI,IAA6C;CACjE,wBAAgB,IAAI,IAA6C;CACjE,2BAAmB,IAAI,IAA+B;CACtD;;CAEA,OAAO;CACP,SAAS;CAET,YAAY,SAAiC;EAC3C,KAAK,WAAW,SAAS;CAC3B;;CAGA,eAAuB;EACrB,OAAO,KAAK,MAAM,OAAO,KAAK,MAAM,OAAO,KAAK,SAAS;CAC3D;CAEA,QACE,YACmC;EACnC,MAAM,QAAQ,KAAK,MAAM,IAAI,UAAU;EACvC,IAAI,OAAO,KAAK;OACX,KAAK;EACV,OAAO,QAAQ,QAAQ,SAAS,IAAI;CACtC;CAEA,QACE,YACA,MACe;EACf,KAAK,MAAM,IAAI,YAAY,IAAI;EAC/B,OAAO,QAAQ,QAAQ;CACzB;CAEA,QACE,YACA,MACmC;EACnC,MAAM,MAAM,QAAQ,YAAY,IAAI;EACpC,MAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;EAChC,IAAI,OAAO;GACT,MAAM,KAAK,OAAO,GAAG;GACrB,KAAK;EACP,OACE,KAAK;EAEP,OAAO,QAAQ,QAAQ,SAAS,IAAI;CACtC;CAEA,QACE,YACA,MACA,MACe;EACf,MAAM,MAAM,QAAQ,YAAY,IAAI;EACpC,IAAI,KAAK,MAAM,IAAI,GAAG,GAAG,KAAK,MAAM,OAAO,GAAG;EAC9C,KAAK,MAAM,IAAI,KAAK,IAAI;EACxB,KAAK,aAAa;EAClB,OAAO,QAAQ,QAAQ;CACzB;CAEA,WACE,YACA,MACmC;EACnC,MAAM,MAAM,QAAQ,YAAY,IAAI;EACpC,MAAM,QAAQ,KAAK,SAAS,IAAI,GAAG;EACnC,IAAI,OAAO;GACT,MAAM,KAAK,UAAU,GAAG;GACxB,KAAK;EACP,OACE,KAAK;EAEP,OAAO,QAAQ,QAAQ,SAAS,IAAI;CACtC;CAEA,WACE,YACA,MACA,MACe;EACf,MAAM,MAAM,QAAQ,YAAY,IAAI;EACpC,IAAI,KAAK,SAAS,IAAI,GAAG,GAAG,KAAK,SAAS,OAAO,GAAG;EACpD,KAAK,SAAS,IAAI,KAAK,IAAI;EAC3B,KAAK,aAAa;EAClB,OAAO,QAAQ,QAAQ;CACzB;CAEA,WAAW,OAAuC;EAChD,IAAI,UAAU,OAAO;GACnB,KAAK,MAAM,MAAM;GACjB,KAAK,MAAM,MAAM;GACjB,KAAK,SAAS,MAAM;GACpB,OAAO,QAAQ,QAAQ;EACzB;EACA,MAAM,OAAO,MAAM,QAAQ;EAC3B,MAAM,aAAa,MAAM;EACzB,IAAI,UAAU,OAAO;GACnB,MAAM,MAAM,QAAQ,YAAY,MAAM,IAAI;GAC1C,IAAI,SAAS,SAAS,SAAS,QAAQ,KAAK,MAAM,OAAO,GAAG;GAC5D,IAAI,SAAS,SAAS,SAAS,WAAW,KAAK,SAAS,OAAO,GAAG;GAElE,OAAO,QAAQ,QAAQ;EACzB;EAEA,IAAI,SAAS,SAAS,SAAS,QAAQ;GACrC,KAAK,MAAM,OAAO,UAAU;GAC5B,MAAM,SAAS,GAAG,WAAW;GAC7B,KAAK,MAAM,OAAO,CAAC,GAAG,KAAK,MAAM,KAAK,CAAC,GACrC,IAAI,IAAI,WAAW,MAAM,GAAG,KAAK,MAAM,OAAO,GAAG;EAErD;EACA,IAAI,SAAS,SAAS,SAAS,WAAW;GACxC,MAAM,SAAS,GAAG,WAAW;GAC7B,KAAK,MAAM,OAAO,CAAC,GAAG,KAAK,SAAS,KAAK,CAAC,GACxC,IAAI,IAAI,WAAW,MAAM,GAAG,KAAK,SAAS,OAAO,GAAG;EAExD;EACA,OAAO,QAAQ,QAAQ;CACzB;CAEA,eAA6B;EAC3B,IAAI,KAAK,aAAa,KAAA,GAAW;EACjC,OAAO,KAAK,MAAM,OAAO,KAAK,UAAU;GACtC,MAAM,WAAW,KAAK,MAAM,KAAK,EAAE,KAAK,EAAE;GAC1C,IAAI,aAAa,KAAA,GAAW;GAC5B,KAAK,MAAM,OAAO,QAAQ;EAC5B;EACA,OAAO,KAAK,SAAS,OAAO,KAAK,UAAU;GACzC,MAAM,WAAW,KAAK,SAAS,KAAK,EAAE,KAAK,EAAE;GAC7C,IAAI,aAAa,KAAA,GAAW;GAC5B,KAAK,SAAS,OAAO,QAAQ;EAC/B;CACF;AACF;;AAGA,IAAM,iBAAN,MAA8C;CAC5C,wBAAgB,IAAI,IAA2B;CAC/C,aAAqB;CACrB;CACA;;CAEA,OAAO;CACP,SAAS;CAET,YAAY,SAA8B;EACxC,KAAK,WAAW,SAAS;EACzB,KAAK,eAAe,SAAS;CAC/B;CAEA,IAAI,MAA6C;EAC/C,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;EACjC,IAAI,OAAO;GACT,MAAM,KAAK,OAAO,IAAI;GACtB,KAAK;EACP,OACE,KAAK;EAEP,OAAO,QAAQ,QAAQ,SAAS,IAAI;CACtC;;CAGA,WAAmD;EACjD,OAAO;GAAE,SAAS,KAAK,MAAM;GAAM,WAAW,KAAK;EAAW;CAChE;CAEA,IAAI,MAAc,MAAmB,aAAoC;EACvE,MAAM,WAAW,KAAK,MAAM,IAAI,IAAI;EACpC,IAAI,UAAU;GACZ,KAAK,cAAc,SAAS,KAAK;GACjC,KAAK,MAAM,OAAO,IAAI;EACxB;EACA,KAAK,MAAM,IAAI,MAAM;GAAE;GAAM;EAAY,CAAC;EAC1C,KAAK,cAAc,KAAK;EACxB,KAAK,aAAa;EAClB,OAAO,QAAQ,QAAQ;CACzB;CAEA,eAA6B;EAC3B,OACG,KAAK,aAAa,KAAA,KAAa,KAAK,MAAM,OAAO,KAAK,YACtD,KAAK,iBAAiB,KAAA,KAAa,KAAK,aAAa,KAAK,cAC3D;GACA,MAAM,WAAW,KAAK,MAAM,KAAK,EAAE,KAAK,EAAE;GAC1C,IAAI,aAAa,KAAA,GAAW;GAC5B,MAAM,SAAS,KAAK,MAAM,IAAI,QAAQ;GACtC,IAAI,QAAQ,KAAK,cAAc,OAAO,KAAK;GAC3C,KAAK,MAAM,OAAO,QAAQ;EAC5B;CACF;AACF;;;;;;;;;;AAWA,SAAgB,YAAY,SAA4C;CACtE,MAAM,MAAM,IAAI,kBAAkB,OAAO;CACzC,MAAM,MAAM,IAAI,eAAe,OAAO;CACtC,OAAO;EACL,MAAM;EACN,SAAS,CAAC,YAAY,OAAO;EAC7B;EACA;EACA,MAAM,QAAoC;GACxC,MAAM,UAAU,IAAI,SAAS;GAY7B,OAAO;IAAE,MAAM;IAAU,KAAK;KAV5B,MAAM,IAAI;KACV,QAAQ,IAAI;KACZ,SAAS,IAAI,aAAa;IAQQ;IAAG,KAAK;KAL1C,MAAM,IAAI;KACV,QAAQ,IAAI;KACZ,SAAS,QAAQ;KACjB,WAAW,QAAQ;IAE6B;GAAE;EACtD;CACF;AACF"}
@@ -98,6 +98,17 @@ interface DataSource<T extends BaseContentItem = BaseContentItem> {
98
98
  * ドキュメントキャッシュ用のオペレーション群。
99
99
  * `CacheAdapter.doc` に実装する。collection 名は引数で渡されるので、
100
100
  * アダプタ側で `{collection}:{slug}` のようなキー戦略を組み立てる。
101
+ *
102
+ * core 側の役割:
103
+ * - SWR ロジックは core の `CollectionClient` が制御し、TTL 切れの判定や差分検出を行う。
104
+ * adapter は読み書きと無効化を実装するだけ。
105
+ * - `invalidate(scope)` の `scope` は `"all"`、コレクション全体、または単一 slug の 3 形態。
106
+ * `kind: "meta" | "content" | "all"` で削除粒度を指定する。
107
+ *
108
+ * adapter 側で気をつけること:
109
+ * - I/O 失敗は `CMSError(code: "cache/io_failed")` でラップしてから throw する (生の Error を投げない)
110
+ * - 値は JSON シリアライズ可能とは限らない (例: `CachedItemContent.bodyHtml`)。バイナリ安全に保存する
111
+ * - 並列書き込みは core では制御しないので、必要なら adapter 内部でロック / トランザクションを取る
101
112
  */
102
113
  interface DocumentCacheOps {
103
114
  getList<T extends BaseContentItem>(collection: string): Promise<CachedItemList<T> | null>;
@@ -108,30 +119,80 @@ interface DocumentCacheOps {
108
119
  setContent(collection: string, slug: string, data: CachedItemContent): Promise<void>;
109
120
  invalidate(scope: InvalidateScope): Promise<void>;
110
121
  }
111
- /** 画像キャッシュ用のオペレーション群。`CacheAdapter.img` に実装する。 */
122
+ /**
123
+ * 画像キャッシュ用のオペレーション群。`CacheAdapter.img` に実装する。
124
+ *
125
+ * 責務境界:
126
+ * - hash 計算と HTTP fetch は core (`fetchAndCacheImage` / `buildCacheImageFn`) が担当する。
127
+ * adapter は `hash` をキーにしたバイナリの永続化だけを担う。
128
+ * - `RenderContext.cacheImage(url)` (=`cms.cacheImage`) は core 側の合成関数で、
129
+ * 「URL → SHA256 → adapter.set → プロキシ URL」を 1 ステップにまとめたもの。
130
+ * adapter から呼び出してはいけない (依存方向の逆転になる)。
131
+ * - 読み取りは miss を `null` で返す。例外を投げないこと (上位の SWR が壊れる)。
132
+ * - 書き込みは I/O 失敗時に `CMSError(code: "cache/io_failed")` でラップ可能だが、
133
+ * 画像系は fail-soft 推奨 (失敗時は通常 fetch にフォールバックする想定)。
134
+ */
112
135
  interface ImageCacheOps {
113
136
  get(hash: string): Promise<StorageBinary | null>;
114
137
  set(hash: string, data: ArrayBuffer, contentType: string): Promise<void>;
115
138
  }
139
+ /**
140
+ * 1 領域 (document / image) ぶんのキャッシュ統計。
141
+ * `CacheAdapter.stats()` の戻り値 / `cms.stats()` の集約形に使う。
142
+ */
143
+ interface CacheAreaStats {
144
+ /** キャッシュヒット回数 (起動から累積)。 */
145
+ hits: number;
146
+ /** キャッシュミス回数 (起動から累積)。 */
147
+ misses: number;
148
+ /** 保持エントリ数。集計不可な adapter は省略可。 */
149
+ entries?: number;
150
+ /** 保持データの合計バイト数。集計不可な adapter は省略可。 */
151
+ sizeBytes?: number;
152
+ }
153
+ /**
154
+ * `CacheAdapter.stats()` が返す統計。document / image を併せ持つ adapter は両方を返す。
155
+ * adapter が `handles` に含めていない領域は省略する。
156
+ */
157
+ interface CacheAdapterStats {
158
+ /** adapter 名 (`CacheAdapter.name` と同値)。`cms.stats()` 側で識別用に保持する。 */
159
+ name?: string;
160
+ doc?: CacheAreaStats;
161
+ img?: CacheAreaStats;
162
+ }
116
163
  /**
117
164
  * 統一キャッシュアダプタ。`handles` で担当領域を申告し、
118
165
  * `doc` / `img` のいずれか(または両方)を実装する。
119
166
  *
120
- * `createClient({ cache })` には `CacheAdapter | CacheAdapter[]` を渡せる。
121
- * 配列で渡された場合、core は `handles` を見て document / image をそれぞれ別アダプタに振り分ける。
167
+ * `createClient({ cache })` には `CacheAdapter[]` を渡す (単一 adapter でも配列で渡す)。
168
+ *
169
+ * `handles` の判定ルール:
170
+ * - core は配列を先頭から走査し、`handles.includes("document")` を満たす最初の adapter を
171
+ * document 担当に、`handles.includes("image")` を満たす最初の adapter を image 担当に割り当てる
172
+ * - 同じ adapter が両方の `handles` を持つ場合は単独で両領域を担当する (memoryCache はこの形)
173
+ * - 領域を申告する一方で対応する `doc` / `img` を実装していない場合、その領域では先勝ちにならず
174
+ * 次の adapter が候補になる
122
175
  *
123
176
  * @example
124
- * cache: memoryCache() // doc + image 両方
125
- * cache: r2Cache({ bucket }) // image のみ
126
- * cache: kvCache({ namespace }) // document のみ
127
- * cache: [kvCache({ ns }), r2Cache({ bucket })] // 個別に組み合わせ
177
+ * cache: [memoryCache()] // doc + image 両方
178
+ * cache: [r2Cache({ bucket })] // image のみ
179
+ * cache: [kvCache({ namespace })] // document のみ
180
+ * cache: [kvCache({ ns }), r2Cache({ bucket })] // KV: doc / R2: image を個別配線
181
+ *
182
+ * オプションの `stats()` を実装すると `cms.stats()` 経由でヒット率・サイズが取得できる。
183
+ * 未実装の adapter は集計から除外される (`cms.stats()` の戻り値で undefined になる)。
128
184
  */
129
185
  interface CacheAdapter {
130
186
  readonly name: string;
131
187
  readonly handles: readonly ("document" | "image")[];
132
188
  doc?: DocumentCacheOps;
133
189
  img?: ImageCacheOps;
190
+ /**
191
+ * キャッシュ統計を返す任意のフック。
192
+ * adapter が hit/miss を集計していない場合は実装しない (cms.stats() 側で無視される)。
193
+ */
194
+ stats?(): Promise<CacheAdapterStats>;
134
195
  }
135
196
  //#endregion
136
- export { InvalidateKind as a, PropertyMap as c, DataSource as i, WebhookConfig as l, DocumentCacheOps as n, InvalidateScope as o, ImageCacheOps as r, PropertyDef as s, CacheAdapter as t };
137
- //# sourceMappingURL=cache-DS81aOcC.d.mts.map
197
+ export { ImageCacheOps as a, InvalidateScope as c, WebhookConfig as d, DocumentCacheOps as i, PropertyDef as l, CacheAdapterStats as n, DataSource as o, CacheAreaStats as r, InvalidateKind as s, CacheAdapter as t, PropertyMap as u };
198
+ //# sourceMappingURL=cache-v9jTMnYd.d.mts.map
@@ -1,6 +1,6 @@
1
1
  import { t as BaseContentItem } from "./content-DwsfWZao.mjs";
2
- import { i as DataSource, t as CacheAdapter } from "./cache-DS81aOcC.mjs";
3
- import { i as CMSHooks, r as Logger, t as CMSPlugin } from "./plugin-Dct12kp2.mjs";
2
+ import { o as DataSource, t as CacheAdapter } from "./cache-v9jTMnYd.mjs";
3
+ import { a as CMSHooks, i as Logger, t as CMSPlugin } from "./plugin-BmrOz8T6.mjs";
4
4
 
5
5
  //#region src/types/sources.d.ts
6
6
  /**
@@ -59,7 +59,14 @@ interface SWRConfig {
59
59
  /** SWR の有効期間 (ミリ秒)。未設定時は TTL なし(失効まで stale を返す)。 */
60
60
  ttlMs?: number;
61
61
  }
62
- /** レートリミット・リトライ設定。 */
62
+ /**
63
+ * `RateLimiterConfig` のデフォルト値 (Issue #313 / M2)。
64
+ * 型からだけでは見えない既定値を表面化することで、IDE 補完と
65
+ * preset の対称化 (`nodePreset` / `cloudflarePreset` / `nextPreset`) で
66
+ * 同じ既定が適用されることを保証する。
67
+ */
68
+ declare const DEFAULT_RATE_LIMITER: Required<RateLimiterConfig>;
69
+ /** レートリミット・リトライ設定。既定値は {@link DEFAULT_RATE_LIMITER}。 */
63
70
  interface RateLimiterConfig {
64
71
  /** 同時実行数の上限。デフォルト: 3 */
65
72
  maxConcurrent?: number;
@@ -90,6 +97,34 @@ interface CollectionDef<T extends BaseContentItem = BaseContentItem> {
90
97
  /** コレクション固有のライフサイクルフック。グローバル hooks の後に実行される。 */
91
98
  hooks?: CMSHooks<T>;
92
99
  }
100
+ /**
101
+ * `CollectionDef` の strict 版。`slugField` / `statusField` が `keyof T & string` で
102
+ * 型ガードされており、誤フィールド名は型エラーになる (Issue #314 / M3)。
103
+ * CLI 生成スキーマや `defineCollection<T>()` 経由で利用される。
104
+ */
105
+ interface StrictCollectionDef<T extends BaseContentItem> extends Omit<CollectionDef<T>, "slugField" | "statusField"> {
106
+ /** slug として使う TS フィールド名。`keyof T` で型ガードされる。 */
107
+ slugField: keyof T & string;
108
+ /** ステータスとして使う TS フィールド名。`keyof T` で型ガードされる。 */
109
+ statusField?: keyof T & string;
110
+ }
111
+ /**
112
+ * 型推論ヘルパー: `T` を明示してコレクション定義を作る。`slugField` / `statusField` は
113
+ * `keyof T & string` で補完・型ガードされ、誤フィールド名 (例: `"slag"`) で型エラーになる
114
+ * (Issue #314 / M3)。CLI 生成 `nhc.schema.ts` で利用される。
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * type PostItem = BaseContentItem & { authorName?: string };
119
+ * const posts = defineCollection<PostItem>({
120
+ * source: notionSource(...),
121
+ * slugField: "slug", // OK
122
+ * statusField: "status", // OK
123
+ * // statusField: "stat", // 型エラー
124
+ * });
125
+ * ```
126
+ */
127
+ declare function defineCollection<T extends BaseContentItem>(def: StrictCollectionDef<T>): CollectionDef<T>;
93
128
  /**
94
129
  * `createClient({ collections })` の map 型。
95
130
  * キーがコレクション名、値が `CollectionDef<T>`。
@@ -146,5 +181,5 @@ interface CreateClientOptions<S extends CMSSources = CMSSources> {
146
181
  content?: ContentConfig;
147
182
  }
148
183
  //#endregion
149
- export { InferCollectionItem as a, RenderOptions as c, SWRConfig as d, CMSAdapter as f, CreateClientOptions as i, RendererFn as l, MergeSourceCollections as m, CollectionsConfig as n, LogLevel as o, CMSSources as p, ContentConfig as r, RateLimiterConfig as s, CollectionDef as t, RendererPluginList as u };
150
- //# sourceMappingURL=config-DYxyW3SR.d.mts.map
184
+ export { MergeSourceCollections as _, DEFAULT_RATE_LIMITER as a, RateLimiterConfig as c, RendererPluginList as d, SWRConfig as f, CMSSources as g, CMSAdapter as h, CreateClientOptions as i, RenderOptions as l, defineCollection as m, CollectionsConfig as n, InferCollectionItem as o, StrictCollectionDef as p, ContentConfig as r, LogLevel as s, CollectionDef as t, RendererFn as u };
185
+ //# sourceMappingURL=config-i99tKRhN.d.mts.map
package/dist/errors.mjs CHANGED
@@ -1,4 +1,138 @@
1
1
  //#region src/errors.ts
2
+ /**
3
+ * 組み込みエラーコードごとのドキュメント URL アンカーと既定の次アクション。
4
+ * `CMSError` コンストラクタが呼び出し側の `docsUrl` / `nextSteps` 未指定時のフォールバックに使用する。
5
+ *
6
+ * `docs/ja/errors/index.md` の各セクション ID と同期させること。
7
+ */
8
+ const ERROR_DOCS_BASE = "https://github.com/kjfsm/notion-headless-cms/blob/main/docs/ja/errors/index.md";
9
+ const BUILT_IN_ERROR_HELP = {
10
+ "core/config_invalid": {
11
+ docsAnchor: "core-config_invalid",
12
+ nextSteps: ["createClient の必須オプション (sources / collections) を確認する", "NOTION_TOKEN が環境変数または .dev.vars に設定されているか確認する"]
13
+ },
14
+ "core/schema_invalid": {
15
+ docsAnchor: "core-schema_invalid",
16
+ nextSteps: ["nhc generate を再実行して schema を最新化する", "PropertyMap / PropertyDef の型が定義どおりか確認する"]
17
+ },
18
+ "core/notion_orm_missing": {
19
+ docsAnchor: "core-notion_orm_missing",
20
+ nextSteps: ["@notion-headless-cms/notion-orm をインストールする", "実行ランタイムが動的 import を許可しているか確認する"]
21
+ },
22
+ "core/sort_unsupported_type": {
23
+ docsAnchor: "core-sort_unsupported_type",
24
+ nextSteps: ["sort.by に指定したフィールドの値型が string / number になっているか確認する", "別フィールドでソートするか、source 側で正規化する"]
25
+ },
26
+ "webhook/signature_invalid": {
27
+ docsAnchor: "webhook-signature_invalid",
28
+ nextSteps: ["Notion 側で発行された webhook secret が一致しているか確認する", "プロキシ / WAF がリクエストボディを書き換えていないか確認する"]
29
+ },
30
+ "webhook/payload_invalid": {
31
+ docsAnchor: "webhook-payload_invalid",
32
+ nextSteps: ["リクエストボディが JSON で送信されているか確認する", "DataSource.parseWebhook の期待するフィールド構造と一致しているか確認する"]
33
+ },
34
+ "webhook/unknown_collection": {
35
+ docsAnchor: "webhook-unknown_collection",
36
+ nextSteps: ["createClient の sources / collections に該当 collection が登録されているか確認する", "Webhook URL が `?collection=` で正しいコレクション名を指しているか確認する"]
37
+ },
38
+ "webhook/not_implemented": {
39
+ docsAnchor: "webhook-not_implemented",
40
+ nextSteps: ["対象 collection の DataSource に parseWebhook を実装する", "Webhook を使わない場合は cms.invalidate() を直接呼ぶ運用に切り替える"]
41
+ },
42
+ "source/fetch_items_failed": {
43
+ docsAnchor: "source-fetch_items_failed",
44
+ nextSteps: [
45
+ "NOTION_TOKEN がインテグレーションに紐づいているか確認する",
46
+ "Notion DB がインテグレーションに接続済みか確認する",
47
+ "ネットワーク / Notion API の障害状況 (status.notion.so) を確認する",
48
+ "rateLimiter.maxRetries / baseDelayMs を調整してリトライ余地を増やす"
49
+ ]
50
+ },
51
+ "source/fetch_item_failed": {
52
+ docsAnchor: "source-fetch_item_failed",
53
+ nextSteps: ["slug プロパティが Notion DB に存在し、値がユニークか確認する", "対象ページがインテグレーションに共有されているか確認する"]
54
+ },
55
+ "source/load_markdown_failed": {
56
+ docsAnchor: "source-load_markdown_failed",
57
+ nextSteps: ["対象ページがアーカイブされていないか確認する", "未対応ブロック (file / video など) が原因なら fetch 戦略を `markdownFetcher()` に切り替えるか、対象ブロックを除外する"]
58
+ },
59
+ "source/load_blocks_failed": {
60
+ docsAnchor: "source-load_blocks_failed",
61
+ nextSteps: ["対象ページが削除 / アーカイブされていないか確認する", "Notion API の rate limit に当たっていないかログで確認する"]
62
+ },
63
+ "source/blocks_unsupported": {
64
+ docsAnchor: "source-blocks_unsupported",
65
+ nextSteps: ["react-renderer を使う場合は fetch 戦略を `fetchBlockTree()` に切り替える", "あるいは markdown 経路で本文表示にフォールバックする"]
66
+ },
67
+ "cache/io_failed": {
68
+ docsAnchor: "cache-io_failed",
69
+ nextSteps: [
70
+ "KV / R2 / メモリキャッシュの binding (env.DOC_CACHE / env.IMG_BUCKET) が正しいか確認する",
71
+ "wrangler.toml の binding 名と createClient に渡した env が一致しているか確認する",
72
+ "一時的な障害なら SWR が次回読み込み時に自己回復する"
73
+ ]
74
+ },
75
+ "cache/image_fetch_failed": {
76
+ docsAnchor: "cache-image_fetch_failed",
77
+ nextSteps: ["Notion 署名 URL の有効期限 (約 1 時間) が切れていないか確認する", "Worker / Node のアウトバウンドネットワークが許可されているか確認する"]
78
+ },
79
+ "cache/image_invalid_content_type": {
80
+ docsAnchor: "cache-image_invalid_content_type",
81
+ nextSteps: ["Notion 画像 URL を直接ブラウザで開いて image/* を返すか確認する", "プロキシ / CDN が Content-Type を書き換えていないか確認する"]
82
+ },
83
+ "renderer/failed": {
84
+ docsAnchor: "renderer-failed",
85
+ nextSteps: ["renderer に渡している remark / rehype プラグインの組み合わせを確認する", "fetch 戦略と renderer の組み合わせが整合しているか (Notion enhanced markdown には notionMarkdownRenderer が必要) を確認する"]
86
+ },
87
+ "swr/item_check_failed": {
88
+ docsAnchor: "swr-item_check_failed",
89
+ nextSteps: ["ログで cause を確認し、source/fetch_item_failed と同じ手順で原因を切り分ける", "バックグラウンドの失敗は次回 SWR で自動再試行されるため恒久対処不要なケースもある"]
90
+ },
91
+ "swr/list_check_failed": {
92
+ docsAnchor: "swr-list_check_failed",
93
+ nextSteps: ["ログで cause を確認し、source/fetch_items_failed と同じ手順で原因を切り分ける", "Notion API の rate limit に近い場合は rateLimiter を絞る"]
94
+ },
95
+ "swr/content_rebuild_failed": {
96
+ docsAnchor: "swr-content_rebuild_failed",
97
+ nextSteps: ["renderer / loadMarkdown が一時的に失敗しただけならログを確認のうえ放置可", "恒常的に再発する場合は対象 slug を fresh: true で取り直して再現を確認する"]
98
+ },
99
+ "cli/config_invalid": {
100
+ docsAnchor: "cli-config_invalid",
101
+ nextSteps: ["nhc.config.ts が defineConfig() を default export しているか確認する", "collections に少なくとも 1 件、databaseId または dbName が指定されているか確認する"]
102
+ },
103
+ "cli/config_load_failed": {
104
+ docsAnchor: "cli-config_load_failed",
105
+ nextSteps: ["nhc.config.ts に構文エラーがないか tsc / エディタで確認する", "ESM の import パスが拡張子付き (.js) になっているか確認する"]
106
+ },
107
+ "cli/schema_invalid": {
108
+ docsAnchor: "cli-schema_invalid",
109
+ nextSteps: ["Notion DB のプロパティ型が CLI 対応 (title / richText / select / status / multiSelect / date / number / checkbox / url) のいずれかか確認する", "未対応プロパティをスキップするか、Notion 側で型を変える"]
110
+ },
111
+ "cli/generate_failed": {
112
+ docsAnchor: "cli-generate_failed",
113
+ nextSteps: ["--verbose を付けて再実行し、失敗箇所のスタックトレースを確認する", "出力先ディレクトリの書き込み権限を確認する"]
114
+ },
115
+ "cli/init_failed": {
116
+ docsAnchor: "cli-init_failed",
117
+ nextSteps: ["出力先パスに既存ファイルがある場合は --force を付けるか別パスを指定する", "親ディレクトリの書き込み権限を確認する"]
118
+ },
119
+ "cli/notion_api_failed": {
120
+ docsAnchor: "cli-notion_api_failed",
121
+ nextSteps: [
122
+ "NOTION_TOKEN がインテグレーションに紐づいているか確認する",
123
+ "対象 DB がインテグレーションに接続されているか (Notion DB → … → Connections) 確認する",
124
+ "DB 名で解決している場合は完全一致 (前後空白 / 全角半角) を確認する",
125
+ "--verbose で Notion API レスポンスの status / code を確認する"
126
+ ]
127
+ },
128
+ "cli/env_file_not_found": {
129
+ docsAnchor: "cli-env_file_not_found",
130
+ nextSteps: ["--env-file で指定したパスを実ファイルパスで確認する", "プロジェクトルートで実行しているか (相対パスは cwd 基準)"]
131
+ }
132
+ };
133
+ function lookupBuiltInHelp(code) {
134
+ return BUILT_IN_ERROR_HELP[code];
135
+ }
2
136
  var CMSError = class extends Error {
3
137
  code;
4
138
  cause;
@@ -13,8 +147,9 @@ var CMSError = class extends Error {
13
147
  this.code = params.code;
14
148
  this.cause = params.cause;
15
149
  this.context = params.context;
16
- this.nextSteps = params.nextSteps;
17
- this.docsUrl = params.docsUrl;
150
+ const help = lookupBuiltInHelp(params.code);
151
+ this.nextSteps = params.nextSteps ?? help?.nextSteps;
152
+ this.docsUrl = params.docsUrl ?? (help ? `${ERROR_DOCS_BASE}#${help.docsAnchor}` : void 0);
18
153
  }
19
154
  /** エラーコードが指定した値と一致するか判定する。 */
20
155
  is(code) {
@@ -1 +1 @@
1
- {"version":3,"file":"errors.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * ライブラリ組み込みの CMS エラーコード。\n *\n * | コード | 発生条件 |\n * |---|---|\n * | `core/config_invalid` | 設定不備(token 未設定など) |\n * | `core/schema_invalid` | schema/mapping の型不整合 |\n * | `core/notion_orm_missing` | `@notion-headless-cms/notion-orm` の動的ロード失敗 |\n * | `core/sort_unsupported_type` | ソートキーの値型が string / number でない |\n * | `webhook/signature_invalid` | Webhook 署名検証失敗 |\n * | `webhook/payload_invalid` | Webhook ペイロード形式不正 |\n * | `webhook/unknown_collection` | Webhook の対象コレクションが未知 |\n * | `webhook/not_implemented` | DataSource が parseWebhook を実装していない |\n * | `source/fetch_items_failed` | `DataSource.list()` 失敗 |\n * | `source/fetch_item_failed` | `DataSource.findByProp()` 失敗 |\n * | `source/load_markdown_failed` | `DataSource.loadMarkdown()` 失敗 |\n * | `source/load_blocks_failed` | `DataSource.loadBlocks()` 失敗 |\n * | `source/blocks_unsupported` | 選択した fetch 戦略が NotionBlockTree 取得を提供していない (markdown 戦略選択時など) |\n * | `cache/io_failed` | document / image キャッシュの I/O 失敗 |\n * | `cache/image_fetch_failed` | Notion 画像の HTTP 取得失敗 |\n * | `cache/image_invalid_content_type` | 画像レスポンスの Content-Type が不正 |\n * | `renderer/failed` | Markdown → HTML 変換失敗 |\n * | `swr/item_check_failed` | SWR バックグラウンドのアイテム差分チェック失敗 |\n * | `swr/list_check_failed` | SWR バックグラウンドのリスト差分チェック失敗 |\n * | `swr/content_rebuild_failed` | SWR バックグラウンドの本文再生成失敗 |\n * | `cli/config_invalid` | `nhc.config.ts` の内容不整合 |\n * | `cli/config_load_failed` | 設定ファイルの読み込み / 評価失敗 |\n * | `cli/schema_invalid` | CLI が受け取ったスキーマ / マッピング不整合 |\n * | `cli/generate_failed` | `nhc generate` の処理失敗 |\n * | `cli/init_failed` | `nhc init` の処理失敗 |\n * | `cli/notion_api_failed` | CLI が Notion API を呼び出す際の失敗 |\n * | `cli/env_file_not_found` | `--env-file` で指定したファイルが存在しない |\n *\n * サードパーティアダプタが独自コードを追加したい場合は `CMSErrorCode` を参照。\n */\nexport type BuiltInCMSErrorCode =\n | \"core/config_invalid\"\n | \"core/schema_invalid\"\n | \"core/notion_orm_missing\"\n | \"core/sort_unsupported_type\"\n | \"webhook/signature_invalid\"\n | \"webhook/payload_invalid\"\n | \"webhook/unknown_collection\"\n | \"webhook/not_implemented\"\n | \"source/fetch_items_failed\"\n | \"source/fetch_item_failed\"\n | \"source/load_markdown_failed\"\n | \"source/load_blocks_failed\"\n | \"source/blocks_unsupported\"\n | \"cache/io_failed\"\n | \"cache/image_fetch_failed\"\n | \"cache/image_invalid_content_type\"\n | \"renderer/failed\"\n | \"swr/item_check_failed\"\n | \"swr/list_check_failed\"\n | \"swr/content_rebuild_failed\"\n | \"cli/config_invalid\"\n | \"cli/config_load_failed\"\n | \"cli/schema_invalid\"\n | \"cli/generate_failed\"\n | \"cli/init_failed\"\n | \"cli/notion_api_failed\"\n | \"cli/env_file_not_found\";\n\n/**\n * CMS エラーコード。\n * `BuiltInCMSErrorCode` のリテラル補完を維持しつつ、\n * サードパーティアダプタが独自コードを定義できるよう `string & {}` で拡張可能にする。\n */\nexport type CMSErrorCode = BuiltInCMSErrorCode | (string & {});\n\nexport interface CMSErrorContext {\n operation: string;\n slug?: string;\n dataSourceId?: string;\n pageId?: string;\n [key: string]: string | number | boolean | null | undefined;\n}\n\nexport class CMSError extends Error {\n readonly code: CMSErrorCode;\n override readonly cause?: unknown;\n readonly context: CMSErrorContext;\n /** エラーを解消するための次のアクション(表示用)。 */\n readonly nextSteps?: readonly string[];\n /** 詳細ドキュメントへの URL(表示用)。 */\n readonly docsUrl?: string;\n\n constructor(params: {\n code: CMSErrorCode;\n message: string;\n cause?: unknown;\n context: CMSErrorContext;\n nextSteps?: readonly string[];\n docsUrl?: string;\n }) {\n super(params.message, { cause: params.cause });\n this.name = \"CMSError\";\n this.code = params.code;\n this.cause = params.cause;\n this.context = params.context;\n this.nextSteps = params.nextSteps;\n this.docsUrl = params.docsUrl;\n }\n\n /** エラーコードが指定した値と一致するか判定する。 */\n is(code: CMSErrorCode): boolean {\n return this.code === code;\n }\n\n /** エラーコードが指定した名前空間に属するか判定する(例: `\"source/\"`)。 */\n inNamespace(namespace: string): boolean {\n return this.code.startsWith(namespace);\n }\n\n /**\n * nextSteps と docsUrl を含む人間向けの詳細メッセージを返す。\n * エラーダイアログ・ログ出力時に使う。\n */\n format(): string {\n const lines: string[] = [this.message];\n if (this.nextSteps?.length) {\n lines.push(\"\\n次にやること:\");\n for (const step of this.nextSteps) {\n lines.push(` - ${step}`);\n }\n }\n if (this.docsUrl) {\n lines.push(`\\n詳細: ${this.docsUrl}`);\n }\n return lines.join(\"\\n\");\n }\n}\n\nexport function isCMSError(error: unknown): error is CMSError {\n return error instanceof CMSError;\n}\n\n/** エラーコードが特定の名前空間に属するかを判定する(例: \"source/\")。 */\nexport function isCMSErrorInNamespace(\n error: unknown,\n namespace: string,\n): error is CMSError {\n return isCMSError(error) && error.code.startsWith(namespace);\n}\n\ntype CMSErrorHandler<R> = (err: CMSError) => R;\n\n/**\n * `CMSError` を switch 式のように分岐して処理するユーティリティ。\n * `_` キーはフォールバック(CMSError 以外 or 未マッチ時)に使われる。\n *\n * @example\n * matchCMSError(err, {\n * \"source/fetch_items_failed\": (e) => handleFetchError(e),\n * _: (e) => { throw e; },\n * });\n */\nexport function matchCMSError<R>(\n error: unknown,\n handlers: Partial<Record<CMSErrorCode, CMSErrorHandler<R>>> & {\n _?: (err: unknown) => R;\n },\n): R | undefined {\n if (!isCMSError(error)) {\n return handlers._?.(error);\n }\n const handler =\n handlers[error.code as CMSErrorCode] ??\n (handlers._ as CMSErrorHandler<R> | undefined);\n return handler?.(error);\n}\n"],"mappings":";AA+EA,IAAa,WAAb,cAA8B,MAAM;CAClC;CACA;CACA;;CAEA;;CAEA;CAEA,YAAY,QAOT;EACD,MAAM,OAAO,SAAS,EAAE,OAAO,OAAO,MAAM,CAAC;EAC7C,KAAK,OAAO;EACZ,KAAK,OAAO,OAAO;EACnB,KAAK,QAAQ,OAAO;EACpB,KAAK,UAAU,OAAO;EACtB,KAAK,YAAY,OAAO;EACxB,KAAK,UAAU,OAAO;CACxB;;CAGA,GAAG,MAA6B;EAC9B,OAAO,KAAK,SAAS;CACvB;;CAGA,YAAY,WAA4B;EACtC,OAAO,KAAK,KAAK,WAAW,SAAS;CACvC;;;;;CAMA,SAAiB;EACf,MAAM,QAAkB,CAAC,KAAK,OAAO;EACrC,IAAI,KAAK,WAAW,QAAQ;GAC1B,MAAM,KAAK,WAAW;GACtB,KAAK,MAAM,QAAQ,KAAK,WACtB,MAAM,KAAK,OAAO,MAAM;EAE5B;EACA,IAAI,KAAK,SACP,MAAM,KAAK,SAAS,KAAK,SAAS;EAEpC,OAAO,MAAM,KAAK,IAAI;CACxB;AACF;AAEA,SAAgB,WAAW,OAAmC;CAC5D,OAAO,iBAAiB;AAC1B;;AAGA,SAAgB,sBACd,OACA,WACmB;CACnB,OAAO,WAAW,KAAK,KAAK,MAAM,KAAK,WAAW,SAAS;AAC7D;;;;;;;;;;;AAcA,SAAgB,cACd,OACA,UAGe;CACf,IAAI,CAAC,WAAW,KAAK,GACnB,OAAO,SAAS,IAAI,KAAK;CAK3B,QAFE,SAAS,MAAM,SACd,SAAS,KACK,KAAK;AACxB"}
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * ライブラリ組み込みの CMS エラーコード。\n *\n * | コード | 発生条件 |\n * |---|---|\n * | `core/config_invalid` | 設定不備(token 未設定など) |\n * | `core/schema_invalid` | schema/mapping の型不整合 |\n * | `core/notion_orm_missing` | `@notion-headless-cms/notion-orm` の動的ロード失敗 |\n * | `core/sort_unsupported_type` | ソートキーの値型が string / number でない |\n * | `webhook/signature_invalid` | Webhook 署名検証失敗 |\n * | `webhook/payload_invalid` | Webhook ペイロード形式不正 |\n * | `webhook/unknown_collection` | Webhook の対象コレクションが未知 |\n * | `webhook/not_implemented` | DataSource が parseWebhook を実装していない |\n * | `source/fetch_items_failed` | `DataSource.list()` 失敗 |\n * | `source/fetch_item_failed` | `DataSource.findByProp()` 失敗 |\n * | `source/load_markdown_failed` | `DataSource.loadMarkdown()` 失敗 |\n * | `source/load_blocks_failed` | `DataSource.loadBlocks()` 失敗 |\n * | `source/blocks_unsupported` | 選択した fetch 戦略が NotionBlockTree 取得を提供していない (markdown 戦略選択時など) |\n * | `cache/io_failed` | document / image キャッシュの I/O 失敗 |\n * | `cache/image_fetch_failed` | Notion 画像の HTTP 取得失敗 |\n * | `cache/image_invalid_content_type` | 画像レスポンスの Content-Type が不正 |\n * | `renderer/failed` | Markdown → HTML 変換失敗 |\n * | `swr/item_check_failed` | SWR バックグラウンドのアイテム差分チェック失敗 |\n * | `swr/list_check_failed` | SWR バックグラウンドのリスト差分チェック失敗 |\n * | `swr/content_rebuild_failed` | SWR バックグラウンドの本文再生成失敗 |\n * | `cli/config_invalid` | `nhc.config.ts` の内容不整合 |\n * | `cli/config_load_failed` | 設定ファイルの読み込み / 評価失敗 |\n * | `cli/schema_invalid` | CLI が受け取ったスキーマ / マッピング不整合 |\n * | `cli/generate_failed` | `nhc generate` の処理失敗 |\n * | `cli/init_failed` | `nhc init` の処理失敗 |\n * | `cli/notion_api_failed` | CLI が Notion API を呼び出す際の失敗 |\n * | `cli/env_file_not_found` | `--env-file` で指定したファイルが存在しない |\n *\n * サードパーティアダプタが独自コードを追加したい場合は `CMSErrorCode` を参照。\n */\nexport type BuiltInCMSErrorCode =\n | \"core/config_invalid\"\n | \"core/schema_invalid\"\n | \"core/notion_orm_missing\"\n | \"core/sort_unsupported_type\"\n | \"webhook/signature_invalid\"\n | \"webhook/payload_invalid\"\n | \"webhook/unknown_collection\"\n | \"webhook/not_implemented\"\n | \"source/fetch_items_failed\"\n | \"source/fetch_item_failed\"\n | \"source/load_markdown_failed\"\n | \"source/load_blocks_failed\"\n | \"source/blocks_unsupported\"\n | \"cache/io_failed\"\n | \"cache/image_fetch_failed\"\n | \"cache/image_invalid_content_type\"\n | \"renderer/failed\"\n | \"swr/item_check_failed\"\n | \"swr/list_check_failed\"\n | \"swr/content_rebuild_failed\"\n | \"cli/config_invalid\"\n | \"cli/config_load_failed\"\n | \"cli/schema_invalid\"\n | \"cli/generate_failed\"\n | \"cli/init_failed\"\n | \"cli/notion_api_failed\"\n | \"cli/env_file_not_found\";\n\n/**\n * CMS エラーコード。\n * `BuiltInCMSErrorCode` のリテラル補完を維持しつつ、\n * サードパーティアダプタが独自コードを定義できるよう `string & {}` で拡張可能にする。\n */\nexport type CMSErrorCode = BuiltInCMSErrorCode | (string & {});\n\nexport interface CMSErrorContext {\n operation: string;\n slug?: string;\n dataSourceId?: string;\n pageId?: string;\n [key: string]: string | number | boolean | null | undefined;\n}\n\n/**\n * 組み込みエラーコードごとのドキュメント URL アンカーと既定の次アクション。\n * `CMSError` コンストラクタが呼び出し側の `docsUrl` / `nextSteps` 未指定時のフォールバックに使用する。\n *\n * `docs/ja/errors/index.md` の各セクション ID と同期させること。\n */\nconst ERROR_DOCS_BASE =\n \"https://github.com/kjfsm/notion-headless-cms/blob/main/docs/ja/errors/index.md\";\n\ninterface ErrorHelp {\n readonly docsAnchor: string;\n readonly nextSteps: readonly string[];\n}\n\nconst BUILT_IN_ERROR_HELP: Record<BuiltInCMSErrorCode, ErrorHelp> = {\n \"core/config_invalid\": {\n docsAnchor: \"core-config_invalid\",\n nextSteps: [\n \"createClient の必須オプション (sources / collections) を確認する\",\n \"NOTION_TOKEN が環境変数または .dev.vars に設定されているか確認する\",\n ],\n },\n \"core/schema_invalid\": {\n docsAnchor: \"core-schema_invalid\",\n nextSteps: [\n \"nhc generate を再実行して schema を最新化する\",\n \"PropertyMap / PropertyDef の型が定義どおりか確認する\",\n ],\n },\n \"core/notion_orm_missing\": {\n docsAnchor: \"core-notion_orm_missing\",\n nextSteps: [\n \"@notion-headless-cms/notion-orm をインストールする\",\n \"実行ランタイムが動的 import を許可しているか確認する\",\n ],\n },\n \"core/sort_unsupported_type\": {\n docsAnchor: \"core-sort_unsupported_type\",\n nextSteps: [\n \"sort.by に指定したフィールドの値型が string / number になっているか確認する\",\n \"別フィールドでソートするか、source 側で正規化する\",\n ],\n },\n \"webhook/signature_invalid\": {\n docsAnchor: \"webhook-signature_invalid\",\n nextSteps: [\n \"Notion 側で発行された webhook secret が一致しているか確認する\",\n \"プロキシ / WAF がリクエストボディを書き換えていないか確認する\",\n ],\n },\n \"webhook/payload_invalid\": {\n docsAnchor: \"webhook-payload_invalid\",\n nextSteps: [\n \"リクエストボディが JSON で送信されているか確認する\",\n \"DataSource.parseWebhook の期待するフィールド構造と一致しているか確認する\",\n ],\n },\n \"webhook/unknown_collection\": {\n docsAnchor: \"webhook-unknown_collection\",\n nextSteps: [\n \"createClient の sources / collections に該当 collection が登録されているか確認する\",\n \"Webhook URL が `?collection=` で正しいコレクション名を指しているか確認する\",\n ],\n },\n \"webhook/not_implemented\": {\n docsAnchor: \"webhook-not_implemented\",\n nextSteps: [\n \"対象 collection の DataSource に parseWebhook を実装する\",\n \"Webhook を使わない場合は cms.invalidate() を直接呼ぶ運用に切り替える\",\n ],\n },\n \"source/fetch_items_failed\": {\n docsAnchor: \"source-fetch_items_failed\",\n nextSteps: [\n \"NOTION_TOKEN がインテグレーションに紐づいているか確認する\",\n \"Notion DB がインテグレーションに接続済みか確認する\",\n \"ネットワーク / Notion API の障害状況 (status.notion.so) を確認する\",\n \"rateLimiter.maxRetries / baseDelayMs を調整してリトライ余地を増やす\",\n ],\n },\n \"source/fetch_item_failed\": {\n docsAnchor: \"source-fetch_item_failed\",\n nextSteps: [\n \"slug プロパティが Notion DB に存在し、値がユニークか確認する\",\n \"対象ページがインテグレーションに共有されているか確認する\",\n ],\n },\n \"source/load_markdown_failed\": {\n docsAnchor: \"source-load_markdown_failed\",\n nextSteps: [\n \"対象ページがアーカイブされていないか確認する\",\n \"未対応ブロック (file / video など) が原因なら fetch 戦略を `markdownFetcher()` に切り替えるか、対象ブロックを除外する\",\n ],\n },\n \"source/load_blocks_failed\": {\n docsAnchor: \"source-load_blocks_failed\",\n nextSteps: [\n \"対象ページが削除 / アーカイブされていないか確認する\",\n \"Notion API の rate limit に当たっていないかログで確認する\",\n ],\n },\n \"source/blocks_unsupported\": {\n docsAnchor: \"source-blocks_unsupported\",\n nextSteps: [\n \"react-renderer を使う場合は fetch 戦略を `fetchBlockTree()` に切り替える\",\n \"あるいは markdown 経路で本文表示にフォールバックする\",\n ],\n },\n \"cache/io_failed\": {\n docsAnchor: \"cache-io_failed\",\n nextSteps: [\n \"KV / R2 / メモリキャッシュの binding (env.DOC_CACHE / env.IMG_BUCKET) が正しいか確認する\",\n \"wrangler.toml の binding 名と createClient に渡した env が一致しているか確認する\",\n \"一時的な障害なら SWR が次回読み込み時に自己回復する\",\n ],\n },\n \"cache/image_fetch_failed\": {\n docsAnchor: \"cache-image_fetch_failed\",\n nextSteps: [\n \"Notion 署名 URL の有効期限 (約 1 時間) が切れていないか確認する\",\n \"Worker / Node のアウトバウンドネットワークが許可されているか確認する\",\n ],\n },\n \"cache/image_invalid_content_type\": {\n docsAnchor: \"cache-image_invalid_content_type\",\n nextSteps: [\n \"Notion 画像 URL を直接ブラウザで開いて image/* を返すか確認する\",\n \"プロキシ / CDN が Content-Type を書き換えていないか確認する\",\n ],\n },\n \"renderer/failed\": {\n docsAnchor: \"renderer-failed\",\n nextSteps: [\n \"renderer に渡している remark / rehype プラグインの組み合わせを確認する\",\n \"fetch 戦略と renderer の組み合わせが整合しているか (Notion enhanced markdown には notionMarkdownRenderer が必要) を確認する\",\n ],\n },\n \"swr/item_check_failed\": {\n docsAnchor: \"swr-item_check_failed\",\n nextSteps: [\n \"ログで cause を確認し、source/fetch_item_failed と同じ手順で原因を切り分ける\",\n \"バックグラウンドの失敗は次回 SWR で自動再試行されるため恒久対処不要なケースもある\",\n ],\n },\n \"swr/list_check_failed\": {\n docsAnchor: \"swr-list_check_failed\",\n nextSteps: [\n \"ログで cause を確認し、source/fetch_items_failed と同じ手順で原因を切り分ける\",\n \"Notion API の rate limit に近い場合は rateLimiter を絞る\",\n ],\n },\n \"swr/content_rebuild_failed\": {\n docsAnchor: \"swr-content_rebuild_failed\",\n nextSteps: [\n \"renderer / loadMarkdown が一時的に失敗しただけならログを確認のうえ放置可\",\n \"恒常的に再発する場合は対象 slug を fresh: true で取り直して再現を確認する\",\n ],\n },\n \"cli/config_invalid\": {\n docsAnchor: \"cli-config_invalid\",\n nextSteps: [\n \"nhc.config.ts が defineConfig() を default export しているか確認する\",\n \"collections に少なくとも 1 件、databaseId または dbName が指定されているか確認する\",\n ],\n },\n \"cli/config_load_failed\": {\n docsAnchor: \"cli-config_load_failed\",\n nextSteps: [\n \"nhc.config.ts に構文エラーがないか tsc / エディタで確認する\",\n \"ESM の import パスが拡張子付き (.js) になっているか確認する\",\n ],\n },\n \"cli/schema_invalid\": {\n docsAnchor: \"cli-schema_invalid\",\n nextSteps: [\n \"Notion DB のプロパティ型が CLI 対応 (title / richText / select / status / multiSelect / date / number / checkbox / url) のいずれかか確認する\",\n \"未対応プロパティをスキップするか、Notion 側で型を変える\",\n ],\n },\n \"cli/generate_failed\": {\n docsAnchor: \"cli-generate_failed\",\n nextSteps: [\n \"--verbose を付けて再実行し、失敗箇所のスタックトレースを確認する\",\n \"出力先ディレクトリの書き込み権限を確認する\",\n ],\n },\n \"cli/init_failed\": {\n docsAnchor: \"cli-init_failed\",\n nextSteps: [\n \"出力先パスに既存ファイルがある場合は --force を付けるか別パスを指定する\",\n \"親ディレクトリの書き込み権限を確認する\",\n ],\n },\n \"cli/notion_api_failed\": {\n docsAnchor: \"cli-notion_api_failed\",\n nextSteps: [\n \"NOTION_TOKEN がインテグレーションに紐づいているか確認する\",\n \"対象 DB がインテグレーションに接続されているか (Notion DB → … → Connections) 確認する\",\n \"DB 名で解決している場合は完全一致 (前後空白 / 全角半角) を確認する\",\n \"--verbose で Notion API レスポンスの status / code を確認する\",\n ],\n },\n \"cli/env_file_not_found\": {\n docsAnchor: \"cli-env_file_not_found\",\n nextSteps: [\n \"--env-file で指定したパスを実ファイルパスで確認する\",\n \"プロジェクトルートで実行しているか (相対パスは cwd 基準)\",\n ],\n },\n};\n\nfunction lookupBuiltInHelp(code: CMSErrorCode): ErrorHelp | undefined {\n return BUILT_IN_ERROR_HELP[code as BuiltInCMSErrorCode];\n}\n\nexport class CMSError extends Error {\n readonly code: CMSErrorCode;\n override readonly cause?: unknown;\n readonly context: CMSErrorContext;\n /** エラーを解消するための次のアクション(表示用)。 */\n readonly nextSteps?: readonly string[];\n /** 詳細ドキュメントへの URL(表示用)。 */\n readonly docsUrl?: string;\n\n constructor(params: {\n code: CMSErrorCode;\n message: string;\n cause?: unknown;\n context: CMSErrorContext;\n nextSteps?: readonly string[];\n docsUrl?: string;\n }) {\n super(params.message, { cause: params.cause });\n this.name = \"CMSError\";\n this.code = params.code;\n this.cause = params.cause;\n this.context = params.context;\n\n // 呼び出し側が nextSteps / docsUrl を指定していない場合は、組み込みコード向けの既定値で補完する。\n // サードパーティコードや未登録コードは undefined のままにする。\n const help = lookupBuiltInHelp(params.code);\n this.nextSteps = params.nextSteps ?? help?.nextSteps;\n this.docsUrl =\n params.docsUrl ??\n (help ? `${ERROR_DOCS_BASE}#${help.docsAnchor}` : undefined);\n }\n\n /** エラーコードが指定した値と一致するか判定する。 */\n is(code: CMSErrorCode): boolean {\n return this.code === code;\n }\n\n /** エラーコードが指定した名前空間に属するか判定する(例: `\"source/\"`)。 */\n inNamespace(namespace: string): boolean {\n return this.code.startsWith(namespace);\n }\n\n /**\n * nextSteps と docsUrl を含む人間向けの詳細メッセージを返す。\n * エラーダイアログ・ログ出力時に使う。\n */\n format(): string {\n const lines: string[] = [this.message];\n if (this.nextSteps?.length) {\n lines.push(\"\\n次にやること:\");\n for (const step of this.nextSteps) {\n lines.push(` - ${step}`);\n }\n }\n if (this.docsUrl) {\n lines.push(`\\n詳細: ${this.docsUrl}`);\n }\n return lines.join(\"\\n\");\n }\n}\n\nexport function isCMSError(error: unknown): error is CMSError {\n return error instanceof CMSError;\n}\n\n/** エラーコードが特定の名前空間に属するかを判定する(例: \"source/\")。 */\nexport function isCMSErrorInNamespace(\n error: unknown,\n namespace: string,\n): error is CMSError {\n return isCMSError(error) && error.code.startsWith(namespace);\n}\n\ntype CMSErrorHandler<R> = (err: CMSError) => R;\n\n/**\n * `CMSError` を switch 式のように分岐して処理するユーティリティ。\n * `_` キーはフォールバック(CMSError 以外 or 未マッチ時)に使われる。\n *\n * @example\n * matchCMSError(err, {\n * \"source/fetch_items_failed\": (e) => handleFetchError(e),\n * _: (e) => { throw e; },\n * });\n */\nexport function matchCMSError<R>(\n error: unknown,\n handlers: Partial<Record<CMSErrorCode, CMSErrorHandler<R>>> & {\n _?: (err: unknown) => R;\n },\n): R | undefined {\n if (!isCMSError(error)) {\n return handlers._?.(error);\n }\n const handler =\n handlers[error.code as CMSErrorCode] ??\n (handlers._ as CMSErrorHandler<R> | undefined);\n return handler?.(error);\n}\n"],"mappings":";;;;;;;AAqFA,MAAM,kBACJ;AAOF,MAAM,sBAA8D;CAClE,uBAAuB;EACrB,YAAY;EACZ,WAAW,CACT,uDACA,+CACF;CACF;CACA,uBAAuB;EACrB,YAAY;EACZ,WAAW,CACT,qCACA,yCACF;CACF;CACA,2BAA2B;EACzB,YAAY;EACZ,WAAW,CACT,6CACA,gCACF;CACF;CACA,8BAA8B;EAC5B,YAAY;EACZ,WAAW,CACT,sDACA,8BACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW,CACT,8CACA,oCACF;CACF;CACA,2BAA2B;EACzB,YAAY;EACZ,WAAW,CACT,gCACA,kDACF;CACF;CACA,8BAA8B;EAC5B,YAAY;EACZ,WAAW,CACT,qEACA,qDACF;CACF;CACA,2BAA2B;EACzB,YAAY;EACZ,WAAW,CACT,mDACA,iDACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW;GACT;GACA;GACA;GACA;EACF;CACF;CACA,4BAA4B;EAC1B,YAAY;EACZ,WAAW,CACT,0CACA,8BACF;CACF;CACA,+BAA+B;EAC7B,YAAY;EACZ,WAAW,CACT,0BACA,mFACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW,CACT,+BACA,0CACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW,CACT,6DACA,iCACF;CACF;CACA,mBAAmB;EACjB,YAAY;EACZ,WAAW;GACT;GACA;GACA;EACF;CACF;CACA,4BAA4B;EAC1B,YAAY;EACZ,WAAW,CACT,6CACA,2CACF;CACF;CACA,oCAAoC;EAClC,YAAY;EACZ,WAAW,CACT,8CACA,0CACF;CACF;CACA,mBAAmB;EACjB,YAAY;EACZ,WAAW,CACT,oDACA,kGACF;CACF;CACA,yBAAyB;EACvB,YAAY;EACZ,WAAW,CACT,0DACA,6CACF;CACF;CACA,yBAAyB;EACvB,YAAY;EACZ,WAAW,CACT,2DACA,gDACF;CACF;CACA,8BAA8B;EAC5B,YAAY;EACZ,WAAW,CACT,oDACA,gDACF;CACF;CACA,sBAAsB;EACpB,YAAY;EACZ,WAAW,CACT,6DACA,4DACF;CACF;CACA,0BAA0B;EACxB,YAAY;EACZ,WAAW,CACT,4CACA,yCACF;CACF;CACA,sBAAsB;EACpB,YAAY;EACZ,WAAW,CACT,4HACA,iCACF;CACF;CACA,uBAAuB;EACrB,YAAY;EACZ,WAAW,CACT,yCACA,uBACF;CACF;CACA,mBAAmB;EACjB,YAAY;EACZ,WAAW,CACT,4CACA,qBACF;CACF;CACA,yBAAyB;EACvB,YAAY;EACZ,WAAW;GACT;GACA;GACA;GACA;EACF;CACF;CACA,0BAA0B;EACxB,YAAY;EACZ,WAAW,CACT,mCACA,kCACF;CACF;AACF;AAEA,SAAS,kBAAkB,MAA2C;CACpE,OAAO,oBAAoB;AAC7B;AAEA,IAAa,WAAb,cAA8B,MAAM;CAClC;CACA;CACA;;CAEA;;CAEA;CAEA,YAAY,QAOT;EACD,MAAM,OAAO,SAAS,EAAE,OAAO,OAAO,MAAM,CAAC;EAC7C,KAAK,OAAO;EACZ,KAAK,OAAO,OAAO;EACnB,KAAK,QAAQ,OAAO;EACpB,KAAK,UAAU,OAAO;EAItB,MAAM,OAAO,kBAAkB,OAAO,IAAI;EAC1C,KAAK,YAAY,OAAO,aAAa,MAAM;EAC3C,KAAK,UACH,OAAO,YACN,OAAO,GAAG,gBAAgB,GAAG,KAAK,eAAe,KAAA;CACtD;;CAGA,GAAG,MAA6B;EAC9B,OAAO,KAAK,SAAS;CACvB;;CAGA,YAAY,WAA4B;EACtC,OAAO,KAAK,KAAK,WAAW,SAAS;CACvC;;;;;CAMA,SAAiB;EACf,MAAM,QAAkB,CAAC,KAAK,OAAO;EACrC,IAAI,KAAK,WAAW,QAAQ;GAC1B,MAAM,KAAK,WAAW;GACtB,KAAK,MAAM,QAAQ,KAAK,WACtB,MAAM,KAAK,OAAO,MAAM;EAE5B;EACA,IAAI,KAAK,SACP,MAAM,KAAK,SAAS,KAAK,SAAS;EAEpC,OAAO,MAAM,KAAK,IAAI;CACxB;AACF;AAEA,SAAgB,WAAW,OAAmC;CAC5D,OAAO,iBAAiB;AAC1B;;AAGA,SAAgB,sBACd,OACA,WACmB;CACnB,OAAO,WAAW,KAAK,KAAK,MAAM,KAAK,WAAW,SAAS;AAC7D;;;;;;;;;;;AAcA,SAAgB,cACd,OACA,UAGe;CACf,IAAI,CAAC,WAAW,KAAK,GACnB,OAAO,SAAS,IAAI,KAAK;CAK3B,QAFE,SAAS,MAAM,SACd,SAAS,KACK,KAAK;AACxB"}
package/dist/hooks.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as BaseContentItem } from "./content-DwsfWZao.mjs";
2
- import { i as CMSHooks, r as Logger, t as CMSPlugin } from "./plugin-Dct12kp2.mjs";
2
+ import { a as CMSHooks, i as Logger, t as CMSPlugin } from "./plugin-BmrOz8T6.mjs";
3
3
 
4
4
  //#region src/hooks.d.ts
5
5
  /**
@@ -12,6 +12,14 @@ declare function mergeHooks<T extends BaseContentItem>(plugins: CMSPlugin<T>[],
12
12
  declare function mergeLoggers(plugins: Array<{
13
13
  logger?: Partial<Logger>;
14
14
  }>, directLogger?: Logger): Logger | undefined;
15
+ /**
16
+ * 既存 Logger をラップし、全ログコンテキストに `traceId` を自動で付与する。
17
+ * 呼び出し側が明示的に traceId を渡した場合はその値を優先する。
18
+ *
19
+ * `createClient` がクライアント単位の trace ID を発行し、ネストした操作
20
+ * (list / find / SWR 再生成 / retry) で同じ ID をログに伝搬するために使う。
21
+ */
22
+ declare function withTraceId(logger: Logger | undefined, traceId: string): Logger | undefined;
15
23
  //#endregion
16
- export { mergeHooks, mergeLoggers };
24
+ export { mergeHooks, mergeLoggers, withTraceId };
17
25
  //# sourceMappingURL=hooks.d.mts.map