Getting a grip on Nuxt's auto-import functionality

Intro

One of Nuxt 3's stand-out features is its auto-import functionality which promises to reduce developer friction by removing the burden of managing imports, and even having to worry about the source of any particular dependency!

But perhaps you – like me – find the auto-import experience doesn't quite deliver, with supposed time savings eclipsed by new problems such as poor IDE integration, difficulty locating files, or understanding your application structure.

I wanted to get to the bottom of this paradox so after my original complaint in my Nuxt Layers article, I decided to spend some time getting to know auto-import; when it was useful, when less so, and what workarounds there might be.

I've tried to keep the article as short as I can, but there's quite a lot to cover!

Overview

So let's reacquaint ourselves with how auto-import works, or if you were fuzzy on the specifics, shine a light on them.

Background

Nuxt leverages unjs/unimport (which in turn leverages unjs/unplugin) to supply the auto-import magic.

It's a build process which scans configured folders, hoists discovered dependencies into the global scope, injects relevant import statements into the build, and connects your IDE via TypeScript declarations (check your .nuxt/components.d.ts and .nuxt/imports.d.ts).

For a little more detail, here's how Chat GPT describes it.

Implementation

Whilst "auto-import" is a catch-all term which covers both components and code, there are actually significant differences and subtle ambiguity across their naming, documentation, behaviour, and defaults:

BehaviourComponentsCode
DocumentationComponentsComposables
Default folders~/components~/composables, ~/utils, ~/server/utils
Direct import#components#imports
Configuration optioncomponentsimports
Heavy-lifting done byFramework codeunjs/unimport
Folder scanning defaultsNestedTop-level
Path formatAbs path, rel path, aliasesAbs path, rel path, aliases, globs
NotesAuto-prefixes nested folders

Did you spot the potential footgun?

It's that Nuxt 3 – by default – will auto-prefix nested components.

It's important to understand the ramifications of this, as it directly affects the nature of your codebase, including:

  • component organisation
  • component usage
  • IDE integration
  • refactoring

Component specifics

So how does auto-prefixing play a part with auto-importing components?

Given that auto-imports are global by definition, and your project could have potentially many 100s of components, Nuxt looks to sidestep global namespace collisions by prefixing component auto-imports with their folder path.

As such, Nuxt's out-of-the-box component "auto-importing" is also component auto-renaming:

FolderComponent nameAuto-import name
componentsDropdown.vueDropdown.vue
components/formDropdown.vueFormDropdown.vue
components/form/optionsDropdown.vueFormOptionsDropdown.vue
components/form/optionsDropdownItem.vueFormOptionsDropdownItem.vue

Unfortunately, the docs mainly skip over this fundamental choice:

The upshot is:

  • if you didn't know about the new defaults, you may already have found auto-imports mysteriously "broken"
  • even if you did, you may not be aware of the indirect ramifications path-prefixing may impose upon you

Configuration

Overview

Of course, like most things in Nuxt, the defaults can be reconfigured.

On first glance it seems that components and imports are configured quite similarly:

export default defineNuxtConfig({
  [option]: {
    ...
    dirs: [
      'path/to/dir',
    ]
  },
})
TypeScript

However, it's good to know there are some subtle variations between them:

  • components has a top-level object configuration as well as an array shorthand
  • components.dirs can be more specifically-configured using objects
  • imports.dirs supports only paths or globs

The config documentation is somewhat scattered and incomplete, so it doesn't hurt to check the actual source code:

