@lupinum/ginko-content 0.1.1 → 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 (200) hide show
  1. package/README.md +34 -4
  2. package/compatibility.json +2 -2
  3. package/dist/cli.mjs +373 -278
  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/core/references/schema.d.ts +1 -0
  18. package/dist/core/references/schema.js +33 -10
  19. package/dist/features/navigation/build.js +14 -6
  20. package/dist/features/navigation/canonical.js +1 -1
  21. package/dist/features/sitemap/query.js +2 -2
  22. package/dist/integrations/nitro/ingest.d.ts +3 -3
  23. package/dist/integrations/nitro/ingest.js +3 -3
  24. package/dist/integrations/nitro/storage.js +3 -1
  25. package/dist/module.d.mts +216 -532
  26. package/dist/module.json +1 -1
  27. package/dist/module.mjs +1250 -718
  28. package/dist/parsers/csv.js +4 -3
  29. package/dist/parsers/index.d.ts +2 -5
  30. package/dist/parsers/index.js +2 -2
  31. package/dist/parsers/markdown-plugins.d.ts +1 -0
  32. package/dist/parsers/markdown-plugins.js +83 -1
  33. package/dist/parsers/markdown.js +1 -1
  34. package/dist/public/client.d.ts +5 -2
  35. package/dist/public/client.js +4 -2
  36. package/dist/public/provider-errors.d.ts +2 -1
  37. package/dist/public/provider-errors.js +1 -18
  38. package/dist/public/provider.d.ts +3 -10
  39. package/dist/public/server.d.ts +6 -1
  40. package/dist/public/server.js +33 -0
  41. package/dist/runtime/agent-paths +1 -0
  42. package/dist/runtime/agent-paths.d.ts +6 -0
  43. package/dist/runtime/agent-paths.js +41 -0
  44. package/dist/runtime/app/components/ContentRenderer.vue +9 -5
  45. package/dist/runtime/app/components/internal/MarkdownRenderer.d.ts +1 -1
  46. package/dist/runtime/app/composables/head.d.ts +30 -3
  47. package/dist/runtime/app/composables/head.js +83 -78
  48. package/dist/runtime/app/composables/hot-reload +1 -0
  49. package/dist/runtime/app/composables/hot-reload.d.ts +5 -0
  50. package/dist/runtime/app/composables/hot-reload.js +15 -0
  51. package/dist/runtime/app/composables/locale-context.d.ts +1 -1
  52. package/dist/runtime/app/composables/locale-context.js +2 -1
  53. package/dist/runtime/app/composables/locale.js +2 -2
  54. package/dist/runtime/app/composables/query-api.d.ts +9 -10
  55. package/dist/runtime/app/composables/search.d.ts +4 -4
  56. package/dist/runtime/app/composables/search.js +19 -9
  57. package/dist/runtime/app/composables/use-content-document +1 -0
  58. package/dist/runtime/app/composables/use-content-document.d.ts +29 -0
  59. package/dist/runtime/app/composables/use-content-document.js +44 -0
  60. package/dist/runtime/app/composables/use-content-list +1 -0
  61. package/dist/runtime/app/composables/use-content-list.d.ts +43 -0
  62. package/dist/runtime/app/composables/use-content-list.js +98 -0
  63. package/dist/runtime/app/composables/use-content-navigation +1 -0
  64. package/dist/runtime/app/composables/use-content-navigation.d.ts +40 -0
  65. package/dist/runtime/app/composables/use-content-navigation.js +102 -0
  66. package/dist/runtime/app/composables/use-content-page +1 -0
  67. package/dist/runtime/app/composables/use-content-page.d.ts +47 -0
  68. package/dist/runtime/app/composables/use-content-page.js +142 -0
  69. package/dist/runtime/app/composables/use-content-shared +1 -0
  70. package/dist/runtime/app/composables/use-content-shared.d.ts +13 -0
  71. package/dist/runtime/app/composables/use-content-shared.js +29 -0
  72. package/dist/runtime/app/composables/use-content.d.ts +11 -186
  73. package/dist/runtime/app/composables/use-content.js +4 -307
  74. package/dist/runtime/app/plugins/hot-reload +1 -0
  75. package/dist/runtime/app/plugins/hot-reload.js +6 -0
  76. package/dist/runtime/query/backlinks +1 -0
  77. package/dist/runtime/query/backlinks.d.ts +8 -0
  78. package/dist/runtime/query/backlinks.js +90 -0
  79. package/dist/runtime/query/context +1 -0
  80. package/dist/runtime/query/context.d.ts +20 -0
  81. package/dist/runtime/query/context.js +0 -0
  82. package/dist/runtime/query/documents +1 -0
  83. package/dist/runtime/query/documents.d.ts +6 -0
  84. package/dist/runtime/query/documents.js +98 -0
  85. package/dist/runtime/query/errors +1 -0
  86. package/dist/runtime/query/errors.d.ts +1 -0
  87. package/dist/runtime/query/errors.js +4 -0
  88. package/dist/runtime/query/handles +1 -0
  89. package/dist/runtime/query/handles.d.ts +2 -0
  90. package/dist/runtime/query/handles.js +7 -0
  91. package/dist/runtime/query/locale-options +1 -0
  92. package/dist/runtime/query/locale-options.d.ts +5 -0
  93. package/dist/runtime/query/locale-options.js +14 -0
  94. package/dist/runtime/query/localized-docs +1 -0
  95. package/dist/runtime/query/localized-docs.d.ts +17 -0
  96. package/dist/runtime/query/localized-docs.js +44 -0
  97. package/dist/runtime/query/navigation +1 -0
  98. package/dist/runtime/query/navigation.d.ts +22 -0
  99. package/dist/runtime/query/navigation.js +73 -0
  100. package/dist/runtime/query/pagination +1 -0
  101. package/dist/runtime/query/pagination.d.ts +7 -0
  102. package/dist/runtime/query/pagination.js +78 -0
  103. package/dist/runtime/query/populate +1 -0
  104. package/dist/runtime/query/populate.d.ts +9 -0
  105. package/dist/runtime/query/populate.js +106 -0
  106. package/dist/runtime/query/responses +1 -0
  107. package/dist/runtime/query/responses.d.ts +10 -0
  108. package/dist/runtime/query/responses.js +47 -0
  109. package/dist/runtime/query/unified.d.ts +4 -19
  110. package/dist/runtime/query/unified.js +14 -457
  111. package/dist/runtime/query/variants +1 -0
  112. package/dist/runtime/query/variants.d.ts +11 -0
  113. package/dist/runtime/query/variants.js +36 -0
  114. package/dist/runtime/server/agent-http +1 -0
  115. package/dist/runtime/server/agent-http.d.ts +7 -0
  116. package/dist/runtime/server/agent-http.js +36 -0
  117. package/dist/runtime/server/agent-markdown +1 -0
  118. package/dist/runtime/server/agent-markdown.d.ts +83 -0
  119. package/dist/runtime/server/agent-markdown.js +449 -0
  120. package/dist/runtime/server/agent-site +1 -0
  121. package/dist/runtime/server/agent-site.d.ts +38 -0
  122. package/dist/runtime/server/agent-site.js +273 -0
  123. package/dist/runtime/server/api/agent-llms +1 -0
  124. package/dist/runtime/server/api/agent-llms-full +1 -0
  125. package/dist/runtime/server/api/agent-llms-full.d.ts +2 -0
  126. package/dist/runtime/server/api/agent-llms-full.js +13 -0
  127. package/dist/runtime/server/api/agent-llms.d.ts +2 -0
  128. package/dist/runtime/server/api/agent-llms.js +14 -0
  129. package/dist/runtime/server/api/agent-raw +1 -0
  130. package/dist/runtime/server/api/agent-raw.d.ts +2 -0
  131. package/dist/runtime/server/api/agent-raw.js +17 -0
  132. package/dist/runtime/server/api/locales.d.ts +2 -2
  133. package/dist/runtime/server/api/locales.js +4 -2
  134. package/dist/runtime/server/api/navigation.d.ts +1 -1
  135. package/dist/runtime/server/api/query.d.ts +1 -1
  136. package/dist/runtime/server/api/query.js +4 -1
  137. package/dist/runtime/server/api/revalidate.js +2 -2
  138. package/dist/runtime/server/api/search.d.ts +2 -1
  139. package/dist/runtime/server/api/search.js +6 -2
  140. package/dist/runtime/server/api/site-data.d.ts +1 -1
  141. package/dist/runtime/server/collection-helpers.js +2 -1
  142. package/dist/runtime/server/middleware/agent-link-headers +1 -0
  143. package/dist/runtime/server/middleware/agent-link-headers.d.ts +2 -0
  144. package/dist/runtime/server/middleware/agent-link-headers.js +42 -0
  145. package/dist/runtime/server/middleware/agent-markdown +1 -0
  146. package/dist/runtime/server/middleware/agent-markdown.d.ts +2 -0
  147. package/dist/runtime/server/middleware/agent-markdown.js +43 -0
  148. package/dist/runtime/server/pagefind.js +1 -0
  149. package/dist/runtime/server/plugins/sitemap.js +1 -0
  150. package/dist/runtime/server/provider-query.d.ts +8 -5
  151. package/dist/runtime/server/provider-query.js +50 -8
  152. package/dist/runtime/server/provider-result.d.ts +19 -2
  153. package/dist/runtime/server/providers/filesystem.js +4 -1
  154. package/dist/runtime/server/providers/index.d.ts +3 -1
  155. package/dist/runtime/server/providers/index.js +2 -2
  156. package/dist/runtime/server/query-api.js +2 -1
  157. package/dist/runtime/server/query-executor.d.ts +3 -2
  158. package/dist/runtime/server/query-executor.js +13 -5
  159. package/dist/runtime/server/search.d.ts +11 -0
  160. package/dist/runtime/server/search.js +24 -7
  161. package/dist/runtime/server/sitemap-provider.js +40 -1
  162. package/dist/runtime/server/sitemap.js +3 -2
  163. package/dist/runtime/shared/search.js +3 -2
  164. package/dist/runtime/transformers/component-resolver.js +2 -1
  165. package/dist/runtime/utils/content-components.d.ts +0 -5
  166. package/dist/storage/content.d.ts +2 -9
  167. package/dist/storage/references.d.ts +1 -8
  168. package/dist/storage/references.js +9 -4
  169. package/dist/testing/provider-contract.js +3 -4
  170. package/dist/testing/provider-fixture.d.ts +3 -1
  171. package/dist/testing/provider-fixture.js +28 -14
  172. package/dist/types/config.d.ts +104 -7
  173. package/dist/types/config.js +49 -2
  174. package/dist/types/module.d.ts +50 -8
  175. package/dist/types/query-parts/collections +1 -0
  176. package/dist/types/query-parts/collections.d.ts +15 -0
  177. package/dist/types/query-parts/collections.js +0 -0
  178. package/dist/types/query-parts/public +1 -0
  179. package/dist/types/query-parts/public.d.ts +202 -0
  180. package/dist/types/query-parts/public.js +0 -0
  181. package/dist/types/query-parts/results +1 -0
  182. package/dist/types/query-parts/results.d.ts +95 -0
  183. package/dist/types/query-parts/results.js +0 -0
  184. package/dist/types/query-parts/transport +1 -0
  185. package/dist/types/query-parts/transport.d.ts +103 -0
  186. package/dist/types/query-parts/transport.js +0 -0
  187. package/dist/types/query.d.ts +4 -862
  188. package/dist/types/query.js +4 -0
  189. package/dist/types/runtime.d.ts +2 -2
  190. package/dist/types/search.d.ts +3 -1
  191. package/dist/types/virtual.d.ts +54 -0
  192. package/dist/types.d.mts +3 -3
  193. package/dist/web-types.json +1 -1
  194. package/package.json +8 -8
  195. package/dist/runtime/app/composables/web-socket +0 -1
  196. package/dist/runtime/app/composables/web-socket.d.ts +0 -3
  197. package/dist/runtime/app/composables/web-socket.js +0 -65
  198. package/dist/runtime/app/plugins/ws +0 -1
  199. package/dist/runtime/app/plugins/ws.js +0 -7
  200. /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,78 +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 useContentNeighbors(handle, { by: { route } }) in route page components."
