Nuxt & VueLesson
Auto-imports
Import lines are noise in a small app and a chore in a big one. Nuxt drops most of them: a component, composable or helper in the right folder can be used anywhere by name. The price is that every name lives in one shared space, and in an app built from many layers, two folders can claim the same one.
Know Nuxt auto-imports already? Skip to In Terrarium.
Outside the box · General
The concept
Nuxt scans a set of folders and makes what they export usable without an import line: app/components/ for Vue components, and, for composables, helper functions and types, app/composables/, app/utils/, app/types/, shared/utils/, shared/types/ and any folder named in the imports.dirs option. Vue's own APIs, such as ref and computed, and Nuxt's, such as useRoute and navigateTo, come the same way. A build step adds the real imports to each file that uses a name, so only code that is used ships.
In composables/ and utils/ only the files at the top of the folder are scanned; a file in a subfolder needs re-exporting from one at the top. Components are named from their path: components/base/foo/Button.vue becomes <BaseFooButton>.
Nuxt writes every composable and util it found, and the file each comes from, into .nuxt/imports.d.ts, and every component into .nuxt/components.d.ts, so the editor and type checker know them. Those files exist only after nuxt prepare, nuxt dev or nuxt build. Composables and utils can also be imported explicitly from #imports, components from #components.
With layers, each layer's folders are scanned too, all into the same set of names. When two of them export the same name, one wins by the same order that settles a clash between layers' pages (Nuxt layers), and the other is dropped.
Full referenceNuxt docs: Auto-imports
In Terrarium
Each is a layer, so every Tenant's components, composables and utils are names every other part of the app can use. The Platform's home page shows how far that reaches. It builds its tiles from the Blog's PERSONA_SLUGS and personaMeta and the Atlas's BIOMES, defined in those Tenants' app/utils/ folders, without importing any of them:
10const blogEntries = PERSONA_SLUGS.map((slug) => ({
11 name: personaMeta(slug).name,
12 path: `/t/blog/${slug}`,
13 note: personaMeta(slug).epithet || undefined,
14 accent: personaMeta(slug).accent,
15}))
16
17const atlasEntries = BIOMES.map((b) => ({
18 name: b.name,
19 path: `/t/atlas/${b.slug}`,
20 note: b.character,
21 accent: b.accent,
22}))
Nothing in Nuxt stops one Tenant's page from calling another Tenant's util the same way. The home page, app/pages/index.vue, is the one exception ADR-0025 (docs/adr/0025-cross-tenant-catalog-and-collection-kinds.md) names; Cross-tenant reads shows how every other page reads another Tenant, through the .
Terrarium keeps names apart instead of relying on priority. The layer that loads whenever an agent reads or edits a layer's files says how:
13- **Auto-imports first.** A layer's `app/components/`, `app/composables/` and
14 `app/utils/` are auto-imported in every layer, components by
15 directory-prefixed name. The namespace is global: name exports in the
16 Tenant's words (`formatBlogDate`, not `fmtDate`), and never give a local
17 binding an auto-imported name.
So the Blog's date helper is formatBlogDate, and Tinkerfund's link builder is tinkerfundPath. Components get their Tenant's name from a folder, as Nuxt layers shows.
Why it's built this way
- Names in the Tenant's words. Every layer's exports share one namespace (Nuxt layers), and when two layers export one name, layer priority keeps one and drops the other without a warning. Terrarium names each export in its Tenant's words, such as
formatBlogDate, rather than rely on priority. The cost: longer names, and a manual check of.nuxt/imports.d.tsbefore adding a generic one. .claude/rules/tenant-layers.md
What to watch for
- A clash fails silently. Two exports with one name build, type-check and run; code everywhere just gets one of them, which may not be the one its author meant. Before adding a generic name such as
formatDate, search.nuxt/imports.d.tsfor it. - A missing component doesn't fail the build. A misspelt or renamed component renders as an unknown HTML element, and the page shows around the gap. The smoke test catches it in a real browser with
collectUnknownElementTagsin tests/support/e2e.ts. - A new export is unknown until the types are regenerated. The editor flags a util added while no dev server runs until
nuxt prepareornuxt devrewrites.nuxt/imports.d.ts, andpnpm typecheckregenerates it first (Type checking). - A top-level local name hides the import in the whole file. Nuxt adds no import for a name the file declares at its top level, so every use of that name in the file gets the local value. A name declared inside a function, or as a parameter, leaves the import in place everywhere else. The rule's last clause forbids both.
Try it in your project
Run nuxt prepare and open .nuxt/imports.d.ts: it lists every composable and util your app can use without an import, and the file each one comes from. Then add app/utils/clash.ts exporting export const clash = 'app', and the same name with another value in a layer; run nuxt prepare again and see which file the listing now names.
Where next
- LessonNuxt layers
- LessonVirtual modules
Last updated · Latest source on GitHub ↗