Skip to content

v2.63.0 — Monorepo-friendly: contributor PRs + scale-invariant health score

Choose a tag to compare

@BartWaardenburg BartWaardenburg released this 03 May 22:52
· 99 commits to main since this release
v2.63.0
509b5fe

A monorepo-friendly release. Five contributor PRs from @fmguerreiro clear false positives across Turborepo CI workflows, ESLint flat-configs, Vitest manual mocks, and Next.js dynamic re-exports. The health_score formula is reworked to be scale-invariant so large monorepos no longer score in the B band by default. CSS @import now follows package.json#exports with the style condition (shadcn / Tailwind v4 plugins).

Added

Vitest /__mocks__ virtual specifiers no longer flagged as unlisted dependencies (#265) — @aws-sdk/__mocks__, @sentry/__mocks__, @supabase/__mocks__, etc. are Vitest manual-mock specifiers that don't exist on npm; they used to trigger an unlisted-dependency finding with an "install this package" auto-fix that pointed at a package that doesn't exist. The Vitest plugin now contributes a /__mocks__ package-name suffix via the new Plugin::virtual_package_suffixes() trait method, and the suffix list merges across workspace plugin runs into the root AggregatedPluginResult so monorepos with Vitest only in a workspace's package.json (not the root) get the same suppression. Thanks @fmguerreiro.

Changed

health_score is now scale-invariant (Closes #260) — The penalty formula previously used absolute counts (unused_dep_count), unweighted averages (avg_cyclomatic), and order-statistics (p90_cyclomatic) that are mathematically incapable of firing at large-monorepo scale: a 50k-LOC monorepo with 200 unused devDependencies and 1500 functions over 60 LOC would score in the B band because the per-dimension caps were saturated and the averages were diluted by clean code in the long tail. The reworked formula switches to scale-invariant aggregators: critical_complexity_pct (functions over a hard CC threshold), maintainability_low_pct (files below the MI threshold), unused_deps_per_k_files, circular_deps_per_k_files, functions_over_60_loc_per_k, coupling_high_pct, and hotspot_top_pct_count (top-percentile hotspots normalized against total_files). Caps on unused_deps and circular_deps raised from 10 to 25. New formula_version: 2 field on HealthScore lets consumers detect the formula change. Older snapshots that lack the scale-invariant fields fall back to the previous aggregators so cached / archived data still scores. Thanks @OmerGronich for the detailed report including the per-dimension cap analysis.

Fixed

CSS @import 'pkg/subpath.css' resolves through package.json#exports with the style condition (Closes #261) — Bare CSS imports whose target is exposed only through an exports map under the "style" condition (shadcn, daisyui, Tailwind v4 plugins) previously surfaced as unresolved_imports even though the file existed and bundlers resolved it correctly. The CSS / SCSS resolver now consults the package's exports map for the requested subpath before falling back to the node_modules/<pkg>/<file> direct path, picking up { "./tailwind.css": { "style": "./dist/tailwind.css" } } shapes. Thanks @VidhyaKumar for the report with a complete shadcn 4.6.0 reproduction.

CI YAML scanner stops emitting WARN invalid entry pattern for shell and regex fragments (#262) — GitHub Actions expressions (${{ env.URL }}/api/health), jq -r '.[]' array iterators, and Perl regex shards (grep -oP '(?<=Module )\./[^ ]+') split on whitespace into tokens like }}/api/health, '.[]', and )\./[^ that reached globset::GlobBuilder::new(...).build() and produced 10+ noise warnings on a typical CI repo. A new could_be_file_path negative-only guard rejects tokens whose syntax precludes a Unix path (unbalanced ${{/}}, backslashes, malformed [...]) before they reach globset compilation. Next.js dynamic-route segments (app/[id]/page.tsx, pages/[...slug].ts) remain valid. Thanks @fmguerreiro.

Next.js dynamic(() => import('./X').then(m => m.X)) lazy-loaded re-exports no longer flagged as duplicate-export (#263) — The Next.js code-splitting idiom where Foo-lazy.tsx exports Foo = dynamic(() => import('./Foo').then(m => m.Foo), { ssr: false }) is semantically a re-export of Foo. find_duplicate_exports now extends re_export_sources with dynamic-import edges that act as re-exports, gated by a wrapper-must-export check that guards against false-negative suppression of legitimate duplicates. Thanks @fmguerreiro.

ESLint flat-config plugin imports trace through workspace-internal config packages (#266) — Turborepo / Nx monorepos that centralize ESLint config in a workspace package were producing false unused-devdep flags for plugins the shared config imports transitively. The ESLint plugin now walks up node_modules/ ancestors (bounded by MAX_NODE_MODULES_WALK_DEPTH = 8) so packages hoisted to the monorepo root are found from a workspace root, and resolves @scope/pkg/subpath imports via the package's exports map with .js/.mjs/.cjs extension fallback. ESLint also joins the must_parse_workspace_config_when_root_active allowlist so workspace eslint.config.* files still get parsed when root-level ESLint is active. Thanks @fmguerreiro.

Full Changelog: v2.62.0...v2.63.0