@lupinum/ginko-content 0.1.6 → 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 (230) hide show
  1. package/compatibility.json +1 -1
  2. package/dist/cli.mjs +2 -2
  3. package/dist/cms-contract/build.js +15 -20
  4. package/dist/cms-contract/types.d.ts +12 -7
  5. package/dist/core/content/document +1 -0
  6. package/dist/core/content/document.d.ts +16 -0
  7. package/dist/core/content/document.js +2 -0
  8. package/dist/core/content/graph.d.ts +3 -1
  9. package/dist/core/content/graph.js +21 -20
  10. package/dist/core/content/locale.d.ts +6 -1
  11. package/dist/core/content/locale.js +17 -6
  12. package/dist/core/content/path.d.ts +5 -5
  13. package/dist/core/content/path.js +8 -8
  14. package/dist/core/content/snapshot +1 -0
  15. package/dist/core/content/snapshot.d.ts +25 -0
  16. package/dist/core/content/snapshot.js +99 -0
  17. package/dist/core/provider-errors.d.ts +1 -1
  18. package/dist/core/provider-errors.js +1 -0
  19. package/dist/core/query/builder.js +3 -3
  20. package/dist/core/query/execute.js +75 -36
  21. package/dist/core/query/filter.js +2 -2
  22. package/dist/core/query/lower.js +79 -3
  23. package/dist/core/query/params.js +7 -7
  24. package/dist/core/query/plan.d.ts +18 -0
  25. package/dist/core/query/plan.js +7 -0
  26. package/dist/core/references/resolve.js +6 -9
  27. package/dist/features/agent/agent-markdown +1 -0
  28. package/dist/features/agent/agent-markdown.d.ts +116 -0
  29. package/dist/features/agent/agent-markdown.js +138 -0
  30. package/dist/features/agent/walker +1 -0
  31. package/dist/features/agent/walker.d.ts +10 -0
  32. package/dist/features/agent/walker.js +174 -0
  33. package/dist/features/collections/resolve.d.ts +2 -2
  34. package/dist/features/collections/resolve.js +19 -16
  35. package/dist/features/localization/links.js +1 -1
  36. package/dist/features/localization/results.d.ts +3 -3
  37. package/dist/features/localization/results.js +46 -41
  38. package/dist/features/localization/translated-slugs.js +5 -5
  39. package/dist/features/navigation/build.d.ts +2 -2
  40. package/dist/features/navigation/build.js +23 -26
  41. package/dist/features/navigation/canonical.d.ts +10 -11
  42. package/dist/features/navigation/canonical.js +26 -21
  43. package/dist/features/navigation/resolve.js +4 -4
  44. package/dist/features/navigation/tree.js +3 -3
  45. package/dist/{runtime → features}/query/backlinks.js +3 -5
  46. package/dist/{runtime → features}/query/documents.js +1 -1
  47. package/dist/{runtime → features}/query/localized-docs.d.ts +1 -1
  48. package/dist/{runtime → features}/query/localized-docs.js +3 -6
  49. package/dist/{runtime → features}/query/navigation.d.ts +1 -1
  50. package/dist/{runtime → features}/query/navigation.js +8 -8
  51. package/dist/{runtime → features}/query/populate.js +6 -14
  52. package/dist/{runtime → features}/query/unified.d.ts +4 -4
  53. package/dist/{runtime → features}/query/unified.js +4 -2
  54. package/dist/{runtime → features}/query/variants.js +4 -4
  55. package/dist/features/search/navigation.js +2 -2
  56. package/dist/features/search/sections.d.ts +1 -1
  57. package/dist/features/search/sections.js +1 -1
  58. package/dist/features/sitemap/query.d.ts +5 -5
  59. package/dist/features/sitemap/query.js +6 -6
  60. package/dist/integrations/nitro/ingest.js +9 -20
  61. package/dist/integrations/nitro/storage.d.ts +1 -0
  62. package/dist/integrations/nitro/storage.js +22 -8
  63. package/dist/module.d.mts +135 -812
  64. package/dist/module.json +1 -1
  65. package/dist/module.mjs +70 -27
  66. package/dist/parsers/csv.js +3 -3
  67. package/dist/parsers/index.js +3 -3
  68. package/dist/parsers/json.js +8 -6
  69. package/dist/parsers/markdown-plugins.js +9 -5
  70. package/dist/parsers/markdown.js +7 -6
  71. package/dist/parsers/path-meta.js +21 -19
  72. package/dist/parsers/reserved +1 -0
  73. package/dist/parsers/reserved.d.ts +15 -0
  74. package/dist/parsers/reserved.js +35 -0
  75. package/dist/parsers/yaml.js +6 -4
  76. package/dist/public/agent +1 -0
  77. package/dist/public/agent.d.ts +17 -0
  78. package/dist/public/agent.js +34 -0
  79. package/dist/public/client.d.ts +2 -3
  80. package/dist/public/client.js +1 -2
  81. package/dist/public/provider-query +1 -0
  82. package/dist/public/provider-query.d.ts +58 -0
  83. package/dist/public/provider-query.js +5 -0
  84. package/dist/public/provider.d.ts +8 -3
  85. package/dist/public/provider.js +2 -0
  86. package/dist/public/server.d.ts +7 -12
  87. package/dist/public/server.js +5 -36
  88. package/dist/runtime/app/components/ContentRenderer.d.vue.ts +3 -3
  89. package/dist/runtime/app/components/ContentRenderer.vue +2 -2
  90. package/dist/runtime/app/components/ContentRenderer.vue.d.ts +3 -3
  91. package/dist/runtime/app/components/ContentRendererInline.d.vue.ts +1 -1
  92. package/dist/runtime/app/components/ContentRendererInline.vue.d.ts +1 -1
  93. package/dist/runtime/app/components/Prose/ProseImg.d.vue.ts +4 -4
  94. package/dist/runtime/app/components/Prose/ProseImg.vue.d.ts +4 -4
  95. package/dist/runtime/app/components/Prose/ProsePre.d.vue.ts +1 -1
  96. package/dist/runtime/app/components/Prose/ProsePre.vue.d.ts +1 -1
  97. package/dist/runtime/app/components/internal/ContentRendererMarkdown.d.vue.ts +1 -1
  98. package/dist/runtime/app/components/internal/ContentRendererMarkdown.vue +4 -4
  99. package/dist/runtime/app/components/internal/ContentRendererMarkdown.vue.d.ts +1 -1
  100. package/dist/runtime/app/components/internal/MarkdownRenderer.d.ts +3 -3
  101. package/dist/runtime/app/composables/preview.d.ts +1 -1
  102. package/dist/runtime/app/composables/query-api.d.ts +5 -5
  103. package/dist/runtime/app/composables/query-api.js +5 -3
  104. package/dist/runtime/app/composables/route.d.ts +1 -1
  105. package/dist/runtime/app/composables/route.js +4 -4
  106. package/dist/runtime/app/composables/search.js +1 -1
  107. package/dist/runtime/app/composables/use-content-document.js +1 -1
  108. package/dist/runtime/app/composables/use-content-list.js +1 -1
  109. package/dist/runtime/app/composables/use-content-navigation.js +2 -2
  110. package/dist/runtime/app/composables/use-content-page.js +4 -4
  111. package/dist/runtime/app/pages/content.vue +2 -1
  112. package/dist/runtime/markdown/plugins.js +6 -8
  113. package/dist/runtime/server/agent-markdown.d.ts +5 -65
  114. package/dist/runtime/server/agent-markdown.js +40 -315
  115. package/dist/runtime/server/agent-site.js +1 -1
  116. package/dist/runtime/server/api/agent-raw.js +1 -1
  117. package/dist/runtime/server/api/cache.d.ts +1 -1
  118. package/dist/runtime/server/api/cache.js +43 -4
  119. package/dist/runtime/server/api/locales.js +3 -3
  120. package/dist/runtime/server/api/navigation.d.ts +1 -1
  121. package/dist/runtime/server/api/navigation.js +3 -1
  122. package/dist/runtime/server/api/query.js +2 -2
  123. package/dist/runtime/server/api/search-index.d.ts +1 -1
  124. package/dist/runtime/server/api/sitemap.d.ts +1 -1
  125. package/dist/runtime/server/cache-adapters.d.ts +18 -0
  126. package/dist/runtime/server/cache-adapters.js +12 -0
  127. package/dist/runtime/server/collection-helpers.d.ts +2 -2
  128. package/dist/runtime/server/collection-helpers.js +15 -11
  129. package/dist/runtime/server/index.d.ts +6 -4
  130. package/dist/runtime/server/index.js +10 -3
  131. package/dist/runtime/server/middleware/agent-link-headers.js +1 -1
  132. package/dist/runtime/server/middleware/agent-markdown.js +1 -1
  133. package/dist/runtime/server/navigation-query.d.ts +2 -2
  134. package/dist/runtime/server/navigation-query.js +52 -7
  135. package/dist/runtime/server/navigation.d.ts +1 -1
  136. package/dist/runtime/server/provider-document +1 -0
  137. package/dist/runtime/server/provider-document.d.ts +59 -0
  138. package/dist/runtime/server/provider-document.js +48 -0
  139. package/dist/runtime/server/provider-query-wire +1 -0
  140. package/dist/runtime/server/provider-query-wire.d.ts +32 -0
  141. package/dist/runtime/server/provider-query-wire.js +27 -0
  142. package/dist/runtime/server/provider-query.d.ts +18 -9
  143. package/dist/runtime/server/provider-query.js +62 -28
  144. package/dist/runtime/server/provider-result.d.ts +4 -4
  145. package/dist/runtime/server/providers/filesystem.js +3 -11
  146. package/dist/runtime/server/providers/index.d.ts +18 -1
  147. package/dist/runtime/server/providers/index.js +72 -20
  148. package/dist/runtime/server/query-api.d.ts +5 -5
  149. package/dist/runtime/server/query-api.js +9 -6
  150. package/dist/runtime/server/query-executor.d.ts +3 -3
  151. package/dist/runtime/server/query-executor.js +43 -59
  152. package/dist/runtime/server/search.d.ts +1 -1
  153. package/dist/runtime/server/search.js +4 -4
  154. package/dist/runtime/server/sitemap.js +5 -5
  155. package/dist/runtime/server/storage-access.d.ts +4 -1
  156. package/dist/runtime/server/storage-access.js +27 -2
  157. package/dist/runtime/transformers/component-resolver.d.ts +1 -1
  158. package/dist/storage/content.d.ts +2 -6
  159. package/dist/storage/content.js +19 -8
  160. package/dist/storage/contents.d.ts +9 -1
  161. package/dist/storage/contents.js +26 -19
  162. package/dist/storage/driver.d.ts +2 -2
  163. package/dist/storage/driver.js +2 -1
  164. package/dist/storage/graph.js +4 -0
  165. package/dist/storage/manifest.js +3 -1
  166. package/dist/storage/references.js +9 -5
  167. package/dist/storage/snapshot-runtime +1 -0
  168. package/dist/storage/snapshot-runtime.d.ts +11 -0
  169. package/dist/storage/snapshot-runtime.js +46 -0
  170. package/dist/storage/validation.d.ts +1 -1
  171. package/dist/storage/validation.js +47 -102
  172. package/dist/testing/provider-contract.d.ts +14 -4
  173. package/dist/testing/provider-contract.js +121 -68
  174. package/dist/testing/provider-fixture.d.ts +1 -1
  175. package/dist/testing/provider-fixture.js +124 -120
  176. package/dist/types/config.d.ts +8 -4
  177. package/dist/types/content.d.ts +104 -70
  178. package/dist/types/fields.js +1 -1
  179. package/dist/types/module.d.ts +1 -1
  180. package/dist/types/query-parts/public.d.ts +14 -5
  181. package/dist/types/query-parts/results.d.ts +6 -2
  182. package/dist/types/query-parts/transport.d.ts +19 -0
  183. package/dist/types/virtual.d.ts +6 -0
  184. package/dist/types.d.mts +2 -2
  185. package/dist/web-types.json +1 -1
  186. package/package.json +39 -9
  187. package/dist/runtime/app/plugins/markdown-components +0 -1
  188. package/dist/runtime/app/plugins/markdown-components.d.ts +0 -2
  189. package/dist/runtime/app/plugins/markdown-components.js +0 -19
  190. /package/dist/{runtime → features/agent}/agent-paths +0 -0
  191. /package/dist/{runtime → features/agent}/agent-paths.d.ts +0 -0
  192. /package/dist/{runtime → features/agent}/agent-paths.js +0 -0
  193. /package/dist/{runtime → features}/query/backlinks +0 -0
  194. /package/dist/{runtime → features}/query/backlinks.d.ts +0 -0
  195. /package/dist/{runtime → features}/query/context +0 -0
  196. /package/dist/{runtime → features}/query/context.d.ts +0 -0
  197. /package/dist/{runtime → features}/query/context.js +0 -0
  198. /package/dist/{runtime → features}/query/documents +0 -0
  199. /package/dist/{runtime → features}/query/documents.d.ts +0 -0
  200. /package/dist/{runtime → features}/query/errors +0 -0
  201. /package/dist/{runtime → features}/query/errors.d.ts +0 -0
  202. /package/dist/{runtime → features}/query/errors.js +0 -0
  203. /package/dist/{runtime → features}/query/handles +0 -0
  204. /package/dist/{runtime → features}/query/handles.d.ts +0 -0
  205. /package/dist/{runtime → features}/query/handles.js +0 -0
  206. /package/dist/{runtime → features}/query/locale-options +0 -0
  207. /package/dist/{runtime → features}/query/locale-options.d.ts +0 -0
  208. /package/dist/{runtime → features}/query/locale-options.js +0 -0
  209. /package/dist/{runtime → features}/query/localized-docs +0 -0
  210. /package/dist/{runtime → features}/query/navigation +0 -0
  211. /package/dist/{runtime → features}/query/pagination +0 -0
  212. /package/dist/{runtime → features}/query/pagination.d.ts +0 -0
  213. /package/dist/{runtime → features}/query/pagination.js +0 -0
  214. /package/dist/{runtime → features}/query/populate +0 -0
  215. /package/dist/{runtime → features}/query/populate.d.ts +0 -0
  216. /package/dist/{runtime → features}/query/public-limits +0 -0
  217. /package/dist/{runtime → features}/query/public-limits.d.ts +0 -0
  218. /package/dist/{runtime → features}/query/public-limits.js +0 -0
  219. /package/dist/{runtime → features}/query/resolve +0 -0
  220. /package/dist/{runtime → features}/query/resolve.d.ts +0 -0
  221. /package/dist/{runtime → features}/query/resolve.js +0 -0
  222. /package/dist/{runtime → features}/query/responses +0 -0
  223. /package/dist/{runtime → features}/query/responses.d.ts +0 -0
  224. /package/dist/{runtime → features}/query/responses.js +0 -0
  225. /package/dist/{runtime → features}/query/routes +0 -0
  226. /package/dist/{runtime → features}/query/routes.d.ts +0 -0
  227. /package/dist/{runtime → features}/query/routes.js +0 -0
  228. /package/dist/{runtime → features}/query/unified +0 -0
  229. /package/dist/{runtime → features}/query/variants +0 -0
  230. /package/dist/{runtime → features}/query/variants.d.ts +0 -0
