DEV Community

Cover image for Your Nuxt layers are leaking, and your linter can't see it
Hamed
Hamed

Posted on

Your Nuxt layers are leaking, and your linter can't see it

Here is a Vue component. Spot the architecture bug.

<!-- lyears/admin/app/components/AdminPanel.vue -->
<script setup lang="ts">
  const cart = useCart();
</script>

<template>
  <CartSummary />
</template>
Enter fullscreen mode Exit fullscreen mode

Looks fine, right? No weird imports. Nothing red in the editor. ESLint is happy. The PR gets approved.

The problem: useCart and <CartSummary> live in the shop layer. The admin layer was never supposed to touch shop. You just tied two parts of your app together, and nobody noticed. Not you, not the reviewer, not a single tool in the pipeline.

Do that thirty more times and your nice modular Nuxt app is one big ball of yarn with a layers/ folder on top.

Why nothing catches it

Nuxt layers are a great way to split a big app: a shared layer for the design system, an auth layer, a shop layer, an admin layer. You get a modular monolith for free.

But the modular part only holds if each layer stays in its lane. And the tools that guard boundaries (ESLint import rules, dependency-cruiser and friends) all read import statements.

In Nuxt, most dependencies have no import statement. Composables, utils, components and Nitro server utils are auto-imported. The boundary tool looks at the file, sees zero imports, and gives it a gold star.

I hit this on a real project. After a few months, "admin may only use shared and auth" was a rule that lived in a README and nowhere else.

So I built layerscope

layerscope resolves auto-imports the same way Nuxt does, and fails CI when a layer uses something it is not allowed to.

You write the rules once:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-layerscope'],
  layerscope: {
    layers: {
      shared: { allow: [] },
      auth: { allow: ['shared'] },
      shop: { allow: ['shared', 'auth'] },
      admin: { allow: ['shared', 'auth'] },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Then run:

npx layerscope check --prepare
Enter fullscreen mode Exit fullscreen mode

And the component from the top of this post finally gets caught:

layers/admin/app/components/AdminPanel.vue
  2:14    error  Auto-import "useCart" crosses from layer "admin" into "shop"  layer-boundary
                 useCart → layers/shop/app/composables/useCart.ts
                 allowed for "admin": shared, auth
  8:5     error  Component <CartSummary> crosses from layer "admin" into "shop"  layer-boundary
                 CartSummary → layers/shop/app/components/CartSummary.vue
                 allowed for "admin": shared, auth
Enter fullscreen mode Exit fullscreen mode

It tells you what crossed, where it came from, and what the layer is allowed to use. No guessing.

How it knows

layerscope does not guess from file names. A small Nuxt module records what Nuxt actually registered: every component, every auto-import, every server util, per layer. So layer priority, overridden components, pathPrefix, custom component dirs and module auto-imports like @vueuse/nuxt all come out the way Nuxt sees them.

Then it parses your scripts with oxc and your templates with the real Vue compiler, tracks scope (a local const useCart shadows the auto-import, same as at runtime), and checks every edge against your rules.

If it can't resolve something, it says so. It never quietly calls an unknown reference "safe".

On a test project with 3,000 source files in 10 layers, a full check takes about 770 ms.

"But my project already has 200 violations"

Same. That is why there is a baseline.

npx layerscope init --baseline
Enter fullscreen mode Exit fullscreen mode

init looks at your layers, writes a config that allows exactly what exists today, and accepts the current findings. Your first CI run is green. From now on, only new violations fail the build. Delete a line from the config when you want to fix that dependency for real.

The GitHub Action even adds a summary like adds 3, fixes 5 and Baseline: 47 → 42, so you can watch the mess shrink one PR at a time.

The fun extras

  • layerscope why useCart shows every place a symbol is used, and which uses break the rules. Great before you move a composable.
  • layerscope graph draws the dependency graph between your layers.
  • layerscope unused finds things nothing depends on.
  • A Nuxt DevTools tab with findings, the layer graph and symbol traces. It updates when you save. Try the live demo.
  • Cycle detection, presets, an ESLint plugin, SARIF output for the GitHub Security tab, and an MCP server so your coding assistant can ask "may I use this here?" before it writes the code.

Works with Nuxt 3 and 4, and with local, npm and git layers.

Try it in five minutes

npx nuxi module add nuxt-layerscope --dev
npx layerscope init --baseline
npx layerscope check --prepare
Enter fullscreen mode Exit fullscreen mode

Or poke at the example project on StackBlitz first. It has four layers and one violation for each rule.

It is MIT and still young (0.x), so bug reports and weird edge cases are very welcome. If your layers have a secret you want to find out, run it and tell me how many violations you got. I promise not to judge. Much.

Top comments (0)