46
- },
47
- {
48
- pattern: /\bqueryCollectionSearchSections\s*\(/,
49
- message: "Nuxt Content v3 search sections helper found.",
50
- suggestion: "Use useContentSearchData(collection) for UI search data."
51
- },
52
- {
53
- pattern: /\bqueryCollectionNavigation\s*\(/,
54
- message: "Nuxt Content v3 navigation helper found.",
55
- suggestion: "Use useContentTree(handle) for layout navigation."
56
- },
57
- {
58
- pattern: /\bqueryCollection\s*\(/,
59
- message: "Removed collection query helper found.",
60
- suggestion: "Use one(handle, options), many(handle, options), paginate(handle, options), or the matching useContent* composable."
61
- },
62
- {
63
- pattern: /\buseContentList\s*\(/,
64
- message: "Removed content list composable found.",
65
- suggestion: "Use useContentMany(handle, options) or many(handle, options)."
66
- },
67
- {
68
- pattern: /\buseContentNavigation\s*\(/,
69
- message: "Removed content navigation composable found.",
70
- suggestion: "Use useContentTree(handle) for layout navigation."
71
- },
72
- {
73
- pattern: /\bcontent\.(database|preview|build)\b/,
74
- message: "Nuxt Content v3 runtime config key found.",
75
- suggestion: "Remove content.database/content.preview/content.build and configure Ginko runtime options instead."
76
- },
77
- {
78
- pattern: /\.editor\s*\(/,
79
- message: "Nuxt Studio Zod .editor(...) helper found.",
80
- suggestion: "Remove .editor(...) from runtime Zod schemas or move editor metadata outside the schema."
81
- },
82
- {
83
- pattern: /<ContentRenderer\b[^>]*:value\s*=\s*["'][^"']*\.body["']/,
84
- message: "ContentRenderer is receiving a document body.",
85
- suggestion: "Pass the full content document to ContentRenderer, not document.body."
86
- },
87
- {
88
- pattern: /<NuxtLink\b[^>]*:to\s*=\s*["'][^"']+\.path["']/,
89
- message: "NuxtLink may be using raw query .path.",
90
- suggestion: "Use useContentMany(handle, options) for route-safe list items, or link route-page payloads with their explicit path field."
91
- },
92
- {
93
- pattern: /<NuxtLink\b[^>]*:to\s*=\s*["'][^"']+\._path["']/,
94
- message: "NuxtLink is using raw content _path.",
95
- suggestion: "Use useContentMany(handle, options) for list pages and bind the route-safe item.path field."
96
- }
97
- ];
98
- const lockfileNames = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
99
- const localeCodePattern = /^[a-z]{2}(?:-[A-Z]{2})?$/;
100
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
+
101
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})?$/;
102
148
  function findMatchingBrace(text, start) {
103
149
  let depth = 0;
104
150
  let quote;
@@ -156,191 +202,90 @@ function findMatchingBrace(text, start) {
156
202
  }
157
203
  return text.length - 1;
158
204
  }
159
- function findCollectionDefinitions(text) {
160
- const definitions = [];
205
+ function findAuthoredCollectionDefinitions(text) {
206
+ const definitions = /* @__PURE__ */ new Map();
161
207
  const callPattern = /\bdefineCollection\s*\(/g;
162
208
  for (const match of text.matchAll(callPattern)) {
163
209
  const callStart = match.index || 0;
164
210
  const argsStart = callStart + match[0].length;
165
211
  const args = text.slice(argsStart);
166
- const namedMatch = args.match(/^\s*(['"])([^'"]+)\1\s*,\s*\{/);
167
- if (namedMatch) {
168
- const bodyStart = argsStart + namedMatch[0].lastIndexOf("{");
169
- const bodyEnd = findMatchingBrace(text, bodyStart);
170
- definitions.push({
171
- name: namedMatch[2],
172
- block: text.slice(bodyStart, bodyEnd + 1)
173
- });
212
+ const objectMatch = args.match(/^\s*\{/);
213
+ if (!objectMatch) {
174
214
  continue;
175
215
  }
176
- const objectMatch = args.match(/^\s*\{/);
177
- const propertyMatch = text.slice(0, callStart).match(/([a-z_$][\w$]*)\s*:\s*$/i);
178
- if (objectMatch && propertyMatch) {
179
- const bodyStart = argsStart + objectMatch[0].lastIndexOf("{");
180
- const bodyEnd = findMatchingBrace(text, bodyStart);
181
- definitions.push({
182
- name: propertyMatch[1],
183
- block: text.slice(bodyStart, bodyEnd + 1)
216
+ const bodyStart = argsStart + objectMatch[0].lastIndexOf("{");
217
+ const bodyEnd = findMatchingBrace(text, bodyStart);
218
+ const block = text.slice(bodyStart, bodyEnd + 1);
219
+ const prefix = text.slice(0, callStart);
220
+ const propertyMatch = prefix.match(/([a-z_$][\w$]*)\s*:\s*$/i);
221
+ if (propertyMatch) {
222
+ definitions.set(propertyMatch[1], {
223
+ key: propertyMatch[1],
224
+ block
184
225
  });
185
- }
186
- }
187
- return definitions;
188
- }
189
- async function collectFiles(dir, rootDir, files = []) {
190
- const entries = await readdir(dir, { withFileTypes: true });
191
- for (const entry of entries) {
192
- const absolutePath = join(dir, entry.name);
193
- if (entry.isDirectory()) {
194
- if (!ignoredDirs.has(entry.name)) {
195
- await collectFiles(absolutePath, rootDir, files);
196
- }
197
226
  continue;
198
227
  }
199
- if (entry.isFile() && sourceExtensions.has(extname(entry.name))) {
200
- files.push(absolutePath);
228
+ const variableMatch = prefix.match(/(?:^|[\s;])(?:export\s+)?(?:const|let|var)\s+([a-z_$][\w$]*)\s*=\s*$/i);
229
+ if (variableMatch) {
230
+ definitions.set(variableMatch[1], { block });
201
231
  }
202
232
  }
203
- return files;
233
+ return definitions;
204
234
  }
205
- function packageDependencyFindings(rootDir, packageJson) {
206
- const dependencyBlocks = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
207
- const findings = [];
208
- for (const blockName of dependencyBlocks) {
209
- const block = packageJson[blockName];
210
- if (!block || typeof block !== "object") {
211
- continue;
212
- }
213
- for (const packageName of stalePackageNames) {
214
- if (packageName in block) {
215
- findings.push({
216
- severity: "error",
217
- file: toRelativePath(rootDir, join(rootDir, "package.json")),
218
- message: `Direct dependency "${packageName}" found in ${blockName}.`,
219
- 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.`
235
+ function findCollectionDefinitions(text) {
236
+ const authoredCollections = findAuthoredCollectionDefinitions(text);
237
+ const definitions = /* @__PURE__ */ new Map();
238
+ for (const block of findObjectPropertyBlocks(text, "collections")) {
239
+ for (const match of block.matchAll(/([a-z_$][\w$]*)\s*:\s*([a-z_$][\w$]*)\b/g)) {
240
+ const [, key, identifier] = match;
241
+ const authored = authoredCollections.get(identifier);
242
+ if (authored) {
243
+ definitions.set(key, {
244
+ name: key,
245
+ block: authored.block
220
246
  });
221
247
  }
222
248
  }
223
- }
224
- return findings;
225
- }
226
- async function inspectPackageJson(rootDir) {
227
- const path = join(rootDir, "package.json");
228
- if (!existsSync(path)) {
229
- return [];
230
- }
231
- try {
232
- const packageJson = JSON.parse(await readFile(path, "utf8"));
233
- return packageDependencyFindings(rootDir, packageJson);
234
- } catch {
235
- return [{
236
- severity: "error",
237
- file: "package.json",
238
- message: "package.json could not be parsed.",
239
- suggestion: "Fix package.json before running ginko-content doctor again."
240
- }];
241
- }
242
- }
243
- async function inspectSourceFiles(rootDir) {
244
- const files = await collectFiles(rootDir, rootDir);
245
- const findings = [];
246
- for (const file of files) {
247
- if (lockfileNames.includes(file.split("/").pop() || "")) {
248
- continue;
249
- }
250
- const text = await readFile(file, "utf8");
251
- for (const check of sourceChecks) {
252
- if (check.pattern.test(text)) {
253
- findings.push({
254
- severity: "error",
255
- file: toRelativePath(rootDir, file),
256
- message: check.message,
257
- suggestion: check.suggestion
249
+ for (const match of block.matchAll(/(?:^|[,{]\s*)([a-z_$][\w$]*)\s*(?=,|\})/g)) {
250
+ const key = match[1];
251
+ const authored = authoredCollections.get(key);
252
+ if (authored) {
253
+ definitions.set(key, {
254
+ name: key,
255
+ block: authored.block
258
256
  });
259
257
  }
260
258
  }
261
259
  }
262
- return findings;
263
- }
264
- async function inspectLockfiles(rootDir) {
265
- const findings = [];
266
- for (const lockfileName of lockfileNames) {
267
- const path = join(rootDir, lockfileName);
268
- if (!existsSync(path)) {
269
- continue;
270
- }
271
- const text = await readFile(path, "utf8");
272
- const presentPackages = stalePackageNames.filter((packageName) => text.includes(packageName));
273
- if (presentPackages.length) {
274
- findings.push({
275
- severity: "info",
276
- file: lockfileName,
277
- message: `Lockfile still mentions ${presentPackages.join(", ")}.`,
278
- suggestion: `Run "pnpm why ${presentPackages.join(" ")}" and only act if one is still a direct app dependency.`
260
+ for (const authored of authoredCollections.values()) {
261
+ if (authored.key) {
262
+ definitions.set(authored.key, {
263
+ name: authored.key,
264
+ block: authored.block
279
265
  });
280
266
  }
281
267
  }
282
- return findings;
268
+ return [...definitions.values()];
283
269
  }
284
- async function readGeneratedSitemaps(rootDir) {
285
- const outputPublicDir = join(rootDir, ".output/public");
286
- if (!existsSync(outputPublicDir)) {
287
- return [];
288
- }
289
- const sitemapPaths = [
290
- join(outputPublicDir, "sitemap.xml"),
291
- join(outputPublicDir, "sitemap_index.xml")
292
- ];
293
- const sitemapDir = join(outputPublicDir, "__sitemap__");
294
- if (existsSync(sitemapDir)) {
295
- const entries = await readdir(sitemapDir, { withFileTypes: true });
296
- for (const entry of entries) {
297
- if (entry.isFile() && entry.name.endsWith(".xml")) {
298
- sitemapPaths.push(join(sitemapDir, entry.name));
299
- }
300
- }
301
- }
302
- const sitemapFiles = [];
303
- for (const sitemapPath of sitemapPaths) {
304
- if (!existsSync(sitemapPath)) {
270
+ function findObjectPropertyBlocks(text, property) {
271
+ const blocks = [];
272
+ const pattern = new RegExp(`\\b${property}\\s*:\\s*\\{`, "g");
273
+ for (const match of text.matchAll(pattern)) {
274
+ const bodyStart = text.indexOf("{", match.index);
275
+ if (bodyStart === -1) {
305
276
  continue;
306
277
  }
307
- const fileStat = await stat(sitemapPath);
308
- if (fileStat.isFile()) {
309
- sitemapFiles.push({
310
- file: toRelativePath(rootDir, sitemapPath),
311
- text: await readFile(sitemapPath, "utf8")
312
- });
313
- }
278
+ const bodyEnd = findMatchingBrace(text, bodyStart);
279
+ blocks.push(text.slice(bodyStart, bodyEnd + 1));
314
280
  }
315
- return sitemapFiles;
281
+ return blocks;
316
282
  }
317
- async function inspectSitemap(rootDir) {
318
- const sitemapFiles = await readGeneratedSitemaps(rootDir);
319
- const sitemapTexts = sitemapFiles.map((file) => file.text);
320
- if (!sitemapTexts.length) {
321
- return [];
322
- }
323
- const urlCount = sitemapTexts.reduce((total, text) => total + countSitemapUrls(text), 0);
324
- const hasSitemapIndex = sitemapTexts.some((text) => /<sitemapindex\b/.test(text));
325
- if (urlCount > 0 || hasSitemapIndex) {
283
+ function extractStringArrayProperty(block, property) {
284
+ const match = block.match(new RegExp(`\\b${property}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
285
+ if (!match) {
326
286
  return [];
327
287
  }
328
- return [{
329
- severity: "error",
330
- file: ".output/public/sitemap.xml",
331
- message: "Generated sitemap contains no <url> entries.",
332
- suggestion: "Enable content.sitemap, configure site.url, and verify public route collections are sitemap-backed."
333
- }];
334
- }
335
- async function readTextIfPresent(path) {
336
- if (!existsSync(path)) {
337
- return "";
338
- }
339
- const fileStat = await stat(path);
340
- if (!fileStat.isFile()) {
341
- return "";
342
- }
343
- return readFile(path, "utf8");
288
+ return [...match[1].matchAll(/['"]([^'"]+)['"]/g)].map((item) => item[1]);
344
289
  }
345
290
  function extractLocaleCodesFromConfig(text) {
346
291
  const locales = /* @__PURE__ */ new Set();
@@ -366,26 +311,7 @@ function extractLocaleCodesFromConfig(text) {
366
311
  }
367
312
  return [...locales];
368
313
  }
369
- async function readPackageJson(rootDir) {
370
- const path = join(rootDir, "package.json");
371
- if (!existsSync(path)) {
372
- return void 0;
373
- }
374
- try {
375
- return JSON.parse(await readFile(path, "utf8"));
376
- } catch {
377
- return void 0;
378
- }
379
- }
380
- function hasDependency(packageJson, packageName) {
381
- if (!packageJson) {
382
- return false;
383
- }
384
- return ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"].some((blockName) => {
385
- const block = packageJson[blockName];
386
- return !!block && typeof block === "object" && packageName in block;
387
- });
388
- }
314
+
389
315
  async function detectI18n(rootDir) {
390
316
  const nuxtConfigPath = join(rootDir, "nuxt.config.ts");
391
317
  const contentConfigPath = join(rootDir, "content.config.ts");
@@ -510,7 +436,7 @@ async function inspectI18nSourceSmells(rootDir, locales) {
510
436
  severity: "error",
511
437
  file: relativeFile,
512
438
  message: "Hardcoded locale route branch found.",
513
- suggestion: "Use @nuxtjs/i18n route helpers, useContentTree(handle), or useContentMany(handle, options) item paths instead of branching on locale codes."
439
+ suggestion: "Use @nuxtjs/i18n route helpers, useContentNavigation('docs'), or useContentMany('docs', options) item paths instead of branching on locale codes."
514
440
  });
515
441
  }
516
442
  if (/<(?:NuxtLink|U[A-Za-z]+)\b[^>]*:to\s*=\s*["'][^"']+\._path["']/.test(text)) {
@@ -518,7 +444,7 @@ async function inspectI18nSourceSmells(rootDir, locales) {
518
444
  severity: "error",
519
445
  file: relativeFile,
520
446
  message: "UI link is bound to raw content _path.",
521
- suggestion: "Use useContentMany(handle, options) for localized list pages and bind item.path."
447
+ suggestion: "Use useContentMany('docs', options) for localized list pages and bind item.path."
522
448
  });
523
449
  }
524
450
  }
@@ -562,22 +488,6 @@ async function inspectI18nDuplicateContentGroups(rootDir, locales) {
562
488
  }
563
489
  return findings;
564
490
  }
565
- async function collectOutputFiles(dir, rootDir, files = []) {
566
- const entries = await readdir(dir, { withFileTypes: true });
567
- for (const entry of entries) {
568
- const absolutePath = join(dir, entry.name);
569
- if (entry.isDirectory()) {
570
- if (entry.name !== "_nuxt") {
571
- await collectOutputFiles(absolutePath, rootDir, files);
572
- }
573
- continue;
574
- }
575
- if (entry.isFile() && [".html", ".json", ".xml"].includes(extname(entry.name))) {
576
- files.push(absolutePath);
577
- }
578
- }
579
- return files;
580
- }
581
491
  async function inspectRepeatedLocalePrefixes(rootDir, locales) {
582
492
  const outputPublicDir = join(rootDir, ".output/public");
583
493
  if (!existsSync(outputPublicDir)) {
@@ -698,22 +608,189 @@ async function inspectI18n(rootDir) {
698
608
  ...await inspectI18nSearchIndex(rootDir, detected.locales)
699
609
  ];
700
610
  }
701
- async function runDoctor(options = {}) {
702
- const rootDir = options.rootDir || process.cwd();
703
- 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 [
704
675
  ...await inspectPackageJson(rootDir),
705
- ...await inspectSourceFiles(rootDir),
706
- ...await inspectLockfiles(rootDir),
707
- ...await inspectSitemap(rootDir),
708
- ...options.i18n ? await inspectI18n(rootDir) : []
709
- ].sort((a, b) => `${a.severity}:${a.file}:${a.message}`.localeCompare(`${b.severity}:${b.file}:${b.message}`));
710
- const exitCode = findings.some((finding) => finding.severity === "error") ? 1 : 0;
711
- return {
712
- rootDir,
713
- findings,
714
- exitCode
715
- };
676
+ ...await inspectLockfiles(rootDir)
677
+ ];
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
+ ]);
716
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
+
717
794
  function formatDoctorResult(result) {
718
795
  const errors = result.findings.filter((finding) => finding.severity === "error");
719
796
  const infos = result.findings.filter((finding) => finding.severity === "info");
@@ -739,6 +816,24 @@ function formatDoctorResult(result) {
739
816
  `;
740
817
  }
741
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
+
742
837
  const [, , command, ...args] = process.argv;
743
838
  if (!command || command === "--help" || command === "-h") {
744
839
  console.log([