@geoql/doctor-core 1.3.0 → 1.3.2
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/dist/index.js +106 -8
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
2
2
|
import { performance } from "node:perf_hooks";
|
|
3
|
-
import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { access, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { execFile, spawn } from "node:child_process";
|
|
6
6
|
import { parseSync } from "oxc-parser";
|
|
@@ -227,7 +227,84 @@ async function checkBuildQuality(projectInfo) {
|
|
|
227
227
|
//#endregion
|
|
228
228
|
//#region src/dead-code/build-knip-config.ts
|
|
229
229
|
const DEMO_WORKSPACES = ["example", "playground"];
|
|
230
|
-
function
|
|
230
|
+
async function findPnpmWorkspaceYaml(startDir) {
|
|
231
|
+
let dir = startDir;
|
|
232
|
+
for (let i = 0; i < 5; i++) {
|
|
233
|
+
const candidate = join(dir, "pnpm-workspace.yaml");
|
|
234
|
+
try {
|
|
235
|
+
if ((await stat(candidate)).isFile()) return candidate;
|
|
236
|
+
} catch {}
|
|
237
|
+
const parent = dirname(dir);
|
|
238
|
+
if (parent === dir) break;
|
|
239
|
+
dir = parent;
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
function collectCatalogNames(packageJson) {
|
|
244
|
+
const names = /* @__PURE__ */ new Set();
|
|
245
|
+
const fields = [
|
|
246
|
+
packageJson.dependencies,
|
|
247
|
+
packageJson.devDependencies,
|
|
248
|
+
packageJson.peerDependencies,
|
|
249
|
+
packageJson.optionalDependencies
|
|
250
|
+
];
|
|
251
|
+
for (const field of fields) {
|
|
252
|
+
if (!field) continue;
|
|
253
|
+
for (const version of Object.values(field)) if (typeof version === "string" && version.startsWith("catalog:")) names.add(version.slice(8));
|
|
254
|
+
}
|
|
255
|
+
return names;
|
|
256
|
+
}
|
|
257
|
+
function extractCatalogDependencies(workspaceYaml, catalogName) {
|
|
258
|
+
const lines = workspaceYaml.split(/\r?\n/);
|
|
259
|
+
const deps = [];
|
|
260
|
+
let inCatalogs = false;
|
|
261
|
+
let inTargetBlock = false;
|
|
262
|
+
let blockIndent = -1;
|
|
263
|
+
const escapedCatalogName = catalogName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
264
|
+
const catalogPattern = new RegExp(`^${escapedCatalogName}\\s*:\\s*$`);
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
const trimmed = line.trim();
|
|
267
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
268
|
+
if (!inCatalogs) {
|
|
269
|
+
if (/^catalogs\s*:/.test(trimmed)) inCatalogs = true;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const indent = line.length - line.trimStart().length;
|
|
273
|
+
if (inTargetBlock) {
|
|
274
|
+
if (indent <= blockIndent) break;
|
|
275
|
+
const match = line.match(/^[ \t]*["']?([@a-zA-Z0-9_./-]+)["']?[ \t]*:/);
|
|
276
|
+
if (match) deps.push(match[1]);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (catalogPattern.test(trimmed)) {
|
|
280
|
+
inTargetBlock = true;
|
|
281
|
+
blockIndent = indent;
|
|
282
|
+
} else if (indent === 0 && /^\w+/.test(trimmed) && !/^catalogs\b/.test(trimmed)) break;
|
|
283
|
+
}
|
|
284
|
+
return deps;
|
|
285
|
+
}
|
|
286
|
+
async function resolveCatalogDependencies(rootDirectory) {
|
|
287
|
+
const workspacePath = await findPnpmWorkspaceYaml(rootDirectory);
|
|
288
|
+
if (!workspacePath) return [];
|
|
289
|
+
let packageJson;
|
|
290
|
+
try {
|
|
291
|
+
packageJson = JSON.parse(await readFile(join(rootDirectory, "package.json"), "utf8"));
|
|
292
|
+
} catch {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
const catalogNames = collectCatalogNames(packageJson);
|
|
296
|
+
if (catalogNames.size === 0) return [];
|
|
297
|
+
let workspaceYaml;
|
|
298
|
+
try {
|
|
299
|
+
workspaceYaml = await readFile(workspacePath, "utf8");
|
|
300
|
+
} catch {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
304
|
+
for (const catalogName of catalogNames) for (const dep of extractCatalogDependencies(workspaceYaml, catalogName)) resolved.add(dep);
|
|
305
|
+
return [...resolved];
|
|
306
|
+
}
|
|
307
|
+
async function buildKnipConfig(projectInfo, doctorConfig) {
|
|
231
308
|
const isNuxt = projectInfo.framework === "nuxt";
|
|
232
309
|
const usesConventionResolution = projectInfo.hasAutoImports || projectInfo.hasComponentsAutoImport || projectInfo.hasVueRouter;
|
|
233
310
|
const entry = isNuxt ? [
|
|
@@ -248,6 +325,7 @@ function buildKnipConfig(projectInfo, doctorConfig) {
|
|
|
248
325
|
"src/stores/**/*.ts"
|
|
249
326
|
] : []
|
|
250
327
|
];
|
|
328
|
+
const catalogDependencies = await resolveCatalogDependencies(projectInfo.rootDirectory);
|
|
251
329
|
const config = {
|
|
252
330
|
cwd: projectInfo.rootDirectory,
|
|
253
331
|
entry,
|
|
@@ -259,11 +337,12 @@ function buildKnipConfig(projectInfo, doctorConfig) {
|
|
|
259
337
|
"!**/knip.config.mjs"
|
|
260
338
|
],
|
|
261
339
|
ignoreFiles: [...doctorConfig.exclude, "knip.config.mjs"],
|
|
262
|
-
ignoreDependencies: [
|
|
340
|
+
ignoreDependencies: [...new Set([
|
|
263
341
|
"vite-plus",
|
|
264
342
|
"@geoql/vue-doctor",
|
|
265
|
-
"@geoql/nuxt-doctor"
|
|
266
|
-
|
|
343
|
+
"@geoql/nuxt-doctor",
|
|
344
|
+
...catalogDependencies
|
|
345
|
+
])],
|
|
267
346
|
ignoreWorkspaces: [...DEMO_WORKSPACES]
|
|
268
347
|
};
|
|
269
348
|
if (isNuxt) config.compilers = { nuxt: true };
|
|
@@ -421,7 +500,7 @@ async function writeKnipConfig(rootDir, config) {
|
|
|
421
500
|
}
|
|
422
501
|
async function checkDeadCode(options) {
|
|
423
502
|
if (!options.enabled) return [];
|
|
424
|
-
const config = buildKnipConfig(options.projectInfo, options.doctorConfig);
|
|
503
|
+
const config = await buildKnipConfig(options.projectInfo, options.doctorConfig);
|
|
425
504
|
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
426
505
|
let createOptions;
|
|
427
506
|
let run;
|
|
@@ -1611,6 +1690,7 @@ async function generateOxlintConfig(input) {
|
|
|
1611
1690
|
...userConfig ? { extends: [userConfig] } : {},
|
|
1612
1691
|
plugins: ["vue"],
|
|
1613
1692
|
jsPlugins: input.pluginPaths,
|
|
1693
|
+
...input.exclude && input.exclude.length > 0 ? { ignorePatterns: input.exclude } : {},
|
|
1614
1694
|
rules
|
|
1615
1695
|
};
|
|
1616
1696
|
const configPath = join(dir, ".oxlintrc.json");
|
|
@@ -1816,7 +1896,8 @@ async function runScriptPass(opts) {
|
|
|
1816
1896
|
pluginPaths,
|
|
1817
1897
|
ruleOverrides: opts.ruleOverrides,
|
|
1818
1898
|
rootDir: opts.rootDir,
|
|
1819
|
-
framework: opts.framework
|
|
1899
|
+
framework: opts.framework,
|
|
1900
|
+
exclude: opts.exclude
|
|
1820
1901
|
});
|
|
1821
1902
|
try {
|
|
1822
1903
|
const raw = await runOxlint({
|
|
@@ -2919,6 +3000,11 @@ function check$20(ctx) {
|
|
|
2919
3000
|
const RULE_ID$3 = "nuxt-doctor/seo/og-image-defined";
|
|
2920
3001
|
const MESSAGE$3 = "Page uses SEO meta but has no og:image property. Open Graph images improve social sharing previews. Add an og:image value or install @nuxtjs/og-image for automatic OG images.";
|
|
2921
3002
|
const RECOMMENDATION$3 = "Add ogImage: \"/path/to/image.png\" to useSeoMeta / useHead, or install @nuxtjs/og-image.";
|
|
3003
|
+
const SEO_PRIMITIVES$1 = new Set(["useSeoMeta", "useHead"]);
|
|
3004
|
+
function isSeoWrapperName$1(name) {
|
|
3005
|
+
if (SEO_PRIMITIVES$1.has(name)) return false;
|
|
3006
|
+
return /^use[a-z]*seo/i.test(name);
|
|
3007
|
+
}
|
|
2922
3008
|
function hasOgImageInCall(program) {
|
|
2923
3009
|
for (const stmt of program.body) {
|
|
2924
3010
|
if (stmt.type !== "ExpressionStatement") continue;
|
|
@@ -2926,6 +3012,7 @@ function hasOgImageInCall(program) {
|
|
|
2926
3012
|
if (call.type !== "CallExpression") continue;
|
|
2927
3013
|
if (call.callee.type !== "Identifier") continue;
|
|
2928
3014
|
const name = call.callee.name;
|
|
3015
|
+
if (isSeoWrapperName$1(name)) return true;
|
|
2929
3016
|
if (name !== "useSeoMeta" && name !== "useHead") continue;
|
|
2930
3017
|
const firstArg = call.arguments[0];
|
|
2931
3018
|
if (!firstArg || firstArg.type !== "ObjectExpression") continue;
|
|
@@ -2979,6 +3066,15 @@ function check$19(ctx) {
|
|
|
2979
3066
|
const RULE_ID$2 = "nuxt-doctor/seo/useSeoMeta-on-public-page";
|
|
2980
3067
|
const MESSAGE$2 = "Public page component is missing SEO metadata. Add useSeoMeta, useHead, or definePageMeta with a title so search engines can index it properly.";
|
|
2981
3068
|
const RECOMMENDATION$2 = "Call useSeoMeta({ title: \"...\" }) in<script setup> to define page title and meta tags for search engines and social previews.";
|
|
3069
|
+
const SEO_PRIMITIVES = new Set([
|
|
3070
|
+
"useSeoMeta",
|
|
3071
|
+
"useHead",
|
|
3072
|
+
"definePageMeta"
|
|
3073
|
+
]);
|
|
3074
|
+
function isSeoWrapperName(name) {
|
|
3075
|
+
if (SEO_PRIMITIVES.has(name)) return false;
|
|
3076
|
+
return /^use[a-z]*seo/i.test(name);
|
|
3077
|
+
}
|
|
2982
3078
|
function hasTitleInCall(program) {
|
|
2983
3079
|
for (const stmt of program.body) {
|
|
2984
3080
|
if (stmt.type !== "ExpressionStatement") continue;
|
|
@@ -2986,6 +3082,7 @@ function hasTitleInCall(program) {
|
|
|
2986
3082
|
if (call.type !== "CallExpression") continue;
|
|
2987
3083
|
if (call.callee.type !== "Identifier") continue;
|
|
2988
3084
|
const name = call.callee.name;
|
|
3085
|
+
if (isSeoWrapperName(name)) return true;
|
|
2989
3086
|
if (name !== "useSeoMeta" && name !== "useHead" && name !== "definePageMeta") continue;
|
|
2990
3087
|
const firstArg = call.arguments[0];
|
|
2991
3088
|
if (!firstArg || firstArg.type !== "ObjectExpression") continue;
|
|
@@ -4127,7 +4224,8 @@ async function audit(config = {}) {
|
|
|
4127
4224
|
ruleOverrides: config.rules,
|
|
4128
4225
|
framework: project.framework === "nuxt" ? "nuxt" : "vue",
|
|
4129
4226
|
fix: config.fix === true && config.scopeFiles === void 0,
|
|
4130
|
-
fixExcludes: config.fixExcludes
|
|
4227
|
+
fixExcludes: config.fixExcludes,
|
|
4228
|
+
exclude
|
|
4131
4229
|
});
|
|
4132
4230
|
scriptDiagnostics = result.diagnostics;
|
|
4133
4231
|
oxlintStderr = result.stderr;
|