Skip to content

fix: stop reporting Nuxt/Nitro convention files and auto-imported Vue components as orphaned - #724

Open
MercurialUroboros wants to merge 1 commit into
peteromallet:mainfrom
MercurialUroboros:fix/nuxt-orphan-false-positives
Open

fix: stop reporting Nuxt/Nitro convention files and auto-imported Vue components as orphaned#724
MercurialUroboros wants to merge 1 commit into
peteromallet:mainfrom
MercurialUroboros:fix/nuxt-orphan-false-positives

Conversation

@MercurialUroboros

Copy link
Copy Markdown

Problem

The orphaned detector has full support for Next.js App Router conventions (_NEXTJS_APP_DIR_CONVENTIONS, _detect_nextjs_project, _is_nextjs_convention_entry) but nothing equivalent for Nuxt. On a Nuxt project, almost every finding it produces is a false positive, for two separate reasons.

First, Nuxt and Nitro register entire directory trees by convention. Nothing imports server/api/**, server/middleware/**, server/plugins/**, server/tasks/**, pages/**, layouts/**, middleware/** or plugins/**, because the framework picks them up from the filesystem. Every one of those files has zero importers by design.

Second, Nuxt auto-imports components/, composables/, utils/ and shared/. Consumers write <GameOutcomeDrawer /> in a template, or call useUser() as a bare identifier, with no import statement anywhere. Those references are invisible to an import graph, so the components and composables that are used most heavily are exactly the ones reported as dead.

I measured this against a real Nuxt 4 codebase (roughly 1,700 files, Nuxt 4 with app/ as srcDir plus a Nitro server/ tree). Running detect orphaned and filtering to that project's own scan exclusions and the TypeScript entry patterns, the detector reported 429 orphans. Broken down by directory:

Surface Findings before
server/api/ 178
app/components/ 103
app/composables/ 29
shared/ 13
server/plugins/ 11
app/{layouts,middleware,plugins}/ 10
app/utils/ 8
server/middleware/ 4
server/tasks/ 2
server/routes/ 1
Everything else 70

All 359 findings on Nuxt-owned surfaces were false positives. I checked the 103 component findings by hand: every one is referenced as a PascalCase tag in some .vue template. Nothing in that project was actually dead code.

Fix

The detector now recognises a Nuxt project the way it recognises a Next.js one: a nuxt.config.{ts,js,mjs,cjs,mts} at the scan root, or a nuxt dependency in package.json. Everything below is gated on that detection, so Next.js projects and the existing app/ handling are untouched, and detect_frameworks=False still disables the whole thing.

Convention entry points are exempted by location, mirroring _is_nextjs_convention_entry. That covers the Nitro trees (server/api, server/routes, server/middleware, server/plugins, server/tasks), the app trees (pages, layouts, middleware, plugins, modules), and the root config and shell files (nuxt.config, app.config, capacitor.config, app, error). Nuxt 3 keeps these at the project root and Nuxt 4 puts them under app/, so an optional leading app/ or src/ srcDir segment is stripped before matching. server/lib/** and similar ordinary module directories keep their orphan verdict.

Auto-import surfaces are handled differently, because blanket-exempting components/ would hide genuinely dead components and cost the detector most of its value on a Nuxt codebase. Instead the file's registered name is computed and then looked up in a project-wide usage index:

  • Component names follow Nuxt's own derivation, which joins the directory path to the filename and drops the directory segments the filename already repeats. app/components/game/GameOutcomeDrawer.vue registers as GameOutcomeDrawer; app/components/account/ThemeSwitcher.vue registers as AccountThemeSwitcher. .client, .server and .global suffixes are stripped, and an index file takes its directory's name. The undeduplicated join and the bare filename are accepted too, since components.dirs with pathPrefix: false registers under the bare stem and so does a consumer that imports the file by path.
  • Composables, utils and shared/ modules resolve through the identifiers they export, parsed from export declarations and export { … } lists, plus the filename stem.
  • The usage index scans every .vue, .ts, .tsx, .js, .jsx, .mjs and .mts file under the scan root for identifiers and for component tags. Kebab-case tags are normalised to PascalCase, so <game-outcome-drawer /> and <GameOutcomeDrawer /> both resolve to the one registered name. The index records which file each name was seen in, so a module's own mention of itself does not count as a reference and an unused composable is still reported.

The index is built lazily, only when the first auto-import candidate is reached, so nothing changes for projects that are not Nuxt. On the 1,700-file codebase above the whole run takes about five seconds, unchanged from before.

Convention matching is anchored on the scan root rather than on rel(), which resolves against the project root and yields a ../.. prefix when the tool is invoked from outside the project being scanned.

After the fix, the same run on the same codebase reports 67 orphans instead of 429. All 359 Nuxt-surface false positives are gone, and the 67 that remain are outside Nuxt's conventions: CLI scripts under scripts/ invoked through npm scripts, tooling configs, a worker bundled by a build step, and a directory the project imports through a ~/ alias the graph does not resolve. None of them is a Nuxt or Nitro convention file, and none of them is an auto-imported component or composable.

Implementation lives in a new desloppify/engine/detectors/_orphaned/nuxt.py, following the _flat_dirs precedent of keeping the public detector module small, and orphaned.py wires it in beside the Next.js checks. Tests are in desloppify/tests/detectors/test_orphaned_nuxt.py: 43 cases covering project detection, convention paths, Nuxt's component-name derivation, export parsing, the usage index, and end-to-end orphan verdicts on a Nuxt-shaped tree, including the negative cases (an unreferenced composable is still reported, a non-Nuxt project still reports server/api/**, detect_frameworks=False disables all of it).

python -m pytest desloppify/tests/ -q passes: 5,695 passed, 152 skipped. Three failures in desloppify/tests/lang/common/test_bash_unused_imports.py are present on main before this change and are unrelated.

https://claude.ai/code/session_01D5dUe5YLshYGAFwwxf1JJx

… components as orphaned

The orphaned detector knows Next.js App Router conventions but nothing about
Nuxt, so every file-routed and auto-registered file in a Nuxt project came back
as "zero importers, not an entry point".

Adds Nuxt project detection (nuxt.config.* at the scan root, or a `nuxt`
dependency in package.json) and, behind that gate:

- Convention entry points: server/api, server/routes, server/middleware,
  server/plugins, server/tasks, plus pages/, layouts/, middleware/, plugins/
  and modules/ under both the Nuxt 3 root layout and the Nuxt 4 app/ srcDir,
  and the root config and shell files.
- Auto-import surfaces (components/, composables/, utils/, shared/,
  server/utils/) resolved by usage rather than exempted wholesale: the file's
  registered name is derived the way Nuxt derives it, and a project-wide index
  of identifiers and template tags decides whether anything references it. A
  component nobody uses is still reported.

Claude-Session: https://claude.ai/code/session_01D5dUe5YLshYGAFwwxf1JJx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant