@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 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 buildKnipConfig(projectInfo, doctorConfig) {
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;