@lupinum/ginko-content 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +18 -4
  2. package/compatibility.json +1 -1
  3. package/dist/cli.mjs +317 -294
  4. package/dist/cms-import/index.js +5 -1
  5. package/dist/config.d.mts +1 -1
  6. package/dist/config.mjs +1 -1
  7. package/dist/core/cache-hints.d.ts +9 -1
  8. package/dist/core/content/collection.js +1 -1
  9. package/dist/core/content/path.js +1 -1
  10. package/dist/core/errors.js +5 -1
  11. package/dist/core/provider-errors +1 -0
  12. package/dist/core/provider-errors.d.ts +10 -0
  13. package/dist/core/provider-errors.js +27 -0
  14. package/dist/core/query/execute.js +12 -3
  15. package/dist/core/query/filter.js +2 -2
  16. package/dist/core/query/lower.js +5 -4
  17. package/dist/features/navigation/build.js +14 -6
  18. package/dist/features/navigation/canonical.js +1 -1
  19. package/dist/features/sitemap/query.js +2 -2
  20. package/dist/integrations/nitro/ingest.d.ts +3 -3
  21. package/dist/integrations/nitro/ingest.js +3 -3
  22. package/dist/integrations/nitro/storage.js +3 -1
  23. package/dist/module.d.mts +151 -526
  24. package/dist/module.json +1 -1
  25. package/dist/module.mjs +1091 -722
  26. package/dist/parsers/csv.js +4 -3
  27. package/dist/parsers/index.d.ts +2 -5
  28. package/dist/parsers/index.js +2 -2
  29. package/dist/parsers/markdown-plugins.d.ts +1 -0
  30. package/dist/parsers/markdown-plugins.js +83 -1
  31. package/dist/parsers/markdown.js +1 -1
  32. package/dist/public/client.d.ts +3 -2
  33. package/dist/public/client.js +2 -2
  34. package/dist/public/provider-errors.d.ts +2 -1
  35. package/dist/public/provider-errors.js +1 -18
  36. package/dist/public/provider.d.ts +3 -10
  37. package/dist/public/server.d.ts +6 -1
  38. package/dist/public/server.js +33 -0
  39. package/dist/runtime/agent-paths +1 -0
  40. package/dist/runtime/agent-paths.d.ts +6 -0
  41. package/dist/runtime/agent-paths.js +41 -0
  42. package/dist/runtime/app/components/internal/MarkdownRenderer.d.ts +1 -1
  43. package/dist/runtime/app/composables/head.d.ts +3 -0
  44. package/dist/runtime/app/composables/head.js +2 -1
  45. package/dist/runtime/app/composables/hot-reload +1 -0
  46. package/dist/runtime/app/composables/hot-reload.d.ts +5 -0
  47. package/dist/runtime/app/composables/hot-reload.js +15 -0
  48. package/dist/runtime/app/composables/locale-context.d.ts +1 -1
  49. package/dist/runtime/app/composables/locale-context.js +2 -1
  50. package/dist/runtime/app/composables/locale.js +2 -2
  51. package/dist/runtime/app/composables/search.js +10 -6
  52. package/dist/runtime/app/composables/use-content-document +1 -0
  53. package/dist/runtime/app/composables/use-content-document.d.ts +29 -0
  54. package/dist/runtime/app/composables/use-content-document.js +44 -0
  55. package/dist/runtime/app/composables/use-content-list +1 -0
  56. package/dist/runtime/app/composables/use-content-list.d.ts +43 -0
  57. package/dist/runtime/app/composables/use-content-list.js +98 -0
  58. package/dist/runtime/app/composables/use-content-navigation +1 -0
  59. package/dist/runtime/app/composables/use-content-navigation.d.ts +40 -0
  60. package/dist/runtime/app/composables/use-content-navigation.js +102 -0
  61. package/dist/runtime/app/composables/use-content-page +1 -0
  62. package/dist/runtime/app/composables/use-content-page.d.ts +47 -0
  63. package/dist/runtime/app/composables/use-content-page.js +142 -0
  64. package/dist/runtime/app/composables/use-content-shared +1 -0
  65. package/dist/runtime/app/composables/use-content-shared.d.ts +13 -0
  66. package/dist/runtime/app/composables/use-content-shared.js +29 -0
  67. package/dist/runtime/app/composables/use-content.d.ts +11 -206
  68. package/dist/runtime/app/composables/use-content.js +4 -377
  69. package/dist/runtime/app/plugins/hot-reload +1 -0
  70. package/dist/runtime/app/plugins/hot-reload.js +6 -0
  71. package/dist/runtime/query/backlinks +1 -0
  72. package/dist/runtime/query/backlinks.d.ts +8 -0
  73. package/dist/runtime/query/backlinks.js +90 -0
  74. package/dist/runtime/query/context +1 -0
  75. package/dist/runtime/query/context.d.ts +20 -0
  76. package/dist/runtime/query/context.js +0 -0
  77. package/dist/runtime/query/documents +1 -0
  78. package/dist/runtime/query/documents.d.ts +6 -0
  79. package/dist/runtime/query/documents.js +98 -0
  80. package/dist/runtime/query/errors +1 -0
  81. package/dist/runtime/query/errors.d.ts +1 -0
  82. package/dist/runtime/query/errors.js +4 -0
  83. package/dist/runtime/query/handles +1 -0
  84. package/dist/runtime/query/handles.d.ts +2 -0
  85. package/dist/runtime/query/handles.js +7 -0
  86. package/dist/runtime/query/locale-options +1 -0
  87. package/dist/runtime/query/locale-options.d.ts +5 -0
  88. package/dist/runtime/query/locale-options.js +14 -0
  89. package/dist/runtime/query/localized-docs +1 -0
  90. package/dist/runtime/query/localized-docs.d.ts +17 -0
  91. package/dist/runtime/query/localized-docs.js +44 -0
  92. package/dist/runtime/query/navigation +1 -0
  93. package/dist/runtime/query/navigation.d.ts +22 -0
  94. package/dist/runtime/query/navigation.js +73 -0
  95. package/dist/runtime/query/pagination +1 -0
  96. package/dist/runtime/query/pagination.d.ts +7 -0
  97. package/dist/runtime/query/pagination.js +78 -0
  98. package/dist/runtime/query/populate +1 -0
  99. package/dist/runtime/query/populate.d.ts +9 -0
  100. package/dist/runtime/query/populate.js +106 -0
  101. package/dist/runtime/query/responses +1 -0
  102. package/dist/runtime/query/responses.d.ts +10 -0
  103. package/dist/runtime/query/responses.js +47 -0
  104. package/dist/runtime/query/unified.d.ts +4 -20
  105. package/dist/runtime/query/unified.js +14 -544
  106. package/dist/runtime/query/variants +1 -0
  107. package/dist/runtime/query/variants.d.ts +11 -0
  108. package/dist/runtime/query/variants.js +36 -0
  109. package/dist/runtime/server/agent-http +1 -0
  110. package/dist/runtime/server/agent-http.d.ts +7 -0
  111. package/dist/runtime/server/agent-http.js +36 -0
  112. package/dist/runtime/server/agent-markdown +1 -0
  113. package/dist/runtime/server/agent-markdown.d.ts +83 -0
  114. package/dist/runtime/server/agent-markdown.js +449 -0
  115. package/dist/runtime/server/agent-site +1 -0
  116. package/dist/runtime/server/agent-site.d.ts +38 -0
  117. package/dist/runtime/server/agent-site.js +273 -0
  118. package/dist/runtime/server/api/agent-llms +1 -0
  119. package/dist/runtime/server/api/agent-llms-full +1 -0
  120. package/dist/runtime/server/api/agent-llms-full.d.ts +2 -0
  121. package/dist/runtime/server/api/agent-llms-full.js +13 -0
  122. package/dist/runtime/server/api/agent-llms.d.ts +2 -0
  123. package/dist/runtime/server/api/agent-llms.js +14 -0
  124. package/dist/runtime/server/api/agent-raw +1 -0
  125. package/dist/runtime/server/api/agent-raw.d.ts +2 -0
  126. package/dist/runtime/server/api/agent-raw.js +17 -0
  127. package/dist/runtime/server/api/locales.d.ts +2 -2
  128. package/dist/runtime/server/api/locales.js +4 -2
  129. package/dist/runtime/server/api/navigation.d.ts +1 -1
  130. package/dist/runtime/server/api/query.d.ts +1 -1
  131. package/dist/runtime/server/api/query.js +4 -1
  132. package/dist/runtime/server/api/revalidate.js +2 -2
  133. package/dist/runtime/server/api/site-data.d.ts +1 -1
  134. package/dist/runtime/server/collection-helpers.js +2 -1
  135. package/dist/runtime/server/middleware/agent-link-headers +1 -0
  136. package/dist/runtime/server/middleware/agent-link-headers.d.ts +2 -0
  137. package/dist/runtime/server/middleware/agent-link-headers.js +42 -0
  138. package/dist/runtime/server/middleware/agent-markdown +1 -0
  139. package/dist/runtime/server/middleware/agent-markdown.d.ts +2 -0
  140. package/dist/runtime/server/middleware/agent-markdown.js +43 -0
  141. package/dist/runtime/server/plugins/sitemap.js +1 -0
  142. package/dist/runtime/server/provider-query.d.ts +8 -5
  143. package/dist/runtime/server/provider-query.js +50 -8
  144. package/dist/runtime/server/provider-result.d.ts +19 -2
  145. package/dist/runtime/server/providers/filesystem.js +4 -1
  146. package/dist/runtime/server/providers/index.d.ts +3 -1
  147. package/dist/runtime/server/providers/index.js +2 -2
  148. package/dist/runtime/server/query-api.js +2 -1
  149. package/dist/runtime/server/query-executor.d.ts +3 -2
  150. package/dist/runtime/server/query-executor.js +13 -5
  151. package/dist/runtime/server/search.js +1 -1
  152. package/dist/runtime/server/sitemap-provider.js +40 -1
  153. package/dist/runtime/server/sitemap.js +3 -2
  154. package/dist/runtime/transformers/component-resolver.js +2 -1
  155. package/dist/runtime/utils/content-components.d.ts +0 -5
  156. package/dist/storage/content.d.ts +2 -9
  157. package/dist/storage/references.d.ts +1 -8
  158. package/dist/storage/references.js +9 -4
  159. package/dist/testing/provider-contract.js +3 -4
  160. package/dist/testing/provider-fixture.d.ts +3 -1
  161. package/dist/testing/provider-fixture.js +27 -14
  162. package/dist/types/config.d.ts +77 -0
  163. package/dist/types/config.js +23 -0
  164. package/dist/types/module.d.ts +15 -5
  165. package/dist/types/query-parts/collections +1 -0
  166. package/dist/types/query-parts/collections.d.ts +15 -0
  167. package/dist/types/query-parts/collections.js +0 -0
  168. package/dist/types/query-parts/public +1 -0
  169. package/dist/types/query-parts/public.d.ts +202 -0
  170. package/dist/types/query-parts/public.js +0 -0
  171. package/dist/types/query-parts/results +1 -0
  172. package/dist/types/query-parts/results.d.ts +95 -0
  173. package/dist/types/query-parts/results.js +0 -0
  174. package/dist/types/query-parts/transport +1 -0
  175. package/dist/types/query-parts/transport.d.ts +103 -0
  176. package/dist/types/query-parts/transport.js +0 -0
  177. package/dist/types/query.d.ts +4 -870
  178. package/dist/types/query.js +4 -0
  179. package/dist/types/runtime.d.ts +2 -2
  180. package/dist/types/virtual.d.ts +54 -0
  181. package/dist/types.d.mts +2 -2
  182. package/dist/web-types.json +1 -1
  183. package/package.json +5 -5
  184. package/dist/runtime/app/composables/web-socket +0 -1
  185. package/dist/runtime/app/composables/web-socket.d.ts +0 -3
  186. package/dist/runtime/app/composables/web-socket.js +0 -65
  187. package/dist/runtime/app/plugins/ws +0 -1
  188. package/dist/runtime/app/plugins/ws.js +0 -7
  189. /package/dist/runtime/app/plugins/{ws.d.ts → hot-reload.d.ts} +0 -0
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync } from 'node:fs';
3
- import { readFile, readdir, stat } from 'node:fs/promises';
3
+ import { stat, readFile, readdir } from 'node:fs/promises';
4
4
  import { join, extname, relative } from 'node:path';