Note, that whilst only some of the config options are documented (and I can't tell you the reason for this) reading through the doc comments in these files provides additional insight into the decisions the framework makes to create your application from the raw source code you write.

Components

If you decide to configure (or, reconfigure) component auto-prefixing, your primary options are:

// src/nuxt.config.ts
export default defineNuxtConfig({
  components: [
    // use defaults: use path prefix
    '~/core/components',

    // override defaults: no path prefix
    { path: '~/layers/site/components', pathPrefix: false },

    // override defaults: no path prefix, register all globally (for Nuxt Content)
    { path: '~/layers/blog/components', pathPrefix: false, global: true },
  ]
})
TypeScript

Note that I'm using the array shorthand above, because I'm not supplying any root options.

You can even completely disable component auto-importing per project, and per layer:

// src/nuxt.config.ts or src/layer/nuxt.config.ts 
export default defineNuxtConfig({
  components: []
})
TypeScript

Imports

For imports – for which the defaults are generally fine – the options are much simpler:

// src/nuxt.config.ts
export default defineNuxtConfig({
  imports: {
    // optionally disable (Nuxt 3)
    autoImport: false,
    
    // optionally disable (Nuxt 4)
    scan: false,
    
    // load all composables at all depths
    dirs: [
      '~/**/composables/**',
    ]
  }
})
TypeScript

Demo

If you want to see the impact of the above, check this sample repo:

Navigate to the nuxt.config.ts file where you can quickly re-configure the components option:

export default defineNuxtConfig({
  components: config.arr.default // <-- change to something like config.obj.noPrefix
})
TypeScript

You'll be able to compare the array, object and pathPrefix settings, and see how they combine to import – or in some cases not import – the components you might expect.

Project size considerations

Approach

Now you're up to speed on configuration, let's cover opting in, opting out, or sitting somewhere between.

On anything other than a quick demo, you may want to either:

  • settle on a folder strategy, such as 2-levels deep, singular-names (i.e. components/dropdown/DropdownItem.vue), or
  • turn off path-prefixing in favour of explicitly-named components, or
  • turn off auto-importing in favour of explicit imports, or
  • a mixture of the last two approaches

The motivation for each may depend on:

  • how large the project is
  • how well the team knows the codebase
  • how comfortable the developers are with magic
  • your style of development
  • your IDE choice

Small to medium projects

In a small or medium projects, it's reasonably simple to keep track of component auto-importing:

+- src
    +- components
        +- account                    <-- nested folders; prefixed component names
        |   +- AccountSettings.vue
        |   +- AccountLogin.vue
        +- article
        |   +- ArticleList.vue
        |   +- ArticleItem.vue
        +- dropdown
        |   +- Dropdown.vue
        |   +- DropdownOption.vue
        +- site
        |   +- Footer.vue
        |   +- Header.vue
        |
        +- Button.vue                 <-- root level; no prefixing
        +- Dropdown.vue

However, note:

  • the mix of top-level contexts, i.e. core (dropdown), global (site) and domain (account, article) concerns
  • the mix of physical prefixes and auto-prefixes site/Footer, Dropdown (which some IDEs may fail to reference)
  • that top-level imports are not renamed, but nested components are

If your project is small and everything is reasonably accessible in the Project Explorer, maybe that's fine.

But, two questions regarding organisation:

  • are you sure your small project won't eventually become a large project?
  • as your project grows, are you happy with enforced path-prefixing constraints?

If your project does expand and you need to organise further, expect longer, concatenated naming such as:

<FormDropdown>
  <FormDropdownOption />
</FormDropdown>
HTML

Or perhaps:

<AccountSettingsDropdownOption />
HTML

You get the idea.

Large projects

So let's take a large project, such as Elk, a Mastodon client written by some of the core Nuxt team, and a great showcase for what Nuxt can do.

The app has about 50 routes, with 24 top-level components folders and around 180 component files. They've stuck mainly to a 2-level structure with occasional strategic nesting:

+- src
    +- components
        +- account
        +- aria
        +- ...
        +- common
        +- ...
        +- status
        |   +- edit
        |   |   +- StatusEditHistory.vue
        |   |   +- StatusEditHistorySkeleton.vue
        |   |   +- ...
        |   +- ...
        +- ...

You could argue at least this is reasonably organised and mainly consistent, but let's say I'm a new team member, and I'm trying to understand how StatusEditHistorySkeleton fits into the overall site.

The actual import hierarchy is:

+- components/status/edit/StatusEditHistorySkeleton.vue
+- components/status/edit/StatusEditHistory.vue
+- components/status/edit/StatusEditIndicator.vue
+- components/status/StatusDetails.vue
+- pages/[[server]]/@[account]/[status].vue

But with auto-imports there are no direct links between the files, so your options are:

  • hope your IDE will determine the real component source or find usages
    • which may only work if the component filename matches the full auto-import
    • plus, this can take significant time if an auto-import or usages have not already been found and cached
  • go via the .nuxt/components.d.ts file
    • which will have ~4x the number of entries as components, plus globals
  • perform a manual search using the component's full name

Note that I'm not taking sides here, I'm merely highlighting:

  • the ratio of ease of importing to ease of locating diminishes as the project grows larger
  • you should understand the mechanisms in case you later decide to refactor

Very large projects

I'm currently working with Forgd on their web app, which has about 75 routes, 34 component folders (not much organisation yet), ~280 components, and additional .story.vue files.

We recently refactored to layers (making it much easier to locate files) and are currently reviewing auto-imports.

Here's an example of both core and domain files in our folder structure:

+- core
|   +- components
|   |   +- chart
|   |   +- ui
|   |   |   +- UiButton.vue
|   |   +- ...
|   +- ...
+- layers
    +- ...
    +- token-designer
    ⋮   +- components
        |   +- td
        |   ⋮   +- adjust
        |       ⋮   +- tab
        |           ⋮   +- TdAdjustTabPriceAndMarketCapPerformance.vue
        +- pages
        ⋮   +- token-designer
            ⋮   +- adjust
                ⋮   +- simulating-post-tge-pops.vue

Things to note about the above:

  • a core layer contains all global concerns
  • we rely on prefixes (such as Td) to keep naming sane
  • we use aliases (such as #td) to target layers

What's interesting regarding the deep nesting above, is that any of the following are valid auto-import locations if path-prefixing is turned on – but IDE tooling may only would likely only locate 4 of these files:

components                -->  TdAdjustTabPriceAndMarketCapPerformance
 
components/td             -->  TdAdjustTabPriceAndMarketCapPerformance
                               AdjustTabPriceAndMarketCapPerformance  
 
components/td/adjust      -->  TdAdjustTabPriceAndMarketCapPerformance
                               AdjustTabPriceAndMarketCapPerformance  
                               TabPriceAndMarketCapPerformance        
 
components/td/adjust/tab  -->  TdAdjustTabPriceAndMarketCapPerformance
                               AdjustTabPriceAndMarketCapPerformance  
                               TabPriceAndMarketCapPerformance        
                               PriceAndMarketCapPerformance

Note also, without short prefixes, we could be typing component names like the following!

td:   <TokenDesignerAdjustTabPriceAndMarketCapPerformance />
ad:   <AutoDistributionConfiguratorStrategyDetails />
amm:  <AutomatedMarketMakingStrategyCompleteCta />

We are currently considering:

  • auto-importing only core-level components, and without auto-prefixing, i.e. core/forms/UiButton
  • moving away from auto-importing domain-level components, i.e. PriceAndMarketCapPerformance
  • moving towards local index files to export related sets of components

If that was the case, domain-level imports / usage may look like this:

<script >
import {
  PriceAndMarketCapPerformance,
  ...
} from '#td/components/adjust'
</script>

<template>
  <PriceAndMarketCapPerformance />
  <UiButton />
  ...
</template>
Vue

It's a very small amount of extra code, but:

  • the major IDEs add imports automatically
  • a Cmd-Click is guaranteed to take you directly to the component
  • VSCode can be coerced into supporting .vue file refactoring

IDE integration

Warning

This article was first published in May 2024, so this section may be out-of date today.

Note to self to review!

Preparation

Note that before you can even begin to see the benefits of type completion you will need to either build the project or run nuxi prepare to generate the stub files in Nuxt's build folder .nuxt/.

If you don't do that, code-completion is not going to work at all!

Overview

So how do the two main IDEs, VSCode and WebStorm work with these magically auto-imported files?

Well, not all IDEs are not created equal, and frameworks authors cater differently to different IDEs.

In the case of auto-imports, both VsCode and WebStorm:

  • can instantly resolve explicit imports
  • can guess at implicit imports, or go via .nuxt/components.d.ts (can take time to search the codebase)

Also:

  • WebStorm has the edge on refactoring, but VS code can be coerced into playing nice
  • Volar under WebStorm is fairly hit-and-miss; sometimes it works, sometimes it doesn't

Support

Navigate to source from usage in .vue file:

  • Cmd-Click component tag
    • WebStorm – navigates to .nuxt/components.ts, then you need to manually click the <Component>.vue filename
      • strangely, PHPStorm navigates direct to the component!
    • VSCode – navigates to the component, via .nuxt/components.ts
  • Right-Click
    • WebStorm – Go To > Declaration or Usages
    • VSCode – Go to Definition

Find all usages from usage in .vue file:

  • WebStormRight-Click > Find Usages; shows in popup
  • VSCodeRight-Click > Go to References; shows inline

Find usages from Project Explorer:

  • WebStormRight-Click > Find Usages; shows "Nothing found in 'All Places'"
  • VSCodeRight-Click > Find File References; shows .nuxt/components.d.ts

Refactoring

Implicit import refactoring:

  • TBC

Explicit import refactoring:

  • WebStorm – updates any combination of .vue or .ts renames or moves
  • VSCode doesn't update .vue to .vue renames or moves

To work around VSCode's limitations, you can re-export .vue components from an index.ts file, and it will update .vue imports if the index.ts file or containing folders are renamed or moved:

// components/somewhere/index.ts
import SomeComponent from './SomeComponent'
...

export {
  SomeComponent,
  ...
}
TypeScript
<script>
import { SomeComponent } from '~/components/somewhere'
</script>
Vue

Summary

Trade-offs

Nuxt's auto-import defaults bring with them some subtle tradeoffs – which can be magnified as the project grows:

SituationIssues
Root vs nested foldersInconsistencies between root and nested components
Missed the documentation on namingOnly same-named components auto-import
Small number of componentsEasy to find
Large number of componentsMore nesting required / harder to find
Low nestingSimple names
Deep nestingHighly-concatenated names
Long namesHard to locate / multiple possible locations
Auto-importingNo direct link in IDE / will need to search
Not-named the same as the path prefixIDE may not find file / or may take a long time to find file
Moving existing prefixed componentsWill break your app if you don't understand prefixing
Want to refactor component pathWill need to manually search and rename component names
How much typing is savedAdding shorter imports once vs typing longer component names often
Leveraging import contextLimited local imports vs filtering all globally available imports
Using layersLocal imports may be in some cases be simpler than unwieldy auto-imports
Want to leverage IDE toolingLack of explicit imports may break tools around usage

And the larger your application gets, the less magic you want and the more safety you need, so it's important to understand the pros and cons, so you can make the right choices for your project and team.

Thoughts

For small or medium projects, auto-imports are fine. But you should consider what might happen if your project grows.

I generally prefer to turn off path-prefixing, as then you're free to decide on your own prefixing strategy, and it makes it easier to refactor entire subtrees of code should you decide to.

For larger projects I feel that global concerns (such as UI components, or site furniture) and some 3rd-party code absolutely benefit from being auto-imported, but it's clearer if domain-level concerns are imported explicitly.

Not only are the relationships between the domain entities clearer, but IDE and tooling support is guaranteed (you can use index files to simplify imports and improve VSCode refactoring), and it's significantly easier to get to grips with a new project – or an old project you haven't looked at in a while!

And finally, some additional opinions from Redditors:

Last words

Maybe you're happy with auto-imports and don't feel the need to change. Or maybe auto-imports never quite worked for you, but at least you now understand them better. Or maybe it's somewhere between – which is ironically where you might end up if your project gets large enough, and you push the boundaries of auto-importing.