New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

nuxt-layerscope

Package Overview
Dependencies
Maintainers
1
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

nuxt-layerscope

Layer boundary checks for Nuxt apps that see auto-imports

latest
Source
npmnpm
Version
0.10.0
Version published
Maintainers
1
Created
Source

nuxt-layerscope

Layer boundary checks for Nuxt 3 and 4 apps that also see auto-imports.

The Layerscope tab in Nuxt DevTools: the layer graph of five layers with three violations, the ui to shop edge selected, with useCart in the side panel

The Layerscope tab in Nuxt DevTools shows every finding, the layer graph and where each symbol is used. It updates when you save a file. Open the live demo.

Boundary tools usually read import statements only, so a component in admin that calls an auto-imported composable from web goes unnoticed. layerscope reads the registry Nuxt resolves (components, app and server auto-imports, layers), resolves every auto-imported identifier, component tag and explicit import to its source file and layer, and checks the result against your rules.

layers/admin/app/components/AdminPanel.vue
  2:14    error  Auto-import "useCart" crosses from layer "admin" into "web"  layer-boundary
                 useCart → layers/web/app/composables/useCart.ts
                 allowed for "admin": shared, auth
                 suggestion: allow "admin" to use "web" (adds 1 edge, clears 4 findings)

Usage

npx nuxi module add nuxt-layerscope --dev   # installs it and adds it to `modules`
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-layerscope'],
});
npx nuxi prepare
npx layerscope check                 # or: npx layerscope check --prepare

The module records what Nuxt resolves during nuxi prepare, dev and build into .nuxt/layerscope/registry.json. Without it, layerscope falls back to the generated .d.ts files, which gives the same boundary findings but cannot see overridden components (see shadowed-component). npx nuxt-layerscope check works without installing anything. Set layerscope: { enabled: false } in nuxt.config to turn the module off.

OptionDescription
[root]Nuxt project root (default: current dir)
--format <format>text (default), github or json
--config <file>Config path, relative to the root (default: layerscope.config.ts)
--prepareRun nuxi prepare first
--baseline <file>Accepted findings, relative to the root (default: the baseline key of the config, else layerscope-baseline.json)
--update-baselineWrite every current finding to the baseline file and exit 0
--strict-baselineFail when the baseline has fixed entries, so that they are removed
--source <source>auto (default: registry when present), registry or types (.d.ts)
--verbosePrint where symbols were read from (to stderr)

Exit codes: 0 clean, 1 rule errors (or fixed baseline entries with --strict-baseline), 2 config or input error. Notes, such as a rule that could not run, go to stderr so stdout stays machine-readable.

--format github prints workflow commands, so findings show up inline on the pull request diff. JSON findings carry target (the file the symbol resolves to) and, for layer-boundary, allowed and a suggestion. The report has a JSON Schema.

layerscope init [root]

Writes layerscope.config.ts with every layer and the smallest allow map the project passes with, and prints each allowed edge, how many references could not be resolved, and how many parts of files it could not read. --baseline also accepts the remaining findings, --dry-run writes nothing and --force replaces an existing config. Delete the edges you consider mistakes, then run check.

layerscope fix (--dry-run | --apply) [root] [options]

Prints the file moves that the suggestions on boundary findings ask for, and the imports that each move breaks. A relative import is listed with its new specifier. An alias that points at a moved file is listed to update by hand. Only moves are printed. The other suggestions are in layerscope check. check and fix do not check the imports in test files, nuxt.config.*, the Nuxt modules/ dir, files outside the layer source dirs, or files that match ignore. Update the imports of moved files in them by hand. --dry-run prints the moves and changes nothing. --apply makes them: it rewrites the relative imports, then moves each file (with git mv when git tracks it). It stops before the first change when the git working tree of the repository has changes or untracked files, when git does not track a file to move or edit, when a file is in a submodule or has a symbolic link in its path, or when a target path exists. --allow-dirty skips the git checks (repository, clean tree, tracked files, submodules). See the CLI reference for all checks.

layerscope drift [root]

