@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.
- package/README.md +34 -4
- package/compatibility.json +2 -2
- package/dist/cli.mjs +373 -278
- package/dist/cms-import/index.js +5 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +1 -1
- package/dist/core/cache-hints.d.ts +9 -1
- package/dist/core/content/collection.js +1 -1
- package/dist/core/content/path.js +1 -1
- package/dist/core/errors.js +5 -1
- package/dist/core/provider-errors +1 -0
- package/dist/core/provider-errors.d.ts +10 -0
- package/dist/core/provider-errors.js +27 -0
- package/dist/core/query/execute.js +12 -3
- package/dist/core/query/filter.js +2 -2
- package/dist/core/query/lower.js +5 -4
- package/dist/core/references/schema.d.ts +1 -0
- package/dist/core/references/schema.js +33 -10
- package/dist/features/navigation/build.js +14 -6
- package/dist/features/navigation/canonical.js +1 -1
- package/dist/features/sitemap/query.js +2 -2
- package/dist/integrations/nitro/ingest.d.ts +3 -3
- package/dist/integrations/nitro/ingest.js +3 -3
- package/dist/integrations/nitro/storage.js +3 -1
- package/dist/module.d.mts +216 -532
- package/dist/module.json +1 -1
- package/dist/module.mjs +1250 -718
- package/dist/parsers/csv.js +4 -3
- package/dist/parsers/index.d.ts +2 -5
- package/dist/parsers/index.js +2 -2
- package/dist/parsers/markdown-plugins.d.ts +1 -0
- package/dist/parsers/markdown-plugins.js +83 -1
- package/dist/parsers/markdown.js +1 -1
- package/dist/public/client.d.ts +5 -2
- package/dist/public/client.js +4 -2
- package/dist/public/provider-errors.d.ts +2 -1
- package/dist/public/provider-errors.js +1 -18
- package/dist/public/provider.d.ts +3 -10
- package/dist/public/server.d.ts +6 -1
- package/dist/public/server.js +33 -0
- package/dist/runtime/agent-paths +1 -0
- package/dist/runtime/agent-paths.d.ts +6 -0
- package/dist/runtime/agent-paths.js +41 -0
- package/dist/runtime/app/components/ContentRenderer.vue +9 -5
- package/dist/runtime/app/components/internal/MarkdownRenderer.d.ts +1 -1
- package/dist/runtime/app/composables/head.d.ts +30 -3
- package/dist/runtime/app/composables/head.js +83 -78
- package/dist/runtime/app/composables/hot-reload +1 -0
- package/dist/runtime/app/composables/hot-reload.d.ts +5 -0
- package/dist/runtime/app/composables/hot-reload.js +15 -0
- package/dist/runtime/app/composables/locale-context.d.ts +1 -1
- package/dist/runtime/app/composables/locale-context.js +2 -1
- package/dist/runtime/app/composables/locale.js +2 -2
- package/dist/runtime/app/composables/query-api.d.ts +9 -10
- package/dist/runtime/app/composables/search.d.ts +4 -4
- package/dist/runtime/app/composables/search.js +19 -9
- package/dist/runtime/app/composables/use-content-document +1 -0
- package/dist/runtime/app/composables/use-content-document.d.ts +29 -0
- package/dist/runtime/app/composables/use-content-document.js +44 -0
- package/dist/runtime/app/composables/use-content-list +1 -0
- package/dist/runtime/app/composables/use-content-list.d.ts +43 -0
- package/dist/runtime/app/composables/use-content-list.js +98 -0
- package/dist/runtime/app/composables/use-content-navigation +1 -0
- package/dist/runtime/app/composables/use-content-navigation.d.ts +40 -0
- package/dist/runtime/app/composables/use-content-navigation.js +102 -0
- package/dist/runtime/app/composables/use-content-page +1 -0
- package/dist/runtime/app/composables/use-content-page.d.ts +47 -0
- package/dist/runtime/app/composables/use-content-page.js +142 -0
- package/dist/runtime/app/composables/use-content-shared +1 -0
- package/dist/runtime/app/composables/use-content-shared.d.ts +13 -0
- package/dist/runtime/app/composables/use-content-shared.js +29 -0
- package/dist/runtime/app/composables/use-content.d.ts +11 -186
- package/dist/runtime/app/composables/use-content.js +4 -307
- package/dist/runtime/app/plugins/hot-reload +1 -0
- package/dist/runtime/app/plugins/hot-reload.js +6 -0
- package/dist/runtime/query/backlinks +1 -0
- package/dist/runtime/query/backlinks.d.ts +8 -0
- package/dist/runtime/query/backlinks.js +90 -0
- package/dist/runtime/query/context +1 -0
- package/dist/runtime/query/context.d.ts +20 -0
- package/dist/runtime/query/context.js +0 -0
- package/dist/runtime/query/documents +1 -0
- package/dist/runtime/query/documents.d.ts +6 -0
- package/dist/runtime/query/documents.js +98 -0
- package/dist/runtime/query/errors +1 -0
- package/dist/runtime/query/errors.d.ts +1 -0
- package/dist/runtime/query/errors.js +4 -0
- package/dist/runtime/query/handles +1 -0
- package/dist/runtime/query/handles.d.ts +2 -0
- package/dist/runtime/query/handles.js +7 -0
- package/dist/runtime/query/locale-options +1 -0
- package/dist/runtime/query/locale-options.d.ts +5 -0
- package/dist/runtime/query/locale-options.js +14 -0
- package/dist/runtime/query/localized-docs +1 -0
- package/dist/runtime/query/localized-docs.d.ts +17 -0
- package/dist/runtime/query/localized-docs.js +44 -0
- package/dist/runtime/query/navigation +1 -0
- package/dist/runtime/query/navigation.d.ts +22 -0
- package/dist/runtime/query/navigation.js +73 -0
- package/dist/runtime/query/pagination +1 -0
- package/dist/runtime/query/pagination.d.ts +7 -0
- package/dist/runtime/query/pagination.js +78 -0
- package/dist/runtime/query/populate +1 -0
- package/dist/runtime/query/populate.d.ts +9 -0
- package/dist/runtime/query/populate.js +106 -0
- package/dist/runtime/query/responses +1 -0
- package/dist/runtime/query/responses.d.ts +10 -0
- package/dist/runtime/query/responses.js +47 -0
- package/dist/runtime/query/unified.d.ts +4 -19
- package/dist/runtime/query/unified.js +14 -457
- package/dist/runtime/query/variants +1 -0
- package/dist/runtime/query/variants.d.ts +11 -0
- package/dist/runtime/query/variants.js +36 -0
- package/dist/runtime/server/agent-http +1 -0
- package/dist/runtime/server/agent-http.d.ts +7 -0
- package/dist/runtime/server/agent-http.js +36 -0
- package/dist/runtime/server/agent-markdown +1 -0
- package/dist/runtime/server/agent-markdown.d.ts +83 -0
- package/dist/runtime/server/agent-markdown.js +449 -0
- package/dist/runtime/server/agent-site +1 -0
- package/dist/runtime/server/agent-site.d.ts +38 -0
- package/dist/runtime/server/agent-site.js +273 -0
- package/dist/runtime/server/api/agent-llms +1 -0
- package/dist/runtime/server/api/agent-llms-full +1 -0
- package/dist/runtime/server/api/agent-llms-full.d.ts +2 -0
- package/dist/runtime/server/api/agent-llms-full.js +13 -0
- package/dist/runtime/server/api/agent-llms.d.ts +2 -0
- package/dist/runtime/server/api/agent-llms.js +14 -0
- package/dist/runtime/server/api/agent-raw +1 -0
- package/dist/runtime/server/api/agent-raw.d.ts +2 -0
- package/dist/runtime/server/api/agent-raw.js +17 -0
- package/dist/runtime/server/api/locales.d.ts +2 -2
- package/dist/runtime/server/api/locales.js +4 -2
- package/dist/runtime/server/api/navigation.d.ts +1 -1
- package/dist/runtime/server/api/query.d.ts +1 -1
- package/dist/runtime/server/api/query.js +4 -1
- package/dist/runtime/server/api/revalidate.js +2 -2
- package/dist/runtime/server/api/search.d.ts +2 -1
- package/dist/runtime/server/api/search.js +6 -2
- package/dist/runtime/server/api/site-data.d.ts +1 -1
- package/dist/runtime/server/collection-helpers.js +2 -1
- package/dist/runtime/server/middleware/agent-link-headers +1 -0
- package/dist/runtime/server/middleware/agent-link-headers.d.ts +2 -0
- package/dist/runtime/server/middleware/agent-link-headers.js +42 -0
- package/dist/runtime/server/middleware/agent-markdown +1 -0
- package/dist/runtime/server/middleware/agent-markdown.d.ts +2 -0
- package/dist/runtime/server/middleware/agent-markdown.js +43 -0
- package/dist/runtime/server/pagefind.js +1 -0
- package/dist/runtime/server/plugins/sitemap.js +1 -0
- package/dist/runtime/server/provider-query.d.ts +8 -5
- package/dist/runtime/server/provider-query.js +50 -8
- package/dist/runtime/server/provider-result.d.ts +19 -2
- package/dist/runtime/server/providers/filesystem.js +4 -1
- package/dist/runtime/server/providers/index.d.ts +3 -1
- package/dist/runtime/server/providers/index.js +2 -2
- package/dist/runtime/server/query-api.js +2 -1
- package/dist/runtime/server/query-executor.d.ts +3 -2
- package/dist/runtime/server/query-executor.js +13 -5
- package/dist/runtime/server/search.d.ts +11 -0
- package/dist/runtime/server/search.js +24 -7
- package/dist/runtime/server/sitemap-provider.js +40 -1
- package/dist/runtime/server/sitemap.js +3 -2
- package/dist/runtime/shared/search.js +3 -2
- package/dist/runtime/transformers/component-resolver.js +2 -1
- package/dist/runtime/utils/content-components.d.ts +0 -5
- package/dist/storage/content.d.ts +2 -9
- package/dist/storage/references.d.ts +1 -8
- package/dist/storage/references.js +9 -4
- package/dist/testing/provider-contract.js +3 -4
- package/dist/testing/provider-fixture.d.ts +3 -1
- package/dist/testing/provider-fixture.js +28 -14
- package/dist/types/config.d.ts +104 -7
- package/dist/types/config.js +49 -2
- package/dist/types/module.d.ts +50 -8
- package/dist/types/query-parts/collections +1 -0
- package/dist/types/query-parts/collections.d.ts +15 -0
- package/dist/types/query-parts/collections.js +0 -0
- package/dist/types/query-parts/public +1 -0
- package/dist/types/query-parts/public.d.ts +202 -0
- package/dist/types/query-parts/public.js +0 -0
- package/dist/types/query-parts/results +1 -0
- package/dist/types/query-parts/results.d.ts +95 -0
- package/dist/types/query-parts/results.js +0 -0
- package/dist/types/query-parts/transport +1 -0
- package/dist/types/query-parts/transport.d.ts +103 -0
- package/dist/types/query-parts/transport.js +0 -0
- package/dist/types/query.d.ts +4 -862
- package/dist/types/query.js +4 -0
- package/dist/types/runtime.d.ts +2 -2
- package/dist/types/search.d.ts +3 -1
- package/dist/types/virtual.d.ts +54 -0
- package/dist/types.d.mts +3 -3
- package/dist/web-types.json +1 -1
- package/package.json +8 -8
- package/dist/runtime/app/composables/web-socket +0 -1
- package/dist/runtime/app/composables/web-socket.d.ts +0 -3
- package/dist/runtime/app/composables/web-socket.js +0 -65
- package/dist/runtime/app/plugins/ws +0 -1
- package/dist/runtime/app/plugins/ws.js +0 -7
- /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
|
|
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
|
|
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
|
|
167
|
-
if (
|
|
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
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
|
233
|
+
return definitions;
|
|
204
234
|
}
|
|
205
|
-
function
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
for (const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
268
|
+
return [...definitions.values()];
|
|
283
269
|
}
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
308
|
-
|
|
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
|
|
281
|
+
return blocks;
|
|
316
282
|
}
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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([
|