unplugin-auto-declare
v0.0.5
Published
Auto-declare composable bindings in Vue <script setup> for Vite and Webpack
Maintainers
Readme
unplugin-auto-declare
Auto-declare composable bindings in Vue <script setup> so you can use template-style globals for commonly used composables.
This plugin transforms code; it doesn't import composables. Pair it with unplugin-auto-import to resolve composables.
without:
<script setup>
const { t: $t } = useI18n();
const greeting = computed(() => $t("hello"));
</script>with:
<script setup>
const greeting = computed(() => $t("hello"));
</script>Install
pnpm add -D unplugin-auto-declare unplugin-auto-importUsage (vue-i18n example)
// vite.config.ts
import AutoImport from "unplugin-auto-import/vite";
import AutoDeclare from "unplugin-auto-declare/vite";
import { vueI18n } from "unplugin-auto-declare/presets";
export default {
plugins: [
AutoImport({ imports: ["vue-i18n"] }),
AutoDeclare({ matchers: [vueI18n] })
],
};Types
Add the shipped ambient types:
// tsconfig.json
{ "compilerOptions": { "types": ["unplugin-auto-declare/vue-i18n"] } }Caveat: these are
declare globalambients, so the identifiers appear in every.ts/.vuefile in the project — TypeScript can't scope a global to a file pattern. Same limitation as auto-import tooling. Referencing$toutside<script setup>typechecks but fails at runtime.
Component-scoped, not global
These look like template globals but aren't — the composable is invoked in each component's <script setup>, same as a hand-written call. So:
- Setup-only. Subject to the same rules as calling the composable manually. Don't add a matcher for something that can't run in setup.
- Per-component instantiation. For composables that share state internally (
useI18n, Pinia stores,useRuntimeConfig) the cost is one extra call per component returning a shared singleton. Composables that create per-call state give every component its own copy — use this for things you'd already call in setup, not to fake an app-wide singleton. - Lifecycle is per-component.
onUnmounted, watchers, and effect scopes attach to the calling component.
Recommendations
Keep the matcher list small. Every identifier is effectively a global in <script setup> — pollution scales with the matcher list, not your codebase, and compounds across every component a contributor reads. Reach for it when an identifier is genuinely ambient (i18n helpers, the active store, a logger), and use ordinary imports for everything else.
Prefix auto-declared identifiers with $ to match Vue's template-global convention ($t, $route, $attrs). The $ makes it visually obvious the binding wasn't declared locally.
Multiple instances
Safe to register more than once (e.g. a Nuxt module + a user project). Disjoint matchers stay independent; overlapping matchers dedupe via the same scope tracking that handles user-written declarations — the first instance injects, the second sees it as already-declared. Order doesn't matter. Cost is one parse + walk per instance for files containing any tracked identifier; if you're stacking many registrations, prefer merging matchers into one.
Custom matchers
A matcher describes which identifier(s) to look for and how to inject the composable. kind defaults to 'direct'.
Direct matcher (default)
One composable, one identifier:
AutoDeclare({
matchers: [
{ identifier: "$config", composable: "useRuntimeConfig" },
],
});becomes:
<script setup>
// const $config = useRuntimeConfig(); — injected by the plugin
const url = $config.public.apiBase;
</script>Destructure matcher
One composable, several named identifiers, emitted as a single destructure. kind: 'destructure' is required.
AutoDeclare({
matchers: [
{
kind: "destructure",
identifiers: ["$t", "$n"],
composable: "useI18n",
mapping: { $t: "t: $t", $n: "n: $n" },
},
],
});becomes:
<script setup>
// const { t: $t, n: $n } = useI18n(); — injected by the plugin
const greeting = computed(() => $t("hello"));
const count = computed(() => $n(42));
</script>mapping[id] is the destructure entry (defaults to id itself). The bundled vue-i18n preset is a pre-filled destructure matcher.
Mix direct and destructure matchers in the same array; one declaration line per matcher with hits, in matcher-list order.
What counts as a usage
The plugin matches any reference to a tracked identifier in <script setup> — calls ($t('hello')), property access ($config.public.foo), or shorthand props ({ $t }). It deliberately skips:
- member-access labels:
obj.$t - object literal keys:
{ $t: 'literal' } - bindings:
function fn ($t) {},const { $t } = …,import { $t } …— treated as already-declared
Typing custom matchers
Ship an ambient .d.ts for your identifiers:
import type { Store } from "pinia";
declare global {
const $store: Store;
}
export {};License
MIT