Prints how many errors the code adds and fixes against the baseline committed on --base (default origin/main), and the baseline size before and after. Only the rules that the current config sets to error count. --format text|markdown|json. --fail-on-growth exits with 1 when the code has errors that the baseline at the base ref does not have.

Baseline

Turn the check on in a codebase that already has violations, then fix them over time:

npx layerscope check --update-baseline   # writes layerscope-baseline.json; commit it
npx layerscope check                     # fails only on findings missing from the baseline

Entries are keyed by rule, file, symbol and target layer, not by line, so unrelated edits and moving a violation within its file keep the entry. A layer-cycle entry is keyed by rule and chain of layers, not by file. Entries that no longer occur are listed as fixed (as ::notice annotations with --format github, or ::error with --strict-baseline). By default they do not fail the check. With --strict-baseline they do, so the file is updated when a violation is fixed.

GitHub Action

# .github/workflows/layerscope.yml
on: pull_request
jobs:
  layerscope:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24
      - run: npm ci
      - run: npx nuxi prepare
      - uses: hamedniroomand/nuxt-layerscope@v0

It runs layerscope check --format github, so findings show up inline on the pull request. It writes the list of the findings (up to 200) and then the drift report to the job summary. Inputs: root (default .), config, prepare (true runs nuxi prepare instead of the separate step), baseline (default: the baseline key of the config, else layerscope-baseline.json), strict-baseline (true fails the check when the baseline has fixed entries), fail-on-growth (true fails the job when the code has errors that the baseline on the base branch does not have), comment (true posts the drift report on the pull request, one comment for each root) and version (used through npx when the project does not install nuxt-layerscope).

layerscope why <symbol> [root]

Lists every file that uses a symbol, where the symbol resolves to and the layer path each use crosses. It accepts auto-imports, component names (BaseButton, base-button and LazyBaseButton are the same component) and import specifiers.

$ npx layerscope why useCart
useCart → layers/web/app/composables/useCart.ts (web)
  layers/admin/app/components/AdminPanel.vue:2:14   admin → web   ✖ not allowed
  layers/web/app/components/CartSummary.vue:3:14    web → web     ✔ same layer

Takes --format text|json, --config, --prepare, --source and --verbose. Exit codes: 0 when the symbol is used, 2 when no use is found.

layerscope graph [root]

Prints the dependency graph between layers (--by layer, the default) or between files (--by file, grouped by layer). Edges that break the layer rules are drawn in red and labelled with how many references they carry. Packages are left out.

npx layerscope graph > layers.mmd                 # Mermaid, for docs and PR descriptions
npx layerscope graph --by file --format dot | dot -Tsvg > files.svg

--format mermaid (default), dot or json. Always exits 0.

layerscope unused [root]

Lists components and auto-imports (composables, utils, server utils) that nothing references, per layer. Explicit imports of a file count as using everything it exports. When the project renders components chosen at runtime (<component :is> bound to a value, <NuxtIsland :name>, and resolveComponent(x) or resolveDynamicComponent(x) with a value that is not a fixed string), unused components are marked "possibly used at runtime". <NuxtIsland name="X"> counts as a use of the island component X. A global component (in components/global/, with a .global suffix, or in a dir registered as global) is marked "possibly used at runtime" when it is listed as unused: Vue can render it by a name that is chosen at runtime. When the project uses Nuxt Content, every component is marked, because a content file can use any component by its name. With --source types, unused does not list island components (in components/islands/ or with an .island.vue suffix), because the .d.ts files do not have them. With --source types, a component in a dir that a module or config registers as global is not marked, because the .d.ts files do not have the global setting. Nuxt Content is found only when a modules list names it as the string '@nuxt/content'. unused does not count uses in test files (*.test.*, *.spec.*, __tests__/), in files that match ignore, in layers installed as packages, or in files outside the layer source dirs that are not in an extra auto-import or component dir and that no counted file imports. unused also counts the uses in nuxt.config.*, in the Nuxt modules/ dir, in files of extra auto-import and component dirs, and in the project files that counted files import. It also counts typeof x in a type, and import type when typeImports is 'ignore'. It lists the files that it could not read in full: a parse error, a template that layerscope does not read (a lang other than HTML, a src attribute, or an expression that does not parse), a dynamic import() or require(), or an import.meta.glob(). The text report has a line that names the uses that it does not count. It does not list the symbols of layers outside the project root that are not installed as packages, for example workspace packages. When such layers exist, a second line names them. When the project uses Nuxt Content, one more line says that every component is marked "possibly used at runtime". why shows only the uses that check reads. --format text (default) or json. Always exits 0.