5
5
 
6
6
  const ignoredDirs = /* @__PURE__ */ new Set([
@@ -27,68 +27,124 @@ const sourceExtensions = /* @__PURE__ */ new Set([
27
27
  ".yml",
28
28
  ".yaml"
29
29
  ]);
30
- const stalePackageNames = ["@nuxt/content", "better-sqlite3", "@standard-schema/spec"];
31
- const sourceChecks = [
32
- {
33
- pattern: /from\s+['"]@nuxt\/content(?:\/[^'"]*)?['"]|import\s*\([^)]*['"]@nuxt\/content(?:\/[^'"]*)?['"][^)]*\)/,
34
- message: "Direct Nuxt Content import found.",
35
- suggestion: "Import Ginko helpers from @lupinum/ginko-content/config, /client, or /server."
36
- },
37
- {
38
- pattern: /['"]@nuxt\/content['"]/,
39
- message: "Nuxt Content module or package reference found.",
40
- suggestion: "Remove @nuxt/content from Nuxt modules and use @lupinum/ginko-content."
41
- },
42
- {
43
- pattern: /\bqueryCollectionItemSurroundings\s*\(/,
44
- message: "Nuxt Content v3 surround helper found.",
45
- suggestion: "Use useContentPage('docs', { surround: true }) in route page components."
46
- },
47
- {
48
- pattern: /\bqueryCollectionSearchSections\s*\(/,
49
- message: "Nuxt Content v3 search sections helper found.",
50
- suggestion: "Use useContentSearchData('docs') for UI search data."
51
- },
52
- {
53
- pattern: /\bqueryCollectionNavigation\s*\(/,
54
- message: "Nuxt Content v3 navigation helper found.",
55
- suggestion: "Use useContentNavigation('docs') for layout navigation."
56
- },
57
- {
58
- pattern: /\bqueryCollection\s*\(/,
59
- message: "Removed collection query helper found.",
60
- suggestion: "Use one('docs', options), many('docs', options), paginate('docs', options), or the matching useContent* composable."
61
- },
62
- {
63
- pattern: /\buseContentList\s*\(/,
64
- message: "Removed content list composable found.",
65
- suggestion: "Use useContentMany('docs', options) or many('docs', options)."
66
- },
67
- {
68
- pattern: /\bcontent\.(database|preview|build)\b/,
69
- message: "Nuxt Content v3 runtime config key found.",
70
- suggestion: "Remove content.database/content.preview/content.build and configure Ginko runtime options instead."
71
- },
72
- {
73
- pattern: /\.editor\s*\(/,
74
- message: "Nuxt Studio Zod .editor(...) helper found.",
75
- suggestion: "Remove .editor(...) from runtime Zod schemas or move editor metadata outside the schema."
76
- },
77
- {
78
- pattern: /<ContentRenderer\b[^>]*:value\s*=\s*["'][^"']*\.body["']/,
79
- message: "ContentRenderer is receiving a document body.",
80
- suggestion: "Pass the full content document to ContentRenderer, not document.body."
81
- },
82
- {
83
- pattern: /<NuxtLink\b[^>]*:to\s*=\s*["'][^"']+\._path["']/,
84
- message: "NuxtLink is using raw content _path.",
85
- suggestion: "Use useContentMany('docs', options) for list pages and bind the route-safe item.path field."
86
- }
87
- ];
88
- const lockfileNames = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
89
- const localeCodePattern = /^[a-z]{2}(?:-[A-Z]{2})?$/;
90
30
  const toRelativePath = (rootDir, file) => relative(rootDir, file) || ".";
31
+ async function collectFiles(dir, rootDir, files = []) {
32
+ const entries = await readdir(dir, { withFileTypes: true });
33
+ for (const entry of entries) {
34
+ const absolutePath = join(dir, entry.name);
35
+ if (entry.isDirectory()) {
36
+ if (!ignoredDirs.has(entry.name)) {
37
+ await collectFiles(absolutePath, rootDir, files);
38
+ }
39
+ continue;
40
+ }
41
+ if (entry.isFile() && sourceExtensions.has(extname(entry.name))) {
42
+ files.push(absolutePath);
43
+ }
44
+ }
45
+ return files;
46
+ }
47
+ async function collectOutputFiles(dir, rootDir, files = []) {
48
+ const entries = await readdir(dir, { withFileTypes: true });
49
+ for (const entry of entries) {
50
+ const absolutePath = join(dir, entry.name);
51
+ if (entry.isDirectory()) {
52
+ if (entry.name !== "_nuxt") {
53
+ await collectOutputFiles(absolutePath, rootDir, files);
54
+ }
55
+ continue;
56
+ }
57
+ if (entry.isFile() && [".html", ".json", ".xml"].includes(extname(entry.name))) {
58
+ files.push(absolutePath);
59
+ }
60
+ }
61
+ return files;
62
+ }
63
+ async function readTextIfPresent(path) {
64
+ if (!existsSync(path)) {
65
+ return "";
66
+ }
67
+ const fileStat = await stat(path);
68
+ if (!fileStat.isFile()) {
69
+ return "";
70
+ }
71
+ return readFile(path, "utf8");
72
+ }
73
+ async function readPackageJson(rootDir) {
74
+ const path = join(rootDir, "package.json");
75
+ if (!existsSync(path)) {
76
+ return void 0;
77
+ }
78
+ try {
79
+ return JSON.parse(await readFile(path, "utf8"));
80
+ } catch {
81
+ return void 0;
82
+ }
83
+ }
84
+ function hasDependency(packageJson, packageName) {
85
+ if (!packageJson) {
86
+ return false;
87
+ }
88
+ return ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"].some((blockName) => {
89
+ const block = packageJson[blockName];
90
+ return !!block && typeof block === "object" && packageName in block;
91
+ });
92
+ }
93
+
91
94
  const countSitemapUrls = (text) => (text.match(/<url>/g) || []).length;
95
+ async function readGeneratedSitemaps(rootDir) {
96
+ const outputPublicDir = join(rootDir, ".output/public");
97
+ if (!existsSync(outputPublicDir)) {
98
+ return [];
99
+ }
100
+ const sitemapPaths = [
101
+ join(outputPublicDir, "sitemap.xml"),
102
+ join(outputPublicDir, "sitemap_index.xml")
103
+ ];
104
+ const sitemapDir = join(outputPublicDir, "__sitemap__");
105
+ if (existsSync(sitemapDir)) {
106
+ const entries = await readdir(sitemapDir, { withFileTypes: true });
107
+ for (const entry of entries) {
108
+ if (entry.isFile() && entry.name.endsWith(".xml")) {
109
+ sitemapPaths.push(join(sitemapDir, entry.name));
110
+ }
111
+ }
112
+ }
113
+ const sitemapFiles = [];
114
+ for (const sitemapPath of sitemapPaths) {
115
+ if (!existsSync(sitemapPath)) {
116
+ continue;
117
+ }
118
+ const fileStat = await stat(sitemapPath);
119
+ if (fileStat.isFile()) {
120
+ sitemapFiles.push({
121
+ file: toRelativePath(rootDir, sitemapPath),
122
+ text: await readFile(sitemapPath, "utf8")
123
+ });
124
+ }
125
+ }
126
+ return sitemapFiles;
127
+ }
128
+ async function inspectSitemap(rootDir) {
129
+ const sitemapFiles = await readGeneratedSitemaps(rootDir);
130
+ const sitemapTexts = sitemapFiles.map((file) => file.text);
131
+ if (!sitemapTexts.length) {
132
+ return [];
133
+ }
134
+ const urlCount = sitemapTexts.reduce((total, text) => total + countSitemapUrls(text), 0);
135
+ const hasSitemapIndex = sitemapTexts.some((text) => /<sitemapindex\b/.test(text));
136
+ if (urlCount > 0 || hasSitemapIndex) {
137
+ return [];
138
+ }
139
+ return [{
140
+ severity: "error",
141
+ file: ".output/public/sitemap.xml",
142
+ message: "Generated sitemap contains no <url> entries.",
143
+ suggestion: "Enable content.sitemap, configure site.url, and verify public route collections are sitemap-backed."
144
+ }];
145
+ }
146
+
147
+ const localeCodePattern = /^[a-z]{2}(?:-[A-Z]{2})?$/;
92
148
  function findMatchingBrace(text, start) {
93
149
  let depth = 0;
94
150
  let quote;
@@ -231,188 +287,6 @@ function extractStringArrayProperty(block, property) {
231
287
  }
232
288
  return [...match[1].matchAll(/['"]([^'"]+)['"]/g)].map((item) => item[1]);
233
289
  }
234
- async function collectFiles(dir, rootDir, files = []) {
235
- const entries = await readdir(dir, { withFileTypes: true });
236
- for (const entry of entries) {
237
- const absolutePath = join(dir, entry.name);
238
- if (entry.isDirectory()) {
239
- if (!ignoredDirs.has(entry.name)) {
240
- await collectFiles(absolutePath, rootDir, files);
241
- }
242
- continue;
243
- }
244
- if (entry.isFile() && sourceExtensions.has(extname(entry.name))) {
245
- files.push(absolutePath);
246
- }
247
- }
248
- return files;
249
- }
250
- function packageDependencyFindings(rootDir, packageJson) {
251
- const dependencyBlocks = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
252
- const findings = [];
253
- for (const blockName of dependencyBlocks) {
254
- const block = packageJson[blockName];
255
- if (!block || typeof block !== "object") {
256
- continue;
257
- }
258
- for (const packageName of stalePackageNames) {
259
- if (packageName in block) {
260
- findings.push({
261
- severity: "error",
262
- file: toRelativePath(rootDir, join(rootDir, "package.json")),
263
- message: `Direct dependency "${packageName}" found in ${blockName}.`,
264
- suggestion: packageName === "@nuxt/content" ? "Remove @nuxt/content and install @lupinum/ginko-content." : `Remove ${packageName} unless the app imports it directly. If it is only transitive, keep it out of package.json.`
265
- });
266
- }
267
- }
268
- }
269
- return findings;
270
- }
271
- async function inspectPackageJson(rootDir) {
272
- const path = join(rootDir, "package.json");
273
- if (!existsSync(path)) {
274
- return [];
275
- }
276
- try {
277
- const packageJson = JSON.parse(await readFile(path, "utf8"));
278
- return packageDependencyFindings(rootDir, packageJson);
279
- } catch {
280
- return [{
281
- severity: "error",
282
- file: "package.json",
283
- message: "package.json could not be parsed.",
284
- suggestion: "Fix package.json before running ginko-content doctor again."
285
- }];
286
- }
287
- }
288
- async function inspectSourceFiles(rootDir) {
289
- const files = await collectFiles(rootDir, rootDir);
290
- const findings = [];
291
- for (const file of files) {
292
- if (lockfileNames.includes(file.split("/").pop() || "")) {
293
- continue;
294
- }
295
- const text = await readFile(file, "utf8");
296
- for (const check of sourceChecks) {
297
- if (check.pattern.test(text)) {
298
- findings.push({
299
- severity: "error",
300
- file: toRelativePath(rootDir, file),
301
- message: check.message,
302
- suggestion: check.suggestion
303
- });
304
- }
305
- }
306
- }
307
- return findings;
308
- }
309
- async function inspectSearchCollections(rootDir) {
310
- const contentConfig = await readTextIfPresent(join(rootDir, "content.config.ts"));
311
- const nuxtConfig = await readTextIfPresent(join(rootDir, "nuxt.config.ts"));
312
- if (!contentConfig || !nuxtConfig) {
313
- return [];
314
- }
315
- const dataCollections = new Set(
316
- findCollectionDefinitions(contentConfig).filter((collection) => /\btype\s*:\s*['"]data['"]/.test(collection.block)).map((collection) => collection.name)
317
- );
318
- if (!dataCollections.size) {
319
- return [];
320
- }
321
- const configuredCollections = new Set(
322
- findObjectPropertyBlocks(nuxtConfig, "search").flatMap((block) => extractStringArrayProperty(block, "collections"))
323
- );
324
- const dataSearchCollections = [...configuredCollections].filter((collection) => dataCollections.has(collection));
325
- if (!dataSearchCollections.length) {
326
- return [];
327
- }
328
- return [{
329
- severity: "info",
330
- file: "nuxt.config.ts",
331
- message: `Data-only collections listed in content.search.collections: ${dataSearchCollections.join(", ")}.`,
332
- suggestion: "Remove data-only collections from the static public search index, make them route-backed pages, or use provider-backed search with route-safe results."
333
- }];
334
- }
335
- async function inspectLockfiles(rootDir) {
336
- const findings = [];
337
- for (const lockfileName of lockfileNames) {
338
- const path = join(rootDir, lockfileName);
339
- if (!existsSync(path)) {
340
- continue;
341
- }
342
- const text = await readFile(path, "utf8");
343
- const presentPackages = stalePackageNames.filter((packageName) => text.includes(packageName));
344
- if (presentPackages.length) {
345
- findings.push({
346
- severity: "info",
347
- file: lockfileName,
348
- message: `Lockfile still mentions ${presentPackages.join(", ")}.`,
349
- suggestion: `Run "pnpm why ${presentPackages.join(" ")}" and only act if one is still a direct app dependency.`
350
- });
351
- }
352
- }
353
- return findings;
354
- }
355
- async function readGeneratedSitemaps(rootDir) {
356
- const outputPublicDir = join(rootDir, ".output/public");
357
- if (!existsSync(outputPublicDir)) {
358
- return [];
359
- }
360
- const sitemapPaths = [
361
- join(outputPublicDir, "sitemap.xml"),
362
- join(outputPublicDir, "sitemap_index.xml")
363
- ];
364
- const sitemapDir = join(outputPublicDir, "__sitemap__");
365
- if (existsSync(sitemapDir)) {
366
- const entries = await readdir(sitemapDir, { withFileTypes: true });
367
- for (const entry of entries) {
368
- if (entry.isFile() && entry.name.endsWith(".xml")) {
369
- sitemapPaths.push(join(sitemapDir, entry.name));
370
- }
371
- }
372
- }
373
- const sitemapFiles = [];
374
- for (const sitemapPath of sitemapPaths) {
375
- if (!existsSync(sitemapPath)) {
376
- continue;
377
- }
378
- const fileStat = await stat(sitemapPath);
379
- if (fileStat.isFile()) {
380
- sitemapFiles.push({
381
- file: toRelativePath(rootDir, sitemapPath),
382
- text: await readFile(sitemapPath, "utf8")
383
- });
384
- }
385
- }
386
- return sitemapFiles;
387
- }
388
- async function inspectSitemap(rootDir) {
389
- const sitemapFiles = await readGeneratedSitemaps(rootDir);
390
- const sitemapTexts = sitemapFiles.map((file) => file.text);
391
- if (!sitemapTexts.length) {
392
- return [];
393
- }
394
- const urlCount = sitemapTexts.reduce((total, text) => total + countSitemapUrls(text), 0);
395
- const hasSitemapIndex = sitemapTexts.some((text) => /<sitemapindex\b/.test(text));
396
- if (urlCount > 0 || hasSitemapIndex) {
397
- return [];
398
- }
399
- return [{
400
- severity: "error",
401
- file: ".output/public/sitemap.xml",
402
- message: "Generated sitemap contains no <url> entries.",
403
- suggestion: "Enable content.sitemap, configure site.url, and verify public route collections are sitemap-backed."
404
- }];
405
- }
406
- async function readTextIfPresent(path) {
407
- if (!existsSync(path)) {
408
- return "";
409
- }
410
- const fileStat = await stat(path);
411
- if (!fileStat.isFile()) {
412
- return "";
413
- }
414
- return readFile(path, "utf8");
415
- }
416
290
  function extractLocaleCodesFromConfig(text) {
417
291
  const locales = /* @__PURE__ */ new Set();
418
292
  for (const match of text.matchAll(/\bcode\s*:\s*['"]([^'"]+)['"]/g)) {
@@ -437,26 +311,7 @@ function extractLocaleCodesFromConfig(text) {
437
311
  }
438
312
  return [...locales];
439
313
  }
440
- async function readPackageJson(rootDir) {
441
- const path = join(rootDir, "package.json");
442
- if (!existsSync(path)) {
443
- return void 0;
444
- }
445
- try {
446
- return JSON.parse(await readFile(path, "utf8"));
447
- } catch {
448
- return void 0;
449
- }
450
- }
451
- function hasDependency(packageJson, packageName) {
452
- if (!packageJson) {
453
- return false;
454
- }
455
- return ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"].some((blockName) => {
456
- const block = packageJson[blockName];
457
- return !!block && typeof block === "object" && packageName in block;
458
- });
459
- }
314
+
460
315
  async function detectI18n(rootDir) {
461
316
  const nuxtConfigPath = join(rootDir, "nuxt.config.ts");
462
317
  const contentConfigPath = join(rootDir, "content.config.ts");
@@ -633,22 +488,6 @@ async function inspectI18nDuplicateContentGroups(rootDir, locales) {
633
488
  }
634
489
  return findings;
635
490
  }
636
- async function collectOutputFiles(dir, rootDir, files = []) {
637
- const entries = await readdir(dir, { withFileTypes: true });
638
- for (const entry of entries) {
639
- const absolutePath = join(dir, entry.name);
640
- if (entry.isDirectory()) {
641
- if (entry.name !== "_nuxt") {
642
- await collectOutputFiles(absolutePath, rootDir, files);
643
- }
644
- continue;
645
- }
646
- if (entry.isFile() && [".html", ".json", ".xml"].includes(extname(entry.name))) {
647
- files.push(absolutePath);
648
- }
649
- }
650
- return files;
651
- }
652
491
  async function inspectRepeatedLocalePrefixes(rootDir, locales) {
653
492
  const outputPublicDir = join(rootDir, ".output/public");
654
493
  if (!existsSync(outputPublicDir)) {
@@ -769,23 +608,189 @@ async function inspectI18n(rootDir) {
769
608
  ...await inspectI18nSearchIndex(rootDir, detected.locales)
770
609
  ];
771
610
  }
772
- async function runDoctor(options = {}) {
773
- const rootDir = options.rootDir || process.cwd();
774
- const findings = [
611
+
612
+ const stalePackageNames = ["@nuxt/content", "better-sqlite3", "@standard-schema/spec"];
613
+ const lockfileNames = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
614
+
615
+ function packageDependencyFindings(rootDir, packageJson) {
616
+ const dependencyBlocks = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
617
+ const findings = [];
618
+ for (const blockName of dependencyBlocks) {
619
+ const block = packageJson[blockName];
620
+ if (!block || typeof block !== "object") {
621
+ continue;
622
+ }
623
+ for (const packageName of stalePackageNames) {
624
+ if (packageName in block) {
625
+ findings.push({
626
+ severity: "error",
627
+ file: toRelativePath(rootDir, join(rootDir, "package.json")),
628
+ message: `Direct dependency "${packageName}" found in ${blockName}.`,
629
+ suggestion: packageName === "@nuxt/content" ? "Remove @nuxt/content and install @lupinum/ginko-content." : `Remove ${packageName} unless the app imports it directly. If it is only transitive, keep it out of package.json.`
630
+ });
631
+ }
632
+ }
633
+ }
634
+ return findings;
635
+ }
636
+ async function inspectPackageJson(rootDir) {
637
+ const path = join(rootDir, "package.json");
638
+ if (!existsSync(path)) {
639
+ return [];
640
+ }
641
+ try {
642
+ const packageJson = JSON.parse(await readFile(path, "utf8"));
643
+ return packageDependencyFindings(rootDir, packageJson);
644
+ } catch {
645
+ return [{
646
+ severity: "error",
647
+ file: "package.json",
648
+ message: "package.json could not be parsed.",
649
+ suggestion: "Fix package.json before running ginko-content doctor again."
650
+ }];
651
+ }
652
+ }
653
+ async function inspectLockfiles(rootDir) {
654
+ const findings = [];
655
+ for (const lockfileName of lockfileNames) {
656
+ const path = join(rootDir, lockfileName);
657
+ if (!existsSync(path)) {
658
+ continue;
659
+ }
660
+ const text = await readFile(path, "utf8");
661
+ const presentPackages = stalePackageNames.filter((packageName) => text.includes(packageName));
662
+ if (presentPackages.length) {
663
+ findings.push({
664
+ severity: "info",
665
+ file: lockfileName,
666
+ message: `Lockfile still mentions ${presentPackages.join(", ")}.`,
667
+ suggestion: `Run "pnpm why ${presentPackages.join(" ")}" and only act if one is still a direct app dependency.`
668
+ });
669
+ }
670
+ }
671
+ return findings;
672
+ }
673
+ async function inspectDependencies(rootDir) {
674
+ return [
775
675
  ...await inspectPackageJson(rootDir),
776
- ...await inspectSourceFiles(rootDir),
777
- ...await inspectSearchCollections(rootDir),
778
- ...await inspectLockfiles(rootDir),
779
- ...await inspectSitemap(rootDir),
780
- ...options.i18n ? await inspectI18n(rootDir) : []
781
- ].sort((a, b) => `${a.severity}:${a.file}:${a.message}`.localeCompare(`${b.severity}:${b.file}:${b.message}`));
782
- const exitCode = findings.some((finding) => finding.severity === "error") ? 1 : 0;
783
- return {
784
- rootDir,
785
- findings,
786
- exitCode
787
- };
676
+ ...await inspectLockfiles(rootDir)
677
+ ];
788
678
  }
679
+
680
+ async function inspectSourceChecks(rootDir, checks) {
681
+ const files = await collectFiles(rootDir, rootDir);
682
+ const findings = [];
683
+ for (const file of files) {
684
+ if (lockfileNames.includes(file.split("/").pop() || "")) {
685
+ continue;
686
+ }
687
+ const text = await readFile(file, "utf8");
688
+ for (const check of checks) {
689
+ if (check.pattern.test(text)) {
690
+ findings.push({
691
+ severity: "error",
692
+ file: toRelativePath(rootDir, file),
693
+ message: check.message,
694
+ suggestion: check.suggestion
695
+ });
696
+ }
697
+ }
698
+ }
699
+ return findings;
700
+ }
701
+
702
+ async function inspectPublicApiUsage(rootDir) {
703
+ return inspectSourceChecks(rootDir, [
704
+ {
705
+ pattern: /from\s+['"]@nuxt\/content(?:\/[^'"]*)?['"]|import\s*\([^)]*['"]@nuxt\/content(?:\/[^'"]*)?['"][^)]*\)/,
706
+ message: "Direct Nuxt Content import found.",
707
+ suggestion: "Import Ginko helpers from @lupinum/ginko-content/config, /client, or /server."
708
+ },
709
+ {
710
+ pattern: /['"]@nuxt\/content['"]/,
711
+ message: "Nuxt Content module or package reference found.",
712
+ suggestion: "Remove @nuxt/content from Nuxt modules and use @lupinum/ginko-content."
713
+ },
714
+ {
715
+ pattern: /\bqueryCollectionItemSurroundings\s*\(/,
716
+ message: "Nuxt Content v3 surround helper found.",
717
+ suggestion: "Use useContentPage('docs', { surround: true }) in route page components."
718
+ },
719
+ {
720
+ pattern: /\bqueryCollectionSearchSections\s*\(/,
721
+ message: "Nuxt Content v3 search sections helper found.",
722
+ suggestion: "Use useContentSearchData('docs') for UI search data."
723
+ },
724
+ {
725
+ pattern: /\bqueryCollectionNavigation\s*\(/,
726
+ message: "Nuxt Content v3 navigation helper found.",
727
+ suggestion: "Use useContentNavigation('docs') for layout navigation."
728
+ },
729
+ {
730
+ pattern: /\bqueryCollection\s*\(/,
731
+ message: "Removed collection query helper found.",
732
+ suggestion: "Use one('docs', options), many('docs', options), paginate('docs', options), or the matching useContent* composable."
733
+ },
734
+ {
735
+ pattern: /\buseContentList\s*\(/,
736
+ message: "Removed content list composable found.",
737
+ suggestion: "Use useContentMany('docs', options) or many('docs', options)."
738
+ },
739
+ {
740
+ pattern: /\bcontent\.(database|preview|build)\b/,
741
+ message: "Nuxt Content v3 runtime config key found.",
742
+ suggestion: "Remove content.database/content.preview/content.build and configure Ginko runtime options instead."
743
+ },
744
+ {
745
+ pattern: /\.editor\s*\(/,
746
+ message: "Nuxt Studio Zod .editor(...) helper found.",
747
+ suggestion: "Remove .editor(...) from runtime Zod schemas or move editor metadata outside the schema."
748
+ }
749
+ ]);
750
+ }
751
+
752
+ async function inspectRenderingUsage(rootDir) {
753
+ return inspectSourceChecks(rootDir, [
754
+ {
755
+ pattern: /<ContentRenderer\b[^>]*:value\s*=\s*["'][^"']*\.body["']/,
756
+ message: "ContentRenderer is receiving a document body.",
757
+ suggestion: "Pass the full content document to ContentRenderer, not document.body."
758
+ },
759
+ {
760
+ pattern: /<NuxtLink\b[^>]*:to\s*=\s*["'][^"']+\._path["']/,
761
+ message: "NuxtLink is using raw content _path.",
762
+ suggestion: "Use useContentMany('docs', options) for list pages and bind the route-safe item.path field."
763
+ }
764
+ ]);
765
+ }
766
+
767
+ async function inspectSearchCollections(rootDir) {
768
+ const contentConfig = await readTextIfPresent(join(rootDir, "content.config.ts"));
769
+ const nuxtConfig = await readTextIfPresent(join(rootDir, "nuxt.config.ts"));
770
+ if (!contentConfig || !nuxtConfig) {
771
+ return [];
772
+ }
773
+ const dataCollections = new Set(
774
+ findCollectionDefinitions(contentConfig).filter((collection) => /\btype\s*:\s*['"]data['"]/.test(collection.block)).map((collection) => collection.name)
775
+ );
776
+ if (!dataCollections.size) {
777
+ return [];
778
+ }
779
+ const configuredCollections = new Set(
780
+ findObjectPropertyBlocks(nuxtConfig, "search").flatMap((block) => extractStringArrayProperty(block, "collections"))
781
+ );
782
+ const dataSearchCollections = [...configuredCollections].filter((collection) => dataCollections.has(collection));
783
+ if (!dataSearchCollections.length) {
784
+ return [];
785
+ }
786
+ return [{
787
+ severity: "info",
788
+ file: "nuxt.config.ts",
789
+ message: `Data-only collections listed in content.search.collections: ${dataSearchCollections.join(", ")}.`,
790
+ suggestion: "Remove data-only collections from the static public search index, make them route-backed pages, or use provider-backed search with route-safe results."
791
+ }];
792
+ }
793
+
789
794
  function formatDoctorResult(result) {
790
795
  const errors = result.findings.filter((finding) => finding.severity === "error");
791
796
  const infos = result.findings.filter((finding) => finding.severity === "info");
@@ -811,6 +816,24 @@ function formatDoctorResult(result) {
811
816
  `;
812
817
  }
813
818
 
819
+ async function runDoctor(options = {}) {
820
+ const rootDir = options.rootDir || process.cwd();
821
+ const findings = [
822
+ ...await inspectDependencies(rootDir),
823
+ ...await inspectPublicApiUsage(rootDir),
824
+ ...await inspectRenderingUsage(rootDir),
825
+ ...await inspectSearchCollections(rootDir),
826
+ ...await inspectSitemap(rootDir),
827
+ ...options.i18n ? await inspectI18n(rootDir) : []
828
+ ].sort((a, b) => `${a.severity}:${a.file}:${a.message}`.localeCompare(`${b.severity}:${b.file}:${b.message}`));
829
+ const exitCode = findings.some((finding) => finding.severity === "error") ? 1 : 0;
830
+ return {
831
+ rootDir,
832
+ findings,
833
+ exitCode
834
+ };
835
+ }
836
+
814
837
  const [, , command, ...args] = process.argv;
815
838
  if (!command || command === "--help" || command === "-h") {
816
839
  console.log([
@@ -31,7 +31,11 @@ async function extractEditableSource(id, source) {
31
31
  if (/\.(md|mdc|markdown)$/i.test(id)) {
32
32
  return parseMarkdownEditableSource(source);
33
33
  }
34
- if (/\.json5?$/i.test(id)) {
34
+ if (/\.json5$/i.test(id)) {
35
+ const json5 = await import("json5").then((m) => m.default || m);
36
+ return { frontmatter: normalizeRecord(json5.parse(source)) };
37
+ }
38
+ if (/\.json$/i.test(id)) {
35
39
  return { frontmatter: normalizeRecord(destr(source)) };
36
40
  }
37
41
  if (/\.ya?ml$/i.test(id)) {
package/dist/config.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- export { CollectionSchema, ContentCmsCollectionConfig, ContentCmsFieldConfig, ContentCmsFieldType, ContentCmsRelationConfig, ContentCollectionConfig, ContentCollectionI18nConfig, ContentCollectionKind, ContentCollectionSource, ContentCollectionSourceObject, ContentConfig, ContentReferenceSchema, DefineCollectionObject, DefineCollectionOptions, defineCollection, defineContentConfig, reference } from '../dist/types/config';
1
+ export { AgentMetadataField, AgentMetadataFieldList, CollectionSchema, ContentAgentCollectionConfig, ContentAgentMarkdownOptions, ContentCmsCollectionConfig, ContentCmsFieldConfig, ContentCmsFieldType, ContentCmsRelationConfig, ContentCollectionConfig, ContentCollectionI18nConfig, ContentCollectionKind, ContentCollectionSource, ContentCollectionSourceObject, ContentConfig, ContentReferenceSchema, DefineCollectionObject, DefineCollectionOptions, agentMetadataFields, defineAgentAppPage, defineAgentMarkdownPolicy, defineAgentMetadataFields, defineAgentSection, defineCollection, defineContentConfig, reference } from '../dist/types/config';
2
2
  export { ContentFieldMetadata, ContentFieldSchema, asset, fields, file, getContentFieldMetadata, image, isContentFieldSchema, relation, relations, richtext, text } from '../dist/types/fields';
3
3
  export { SlugifyUrlSegmentOptions, slugifyUrlSegment } from '../dist/core/content/slug';
package/dist/config.mjs CHANGED
@@ -1,3 +1,3 @@
1
- export { defineCollection, defineContentConfig, reference } from '../dist/types/config';
1
+ export { agentMetadataFields, defineAgentAppPage, defineAgentMarkdownPolicy, defineAgentMetadataFields, defineAgentSection, defineCollection, defineContentConfig, reference } from '../dist/types/config';
2
2
  export { asset, fields, file, getContentFieldMetadata, image, isContentFieldSchema, relation, relations, richtext, text } from '../dist/types/fields';
3
3
  export { slugifyUrlSegment } from '../dist/core/content/slug';