Glass Box Terrarium, explained from the inside

Last updated · Latest source on GitHub ↗

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

Looking inside

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:

app/pages/index.vue ↗10–22as of
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:

.claude/rules/tenant-layers.md ↗13–17as of
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.ts before 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.ts for 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 collectUnknownElementTags in 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 prepare or nuxt dev rewrites .nuxt/imports.d.ts, and pnpm typecheck regenerates 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

Last updated · Latest source on GitHub ↗

Something went wrong

This content couldn’t be loaded. Reloading the page usually fixes it.

Technical details
route: /t/glassbox/guide/nuxt-vue/auto-imports