Editor feedback: ESLint and oxlint

nuxt-layerscope/eslint reports the same findings as check while you type. It reads the registry the module writes, so the module is required, and it never boots Nuxt.

// eslint.config.js
import layerscope from 'nuxt-layerscope/eslint';

export default [
  // ...your parsers (vue-eslint-parser for .vue files)
  layerscope.configs.recommended,
];

The rules are layerscope/layer-boundary and layerscope/unresolved-reference. Each takes an optional { root } (the Nuxt project, relative to the working directory); by default the project is the nearest directory above the file with .nuxt/layerscope/registry.json. Set root for layers that live outside the app (for example a layer package in a monorepo).

oxlint loads the same plugin through jsPlugins:

{
  "jsPlugins": [{ "name": "layerscope", "specifier": "nuxt-layerscope/eslint" }],
  "rules": { "layerscope/layer-boundary": "error", "layerscope/unresolved-reference": "warn" }
}

oxlint passes only the <script> of .vue files to plugins, so findings in a template are shown at the start of the script with their template line in the message.

Nuxt DevTools

While nuxi dev runs, the module adds a Layerscope tab to Nuxt DevTools with the layers, what each may depend on and the current findings. File links open in your editor. The report is also served as JSON at /__layerscope?format=json. Turn it off with layerscope: { devtools: false }. With layerscope: { devtools: { static: true } }, nuxi build and nuxi generate also write a read-only copy of the tab to /__layerscope/, for any static host.

Config

// layerscope.config.ts
import { defineConfig } from 'nuxt-layerscope';

export default defineConfig({
  preset: 'layered', // optional: fills `allow` for layers that do not set it
  layers: {
    shared: { allow: [] },
    auth: { allow: ['shared'] },
    web: { allow: ['shared', 'auth'] },
    admin: { allow: ['shared', 'auth'] },
  },
  rules: {
    'layer-boundary': 'error',
    'layer-cycle': 'error', // off by default
    'unresolved-reference': 'warn',
    'shadowed-component': 'warn',
  },
});

Or put the same options under layerscope in nuxt.config, next to enabled and devtools. The module records them on nuxi prepare, dev and build; use one place or the other, not both.

  • Layer names come from Nuxt: the folder name for layers/<name> and for extends entries, or $meta.name. The project itself is root. Set path on an entry to name a layer explicitly, or source (the extends string, such as github:acme/console) to name a remote layer.
  • A layer missing from layers is unrestricted. Edges within a layer are always allowed.
  • ignore adds globs to skip, and globals lists identifiers or components registered at runtime (for example by a plugin) so they are not reported as unresolved. typeImports: 'ignore' leaves type-only imports out of every check.