@@ -71,28 +71,19 @@ const collectDerivedReferenceIssues = (references, value, resolveReference, issu
71
71
  }
72
72
  };
73
73
  const internalDocumentFields = /* @__PURE__ */ new Set([
74
- "_id",
75
- "_file",
76
- "_path",
77
- "_type",
78
- "_collection",
79
- "_dir",
80
- "_locale",
81
- "_canonicalKey",
82
- "_partial",
83
- "_navigation",
84
- "_draft",
85
- "_stem",
86
- "_extension",
87
- "_resolvedLocale",
88
- "_requestedLocale",
89
- "_fallback",
90
- "_availableLocales",
91
- "_variantPaths",
92
- "_resolvedRefs",
93
- "_source",
94
- "body",
95
- "draft"
74
+ "id",
75
+ "file",
76
+ "path",
77
+ "type",
78
+ "collection",
79
+ "canonicalKey",
80
+ "partial",
81
+ "draft",
82
+ "dir",
83
+ "locale",
84
+ "navigationFile",
85
+ "resolved",
86
+ "body"
96
87
  ]);
97
88
  const toUserContentDocument = (document) => {
98
89
  const userDocument = {};
@@ -104,7 +95,7 @@ const toUserContentDocument = (document) => {
104
95
  return userDocument;
105
96
  };
106
97
  const validateNavigationDocument = (document) => {
107
- if (!document._navigation) {
98
+ if (!document.navigationFile) {
108
99
  return ok(void 0);
109
100
  }
110
101
  const invalidFields = [];
@@ -129,7 +120,7 @@ const validateNavigationDocument = (document) => {
129
120
  if (invalidFields.length) {
130
121
  return fail(createContentError(
131
122
  "INVALID_NAVIGATION_YAML",
132
- document._file || document._id,
123
+ document.file?.path || document.id,
133
124
  "malformed .navigation.yml",
134
125
  invalidFields.join("; ")
135
126
  ));
@@ -137,13 +128,10 @@ const validateNavigationDocument = (document) => {
137
128
  return ok(void 0);
138
129
  };
139
130
  export const getCanonicalContentId = (document, locales = []) => {
140
- if (typeof document.id === "string" && document.id.length) {
141
- return document.id;
131
+ if (typeof document.canonicalKey === "string" && document.canonicalKey.length) {
132
+ return document.canonicalKey;
142
133
  }
143
- if (typeof document._canonicalKey === "string" && document._canonicalKey.length) {
144
- return document._canonicalKey;
145
- }
146
- const file = (document._file || "").replace(/^\/+/, "");
134
+ const file = (document.file?.path || "").replace(/^\/+/, "");
147
135
  const parts = file.split("/").filter(Boolean);
148
136
  const normalized = parts[0] && locales.includes(parts[0]) ? parts.slice(1) : parts;
149
137
  return normalized.join("/").replace(/\.[^.]+$/, "");
@@ -153,10 +141,10 @@ export const validateCollectionDocument = (document, collections = {}) => {
153
141
  if (!navigationResult.ok) {
154
142
  return navigationResult;
155
143
  }
156
- if (!document._collection) {
144
+ if (!document.collection) {
157
145
  return ok(document);
158
146
  }
159
- const collection = getCollectionConfig(document._collection, collections);
147
+ const collection = getCollectionConfig(document.collection, collections);
160
148
  const schema = collection?.schema;
161
149
  if (!hasSafeParse(schema)) {
162
150
  return ok(document);
@@ -172,7 +160,7 @@ export const validateCollectionDocument = (document, collections = {}) => {
172
160
  const path = issue.path?.length ? issue.path.join(".") : "document";
173
161
  return `${path}: ${issue.message}`;
174
162
  }).join("; ");
175
- const message = `Invalid content in ${document._file || document._id}${details ? ` (${details})` : ""}`;
163
+ const message = `Invalid content in ${document.file?.path || document.id}${details ? ` (${details})` : ""}`;
176
164
  if (collection?.strict === false) {
177
165
  console.warn(message);
178
166
  return ok(document);
@@ -181,32 +169,30 @@ export const validateCollectionDocument = (document, collections = {}) => {
181
169
  "SCHEMA_VALIDATION_FAILED",
182
170
  message,
183
171
  {
184
- file: document._file || document._id,
185
- collection: document._collection,
172
+ file: document.file?.path || document.id,
173
+ collection: document.collection,
186
174
  ...details ? { details } : {}
187
175
  }
188
176
  ));
189
177
  };
190
178
  export const validateContentGraph = (contents, config) => {
191
179
  const locales = config.locales || [];
192
- const docs = contents.filter((content) => content && content._path);
193
- const routeEntries = docs.filter((content) => !content._partial && !content._navigation);
194
- const markdownEntries = routeEntries.filter((content) => content._type === "markdown");
180
+ const docs = contents.filter((content) => content && content.path);
181
+ const routeEntries = docs.filter((content) => !content.partial && !content.navigationFile);
182
+ const markdownEntries = routeEntries.filter((content) => content.type === "markdown");
195
183
  const idsByLocale = /* @__PURE__ */ new Map();
196
184
  const pathsByLocale = /* @__PURE__ */ new Map();
197
185
  const referenceTargets = buildReferenceTargets(routeEntries, locales);
198
186
  const targetCollections = /* @__PURE__ */ new Map();
199
- const variantsByCanonicalKey = /* @__PURE__ */ new Map();
200
187
  const markdownVariantsByCanonicalKey = /* @__PURE__ */ new Map();
201
- const explicitIdsByValue = /* @__PURE__ */ new Map();
202
188
  const refsByValue = /* @__PURE__ */ new Map();
203
189
  for (const document of routeEntries) {
204
- const canonicalId = document._canonicalKey || getCanonicalContentId(document, locales);
205
- if (!canonicalId || !document._collection) {
190
+ const canonicalId = document.canonicalKey || getCanonicalContentId(document, locales);
191
+ if (!canonicalId || !document.collection) {
206
192
  continue;
207
193
  }
208
194
  const collections = targetCollections.get(canonicalId) || /* @__PURE__ */ new Set();
209
- collections.add(document._collection);
195
+ collections.add(document.collection);
210
196
  targetCollections.set(canonicalId, collections);
211
197
  }
212
198
  for (const issue of collectTranslatedSlugValidationIssues(markdownEntries, {
@@ -222,85 +208,44 @@ export const validateContentGraph = (contents, config) => {
222
208
  }
223
209
  return fail(createContentError("TRANSLATED_SLUG_CONFLICT", issue.file, issue.reason, issue.details));
224
210
  }
225
- for (const document of routeEntries) {
226
- const variantKey = document._canonicalKey || getCanonicalContentId(document, locales);
227
- const siblings = variantsByCanonicalKey.get(variantKey) || [];
228
- siblings.push(document);
229
- variantsByCanonicalKey.set(variantKey, siblings);
230
- }
231
211
  for (const document of markdownEntries) {
232
- const variantKey = document._canonicalKey || getCanonicalContentId(document, locales);
212
+ const variantKey = document.canonicalKey || getCanonicalContentId(document, locales);
233
213
  const siblings = markdownVariantsByCanonicalKey.get(variantKey) || [];
234
214
  siblings.push(document);
235
215
  markdownVariantsByCanonicalKey.set(variantKey, siblings);
236
216
  }
237
- for (const [variantKey, variants] of variantsByCanonicalKey.entries()) {
238
- const explicitIds = Array.from(new Set(variants.map((document) => typeof document.id === "string" && document.id.length ? document.id : void 0).filter((value) => Boolean(value))));
239
- if (explicitIds.length > 1) {
240
- return fail(createContentError(
241
- "CONFLICTING_REFS",
242
- variants[0]?._file || variants[0]?._id || variantKey,
243
- `conflicting explicit ids across locale variants for canonical key "${variantKey}"`,
244
- explicitIds.join(", ")
245
- ));
246
- }
247
- if (explicitIds.length === 1 && variants.some((document) => document.id !== explicitIds[0])) {
248
- return fail(createContentError(
249
- "CONFLICTING_REFS",
250
- variants[0]?._file || variants[0]?._id || variantKey,
251
- `explicit id "${explicitIds[0]}" must be declared consistently across locale variants for canonical key "${variantKey}"`
252
- ));
253
- }
254
- }
255
- for (const document of routeEntries) {
256
- const explicitId = typeof document.id === "string" && document.id.length ? document.id : void 0;
257
- if (!explicitId) {
258
- continue;
259
- }
260
- const variantKey = document._canonicalKey || getCanonicalContentId(document, locales);
261
- const previous = explicitIdsByValue.get(explicitId);
262
- if (previous && previous.variantKey !== variantKey) {
263
- return fail(createContentError(
264
- "CONFLICTING_REFS",
265
- document._file || document._id,
266
- `duplicate explicit id "${explicitId}"`,
267
- `conflicts with ${previous.document._file || previous.document._id}`
268
- ));
269
- }
270
- explicitIdsByValue.set(explicitId, { document, variantKey });
271
- }
272
217
  for (const [variantKey, variants] of markdownVariantsByCanonicalKey.entries()) {
273
218
  const explicitRefs = Array.from(new Set(variants.map((document) => typeof document.ref === "string" && document.ref.length ? document.ref : void 0).filter((value) => Boolean(value))));
274
219
  if (explicitRefs.length > 1) {
275
220
  return fail(createContentError(
276
221
  "CONFLICTING_REFS",
277
- variants[0]?._file || variants[0]?._id || variantKey,
222
+ variants[0]?.file?.path || variants[0]?.id || variantKey,
278
223
  `conflicting refs across locale variants for canonical key "${variantKey}"`,
279
224
  explicitRefs.join(", ")
280
225
  ));
281
226
  }
282
227
  }
283
228
  for (const document of routeEntries) {
284
- const canonicalId = document._canonicalKey || getCanonicalContentId(document, locales);
285
- const localeKey = `${canonicalId}:${document._locale || ""}`;
229
+ const canonicalId = document.canonicalKey || getCanonicalContentId(document, locales);
230
+ const localeKey = `${canonicalId}:${document.locale || ""}`;
286
231
  if (idsByLocale.has(localeKey)) {
287
232
  const previous = idsByLocale.get(localeKey);
288
233
  return fail(createContentError(
289
234
  "DUPLICATE_CANONICAL_ID",
290
- document._file || document._id,
291
- `duplicate canonical id "${canonicalId}" for locale "${document._locale || "default"}"`,
292
- `conflicts with ${previous._file || previous._id}`
235
+ document.file?.path || document.id,
236
+ `duplicate canonical id "${canonicalId}" for locale "${document.locale || "default"}"`,
237
+ `conflicts with ${previous.file?.path || previous.id}`
293
238
  ));
294
239
  }
295
240
  idsByLocale.set(localeKey, document);
296
- const pathKey = `${document._locale || ""}:${document._path || ""}`;
241
+ const pathKey = `${document.locale || ""}:${document.path || ""}`;
297
242
  if (pathsByLocale.has(pathKey)) {
298
243
  const previous = pathsByLocale.get(pathKey);
299
244
  return fail(createContentError(
300
245
  "DUPLICATE_LOCALIZED_PATH",
301
- document._file || document._id,
302
- `duplicate localized path "${document._path}" for locale "${document._locale || "default"}"`,
303
- `conflicts with ${previous._file || previous._id}`
246
+ document.file?.path || document.id,
247
+ `duplicate localized path "${document.path}" for locale "${document.locale || "default"}"`,
248
+ `conflicts with ${previous.file?.path || previous.id}`
304
249
  ));
305
250
  }
306
251
  pathsByLocale.set(pathKey, document);
@@ -309,7 +254,7 @@ export const validateContentGraph = (contents, config) => {
309
254
  if (typeof document.ref !== "undefined" && (typeof document.ref !== "string" || !document.ref.length)) {
310
255
  return fail(createContentError(
311
256
  "INVALID_REF_VALUE",
312
- document._file || document._id,
257
+ document.file?.path || document.id,
313
258
  "ref must be a non-empty string"
314
259
  ));
315
260
  }
@@ -318,21 +263,21 @@ export const validateContentGraph = (contents, config) => {
318
263
  continue;
319
264
  }
320
265
  const previous = refsByValue.get(ref);
321
- if (previous && (previous._canonicalKey || getCanonicalContentId(previous, locales)) !== (document._canonicalKey || getCanonicalContentId(document, locales))) {
266
+ if (previous && (previous.canonicalKey || getCanonicalContentId(previous, locales)) !== (document.canonicalKey || getCanonicalContentId(document, locales))) {
322
267
  return fail(createContentError(
323
268
  "CONFLICTING_REFS",
324
- document._file || document._id,
269
+ document.file?.path || document.id,
325
270
  `duplicate ref "${ref}"`,
326
- `conflicts with ${previous._file || previous._id}`
271
+ `conflicts with ${previous.file?.path || previous.id}`
327
272
  ));
328
273
  }
329
274
  refsByValue.set(ref, document);
330
275
  }
331
276
  for (const document of docs) {
332
- if (!document._collection) {
277
+ if (!document.collection) {
333
278
  continue;
334
279
  }
335
- const collection = getCollectionConfig(document._collection, config.collections);
280
+ const collection = getCollectionConfig(document.collection, config.collections);
336
281
  const references = collection?.references;
337
282
  const schema = collection?.schema;
338
283
  if (!schema && !references) {
@@ -356,7 +301,7 @@ export const validateContentGraph = (contents, config) => {
356
301
  }
357
302
  const error = createContentError(
358
303
  "SCHEMA_VALIDATION_FAILED",
359
- document._file || document._id,
304
+ document.file?.path || document.id,
360
305
  "unresolved content references",
361
306
  issues.join("; ")
362
307
  );
@@ -1,11 +1,21 @@
1
- import { type ContentProvider } from '../public/provider';
2
- export interface SaasProviderFixtureContractSuiteOptions {
1
+ import { type ContentProvider, type ContentProviderCapabilities } from '../public/provider';
2
+ export interface ProviderContractSuiteOptions {
3
3
  name: string;
4
4
  expectedProviderName: string;
5
5
  loadProvider: () => Promise<ContentProvider>;
6
6
  createEvent: () => any;
7
+ /**
8
+ * The capabilities the provider under test declares. Each capability block
9
+ * runs its positive assertions only when the matching flag is `true`.
10
+ * Operation capabilities (`routeBackedCollections`, `navigation`,
11
+ * `surroundings`, `searchSections`, `sitemap`) and the query sub-capabilities
12
+ * (`query.limit`/`skip`/`count`) additionally assert the typed provider error
13
+ * when declared `false`; descriptive flags (`dataCollections`,
14
+ * `localizedRoutes`, `translatedSlugs`) only gate their positive block.
15
+ */
16
+ expectedCapabilities: ContentProviderCapabilities;
7
17
  collectNavPaths?: (items: Array<{
8
- _path?: string;
18
+ unprefixedPath?: string;
9
19
  children?: any[];
10
20
  }>) => string[];
11
21
  }
@@ -18,7 +28,7 @@ export interface AuthorDependencyContractOptions {
18
28
  key: string;
19
29
  }>;
20
30
  }
21
- export declare const runSaasProviderFixtureContractSuite: ({ name, expectedProviderName, loadProvider, createEvent, collectNavPaths }: SaasProviderFixtureContractSuiteOptions) => void;
31
+ export declare const runProviderContractSuite: ({ name, expectedProviderName, loadProvider, createEvent, expectedCapabilities, collectNavPaths }: ProviderContractSuiteOptions) => void;
22
32
  export declare const createAuthorDependencyContractProvider: () => {
23
33
  fixture: import("./provider-fixture").ProviderFixture;
24
34
  provider: ContentProvider & {
@@ -1,42 +1,53 @@
1
1
  import { expect, test } from "vitest";
2
- import { contentProviderResultMarker } from "../public/provider";
2
+ import { contentProviderResultMarker, toContentProviderNavigationQuery, toContentProviderQuery } from "../public/provider";
3
3
  import { createAuthorDependencyProviderFixture, createFixtureContentProvider, createProviderFixtureEvent, getProviderFixtureCacheHint } from "./provider-fixture.js";
4
4
  const defaultCollectNavPaths = (items) => items.flatMap((item) => [
5
- item._path,
5
+ item.unprefixedPath,
6
6
  ...defaultCollectNavPaths(item.children || [])
7
7
  ].filter(Boolean));
8
8
  const unwrapProviderResult = (value) => value && typeof value === "object" && value[contentProviderResultMarker] ? value.data : value;
9
- export const runSaasProviderFixtureContractSuite = ({
9
+ const expectUnsupportedOperation = (run, operation) => expect(Promise.resolve().then(run)).rejects.toMatchObject({
10
+ statusCode: 400,
11
+ statusMessage: "unsupported_provider_operation",
12
+ data: { code: "unsupported_provider_operation", operation }
13
+ });
14
+ const expectUnsupportedQueryShape = (run, field) => expect(Promise.resolve().then(run)).rejects.toMatchObject({
15
+ statusCode: 400,
16
+ statusMessage: "unsupported_query_shape",
17
+ data: { code: "unsupported_query_shape", field }
18
+ });
19
+ export const runProviderContractSuite = ({
10
20
  name,
11
21
  expectedProviderName,
12
22
  loadProvider,
13
23
  createEvent,
24
+ expectedCapabilities,
14
25
  collectNavPaths = defaultCollectNavPaths
15
26
  }) => {
27
+ const caps = expectedCapabilities;
16
28
  test(`${name} exposes explicit capabilities`, async () => {
17
29
  const provider = await loadProvider();
18
30
  expect(provider.name).toBe(expectedProviderName);
19
31
  expect(provider.capabilities).toMatchObject({
20
- routeBackedCollections: true,
21
- dataCollections: true,
22
- localizedRoutes: true,
23
- translatedSlugs: true,
24
- navigation: true,
25
- surroundings: true,
26
- searchSections: true,
27
- sitemap: true,
32
+ ...caps,
28
33
  query: {
29
- limit: true,
30
- skip: true,
31
- count: true
34
+ limit: caps.query.limit,
35
+ skip: caps.query.skip,
36
+ count: caps.query.count
32
37
  }
33
38
  });
34
- expect(provider.capabilities.query.operators).toContain("$eq");
35
- expect(provider.capabilities.query.operators).toContain("$contains");
39
+ expect(provider.capabilities.query.operators).toEqual(
40
+ expect.arrayContaining(caps.query.operators)
41
+ );
36
42
  });
37
43
  test(`${name} returns complete localized page and route metadata`, async () => {
38
44
  const provider = await loadProvider();
39
45
  const event = createEvent();
46
+ if (!caps.routeBackedCollections) {
47
+ await expectUnsupportedOperation(() => provider.page?.(event, "docs", "/de/dokumentation/einstieg"), "route-backed pages");
48
+ await expectUnsupportedOperation(() => provider.routeMeta?.(event, "docs", "/de/dokumentation/einstieg", { locale: "de" }), "route metadata");
49
+ return;
50
+ }
40
51
  const page = unwrapProviderResult(await provider.page?.(event, "docs", "/de/dokumentation/einstieg"));
41
52
  expect(page).toMatchObject({
42
53
  title: "Einstieg",
@@ -61,43 +72,62 @@ export const runSaasProviderFixtureContractSuite = ({
61
72
  locale: "de"
62
73
  })
63
74
  });
64
- });
65
- test(`${name} supports localized page misses without fallback routes`, async () => {
66
- const provider = await loadProvider();
67
75
  const miss = unwrapProviderResult(await provider.page?.(createEvent(), "docs", "/de/dokumentation/not-found"));
68
76
  expect(miss).toBeNull();
69
77
  });
70
78
  test(`${name} supports list queries with locale, ordering, limit, count, and projection`, async () => {
71
79
  const provider = await loadProvider();
72
- const response = unwrapProviderResult(await provider.query(createEvent(), {
73
- collection: "posts",
74
- resolveLocale: { locale: "de", fallback: false },
75
- sort: [{ date: -1 }],
76
- limit: 1,
77
- only: ["title", "_path", "_locale"]
78
- }));
79
- expect(response.result).toEqual([
80
- {
81
- title: "Mehrsprachiges Onboarding",
82
- _path: "/magazin/mehrsprachiges-onboarding",
83
- _locale: "de"
84
- }
85
- ]);
86
- expect(typeof response.total).toBe("number");
87
- const countResponse = unwrapProviderResult(await provider.query(createEvent(), {
88
- collection: "posts",
89
- resolveLocale: { locale: "de", fallback: false },
90
- count: true
91
- }));
92
- expect(countResponse).toEqual({ result: 1 });
80
+ if (!caps.query.limit) {
81
+ await expectUnsupportedQueryShape(() => provider.query(createEvent(), toContentProviderQuery({
82
+ collection: "posts",
83
+ resolveLocale: { locale: "de", fallback: false },
84
+ limit: 1
85
+ })), "limit");
86
+ } else {
87
+ const response = unwrapProviderResult(await provider.query(createEvent(), toContentProviderQuery({
88
+ collection: "posts",
89
+ resolveLocale: { locale: "de", fallback: false },
90
+ sort: [{ date: -1 }],
91
+ limit: 1,
92
+ only: ["title", "path", "locale"]
93
+ })));
94
+ expect(response.result).toEqual([
95
+ {
96
+ title: "Mehrsprachiges Onboarding",
97
+ path: "/magazin/mehrsprachiges-onboarding",
98
+ locale: "de"
99
+ }
100
+ ]);
101
+ expect(typeof response.total).toBe("number");
102
+ }
103
+ if (!caps.query.count) {
104
+ await expectUnsupportedQueryShape(() => provider.query(createEvent(), toContentProviderQuery({
105
+ collection: "posts",
106
+ resolveLocale: { locale: "de", fallback: false },
107
+ count: true
108
+ })), "count");
109
+ } else {
110
+ const countResponse = unwrapProviderResult(await provider.query(createEvent(), toContentProviderQuery({
111
+ collection: "posts",
112
+ resolveLocale: { locale: "de", fallback: false },
113
+ count: true
114
+ })));
115
+ expect(countResponse).toEqual({ result: 1 });
116
+ }
93
117
  });
94
- test(`${name} produces navigation, surroundings, search sections, and sitemap entries`, async () => {
118
+ test(`${name} produces navigation entries`, async () => {
95
119
  const provider = await loadProvider();
96
120
  const event = createEvent();
121
+ if (!caps.navigation) {
122
+ await expectUnsupportedOperation(() => provider.navigation?.(event, "docs", { locale: "de" }), "navigation");
123
+ const wire = toContentProviderNavigationQuery({ where: { locale: "de" } });
124
+ await expectUnsupportedOperation(() => provider.navigationQuery?.(event, wire.query, wire.options), "navigation");
125
+ return;
126
+ }
97
127
  const nav = unwrapProviderResult(await provider.navigation?.(event, "docs", { locale: "de" }));
98
128
  expect(collectNavPaths(nav || [])).toContain("/dokumentation/einstieg");
99
129
  expect(nav?.[0]).toEqual(expect.objectContaining({
100
- _locale: "de",
130
+ locale: "de",
101
131
  ref: expect.any(String),
102
132
  stableId: expect.any(String)
103
133
  }));
@@ -112,27 +142,54 @@ export const runSaasProviderFixtureContractSuite = ({
112
142
  title: expect.any(String),
113
143
  description: expect.any(String)
114
144
  }));
115
- const globalNav = unwrapProviderResult(await provider.navigationQuery?.(event, { where: { _locale: "de" } }));
145
+ const globalNavWire = toContentProviderNavigationQuery({ where: { locale: "de" } });
146
+ const globalNav = unwrapProviderResult(await provider.navigationQuery?.(event, globalNavWire.query, globalNavWire.options));
116
147
  const globalNavPaths = collectNavPaths(globalNav || []);
117
148
  expect(globalNavPaths).toContain("/dokumentation/einstieg");
118
149
  expect(globalNavPaths.length).toBeGreaterThan(0);
119
- const resolvedLocaleNav = unwrapProviderResult(await provider.navigationQuery?.(event, {
150
+ const resolvedLocaleNavWire = toContentProviderNavigationQuery({
120
151
  collection: "docs",
121
152
  resolveLocale: { locale: "de", fallback: false, exact: true }
122
- }));
153
+ });
154
+ const resolvedLocaleNav = unwrapProviderResult(await provider.navigationQuery?.(event, resolvedLocaleNavWire.query, resolvedLocaleNavWire.options));
123
155
  expect(collectNavPaths(resolvedLocaleNav || [])).toContain("/dokumentation/einstieg");
156
+ expect(JSON.stringify(nav)).not.toContain("Draft Roadmap");
157
+ });
158
+ test(`${name} produces surroundings entries`, async () => {
159
+ const provider = await loadProvider();
160
+ const event = createEvent();
161
+ if (!caps.surroundings) {
162
+ await expectUnsupportedOperation(() => provider.surroundings?.(event, "docs", "/de/dokumentation/einstieg/installation", { locale: "de" }), "surroundings");
163
+ return;
164
+ }
124
165
  const surround = unwrapProviderResult(await provider.surroundings?.(event, "docs", "/de/dokumentation/einstieg/installation", { locale: "de" }));
125
166
  expect(surround).toHaveLength(2);
126
- expect(surround?.map((item) => item?._path)).toContain("/dokumentation/einstieg/alltag");
167
+ expect(surround?.map((item) => item?.unprefixedPath)).toContain("/dokumentation/einstieg/alltag");
168
+ });
169
+ test(`${name} produces search sections`, async () => {
170
+ const provider = await loadProvider();
171
+ const event = createEvent();
172
+ if (!caps.searchSections) {
173
+ await expectUnsupportedOperation(() => provider.searchSections?.(event, "docs", { locale: "de" }), "search sections");
174
+ return;
175
+ }
127
176
  const sections = unwrapProviderResult(await provider.searchSections?.(event, "docs", { locale: "de" }));
128
177
  expect(sections?.some((section) => section.id.startsWith("/de/dokumentation/einstieg") || section.id.startsWith("/dokumentation/einstieg"))).toBe(true);
129
178
  expect(JSON.stringify(sections)).not.toContain("Draft Roadmap");
179
+ });
180
+ test(`${name} produces sitemap entries`, async () => {
181
+ const provider = await loadProvider();
182
+ const event = createEvent();
183
+ if (!caps.sitemap) {
184
+ await expectUnsupportedOperation(() => provider.sitemapEntries?.(event, { include: ["docs"] }), "sitemap entries");
185
+ return;
186
+ }
130
187
  const sitemap = unwrapProviderResult(await provider.sitemapEntries?.(event, { include: ["docs"] }));
131
188
  expect(sitemap?.some((entry) => entry.loc.endsWith("/de/dokumentation/einstieg"))).toBe(true);
132
189
  expect(JSON.stringify(sitemap)).not.toContain("/docs/draft-roadmap");
133
- expect(JSON.stringify(nav)).not.toContain("Draft Roadmap");
134
190
  });
135
191
  test(`${name} emits cache hints for rendered content`, async () => {
192
+ if (!caps.routeBackedCollections) return;
136
193
  const provider = await loadProvider();
137
194
  const event = createEvent();
138
195
  await provider.page?.(event, "posts", "/de/magazin/mehrsprachiges-onboarding");
@@ -154,38 +211,34 @@ export const runSaasProviderFixtureContractSuite = ({
154
211
  }
155
212
  });
156
213
  test(`${name} supports data-only collection reads and rejects data-only sitemap access`, async () => {
214
+ if (!caps.dataCollections) return;
157
215
  const provider = await loadProvider();
158
- const response = unwrapProviderResult(await provider.query(createEvent(), {
216
+ const response = unwrapProviderResult(await provider.query(createEvent(), toContentProviderQuery({
159
217
  collection: "versions",
160
218
  resolveLocale: { locale: "en", fallback: false },
161
219
  sort: [{ date: -1 }]
162
- }));
220
+ })));
163
221
  expect(response.result[0]?.title).toBe("Launch readiness");
164
- await expect(provider.sitemapEntries?.(createEvent(), { include: ["versions"] })).rejects.toMatchObject({
165
- statusCode: 400,
166
- statusMessage: "data_collection_sitemap_access",
167
- data: {
168
- code: "data_collection_sitemap_access",
169
- collection: "versions"
170
- }
171
- });
222
+ if (caps.sitemap) {
223
+ await expect(provider.sitemapEntries?.(createEvent(), { include: ["versions"] })).rejects.toMatchObject({
224
+ statusCode: 400,
225
+ statusMessage: "data_collection_sitemap_access",
226
+ data: {
227
+ code: "data_collection_sitemap_access",
228
+ collection: "versions"
229
+ }
230
+ });
231
+ }
172
232
  });
173
233
  test(`${name} fails loudly for unsupported query operators and unknown collections`, async () => {
174
234
  const provider = await loadProvider();
175
- await expect(provider.query(createEvent(), {
235
+ expect(() => toContentProviderQuery({
176
236
  collection: "posts",
177
237
  where: { title: { $near: "launch" } }
178
- })).rejects.toMatchObject({
179
- statusCode: 400,
180
- statusMessage: "unsupported_query_operator",
181
- data: {
182
- code: "unsupported_query_operator",
183
- operator: "$near"
184
- }
185
- });
186
- await expect(provider.query(createEvent(), {
238
+ })).toThrow(/Unsupported content query operator: \$near/);
239
+ await expect(provider.query(createEvent(), toContentProviderQuery({
187
240
  collection: "missing"
188
- })).rejects.toMatchObject({
241
+ }))).rejects.toMatchObject({
189
242
  statusCode: 400,
190
243
  statusMessage: "unknown_collection",
191
244
  data: {
@@ -63,5 +63,5 @@ export declare const createProviderFixtureEvent: (options?: ProviderFixtureEvent
63
63
  export declare const createFixtureContentProvider: (fixture: ProviderFixture, name?: string) => ContentProvider & {
64
64
  cache: ProviderFixtureCacheState;
65
65
  };
66
- export declare const createSaasProviderFixture: () => ProviderFixture;
66
+ export declare const createDefaultProviderFixture: () => ProviderFixture;
67
67
  export declare const createAuthorDependencyProviderFixture: () => ProviderFixture;