Rules

  • layer-boundary: a file depends on a layer its own layer does not allow. This covers auto-imported composables and utils, components (including Lazy*), Nitro server utils and explicit imports (#layers/..., ~/..., relative paths and the package name of a layer, for example @acme/ui-layer/app/composables/useAcme).
  • layer-cycle (off by default): layers depend on each other in a loop. The finding names the whole chain. Turn it on once a baseline holds the cycles you have today.
  • unresolved-reference: something could not be resolved, so layerscope will not call it safe. Examples are an identifier that is neither a local binding, a known global nor an auto-import; a component that Nuxt does not have; an import path that does not exist; or <component :is> bound to a runtime value.
  • shadowed-component (warning): two layers register a component with the same name and Nuxt uses the higher-priority one. The overridden component is reported with both paths, so accidental overrides are visible. Needs the module; with the .d.ts fallback the rule reports nothing and says so on stderr.

How it works

  • Layers come from the project's own @nuxt/kit, which every Nuxt install already has: loadNuxtConfig for the list and names, getLayerDirectories for each layer's srcDir, serverDir, shared dir, modules dir and public dir. check does not read the modules and public dirs. unused reads modules, so that its uses count. Without kit, layers can be declared with path in the config.
  • Each file belongs to the layer with the longest matching dir. The dirs of a layer are its root, and also its srcDir, serverDir and shared dir when one of them is outside the root. When a source dir of one layer is the root of another layer, the layer with that root owns the files. Paths are compared as real paths so pnpm symlinks do not matter. Layers installed from npm or a remote source live under node_modules and still own their files, so boundaries apply to them; their own code is resolved but not checked. Anything outside every layer is external. Files under a layer's server dir use the Nitro symbol table, and files under its shared dir use the shared one.
  • Scripts are parsed with oxc-parser and scope-tracked, so a local useCart shadows the auto-imported one. Templates are compiled with @vue/compiler-sfc, and the render function's resolveComponent("X") calls and _ctx.x reads show which components and identifiers Nuxt would auto-import.
  • With the module, symbols come from .nuxt/layerscope/registry.json, recorded from Nuxt's own hooks: components:dirs and components:extend (every scanned component, including overridden ones), imports:context (app auto-imports), Nitro's unimport context (server auto-imports) and getLayerDirectories() (layers in priority order). The file has a versioned schema; a registry from an incompatible release is ignored with a note.
  • Without the module, components come from .nuxt/components.d.ts (or .nuxt/types/components.d.ts) and auto-imports from .nuxt/types/*imports.d.ts. On Nuxt 3, which writes no shared-imports.d.ts, the shared context gets the imports app and server have in common, as Nuxt 4 does.
  • Generated files that point at deleted files, or that miss a component, are treated as stale (exit 2). With the registry every dir Nuxt scanned is compared, including custom component dirs and auto-import dirs, so a new file in one of them is found (a file that Nuxt ignores is skipped, except in the auto-import dirs of Nitro); without it only the default components/ dirs are.

The JSON report (--format json) is versioned and deterministic: the same input produces the same bytes.

Package entry points

ImportContents
nuxt-layerscopeThe Nuxt module (default export), defineConfig and the types. Light enough for Nuxt startup.
nuxt-layerscope/apianalyze, formatResult, createBaseline and the report and registry constants.
nuxt-layerscope/eslintESLint and oxlint plugin with layer-boundary and unresolved-reference rules.
nuxt-layerscope/schema/report-1.jsonJSON Schema of the --format json report.
layerscope (bin)The CLI.
import { analyze, formatResult } from 'nuxt-layerscope/api';

const result = await analyze({ rootDir: 'apps/shop' });
process.stdout.write(formatResult(result, 'text'));

Compared with other tools

Other tools cover parts of this. layerscope is narrower: it resolves symbols exactly the way Nuxt does, understands Nuxt layer semantics, and is built to fail a CI job.

layerscopeeslint-plugin-nuxt-layersnuxt-fsdArchoraNuxt DevTools
Boundary rules on explicit imports✓✓✓ (FSD layers)✓–
Boundary rules on auto-imports and components✓––✓–
Resolves symbols fromNuxt's generated types and @nuxt/kitimport pathsits own aliasesits own analyzerthe running app
Knows Nuxt layers (extends, layers/, npm)✓folder layout–not Nuxt-specificshows them
CI gateexit codes, PR annotationsvia ESLint–exit codes, thresholds–
ScopeNuxt layer boundariesimport boundariesFSD structuregeneral architecture analyzerdev-time inspection
  • eslint-plugin-nuxt-layers checks import statements against a layer map. It works inside ESLint and the editor, but auto-imports have no import statement to check.
  • nuxt-fsd sets up Feature-Sliced Design in Nuxt and blocks cross-imports, but its own README notes that auto-imports do not respect those rules.
  • Archora (@archora/cli) is a general frontend architecture analyzer that also resolves Nuxt auto-imported composables, alongside cycles, churn and bundle analysis. Pick it for a broad architecture report; pick layerscope for exact Nuxt layer boundaries as a CI gate.
  • Nuxt DevTools shows components and auto-imports of a running app, but does not enforce rules.

Keywords

architecture

FAQs

Package last updated on 15 Oct 2026

Related posts