@eduvia-app/nuxt-can
v1.1.1
Published
Nuxt directives (`v-can`, `v-cannot`) to layer permissions without touching business v-ifs.
Downloads
53
Maintainers
Readme
nuxt-can
nuxt-can ships two Vue directives (v-can, v-cannot) so you can encode permissions directly in Nuxt templates. Each directive is transformed at build time into a composable __can__ call provided by your app, keeping the runtime lean, tree-shake friendly, and fully typed.
Highlights
- ✅ Cleanly adds permissions to existing templates without rewriting your business
v-ifs - ✅ Compile-time transform of
v-can/v-cannotintov-ifguards - ✅ Smart merge with existing
v-ifconditions (no extra wrappers) - ✅ Auto-generated
canproxy with types derived from your permissions map - ✅ Pluggable import of the host
__can__function (stores, APIs, etc.) - ✅ Helpful DX errors for unsupported directive shapes
Quick Start
Install the module in your Nuxt app:
npm install @eduvia-app/nuxt-can
# or
npx nuxi module add @eduvia-app/nuxt-canEnable it inside nuxt.config.ts and describe the permissions tree:
// nuxt.config.ts
import NuxtCan from '@eduvia-app/nuxt-can'
export default defineNuxtConfig({
modules: [NuxtCan],
nuxtCan: {
permissions: {
employee: ['view', 'edit'],
contract: ['create'],
},
canFunctionImport: '~/permissions/can', // path to your __can__ implementation
},
})Provide the __can__ implementation referenced above:
// permissions/can.ts
const permissionsStore = usePermissionsStore()
export function __can__(...path: string[]) {
return permissionsStore.check(path.join('.'))
}Now you can write directives that stay type-safe:
<template>
<button v-can="can.employee.view">View profile</button>
<button v-if="isReady" v-can="can.employee.edit">
Edit profile
</button>
<p v-cannot>Access denied</p>
</template>…and the compiler rewrites them into plain conditionals:
<button v-if="__can__('employee', 'view')">View profile</button>
<button v-if="__can__('employee', 'edit') && (isReady)">Edit profile</button>
<p v-if="!(__can__('employee', 'edit'))">Access denied</p>Directive Patterns
Guard entire v-if / v-else-if / v-else chains
Once the first branch of a conditional chain carries v-can, the transformer automatically mirrors that guard (with the same permission path) on every subsequent v-else-if and v-else. You can still repeat the directive manually for clarity, but it’s no longer required.
<div v-if="status === 'draft'" v-can="can.foo.bar">
Draft state
</div>
<div v-else-if="status === 'pending'">
Pending state
</div>
<div v-else>
Fallback state
</div>
<div v-cannot="can.foo.bar">
Missing permission
</div>Transforms into:
<div v-if="__can__('foo', 'bar') && (status === 'draft')">
Draft state
</div>
<div v-else-if="__can__('foo', 'bar') && (status === 'pending')">
Pending state
</div>
<div v-else-if="__can__('foo', 'bar')">
Fallback state
</div>
<div v-if="!__can__('foo', 'bar')">
Missing permission
</div>Pass arguments to v-cannot
v-cannot can mirror the permission expression used by its matching v-can by adding the same argument (v-cannot="can.foo.bar"). When no argument is specified, the directive must immediately follow the preceding v-can block so the transformer can re-use that context.
<button v-can="can.contract.submit">Submit contract</button>
<p v-cannot="can.contract.submit">Contact your admin to unlock submissions.</p>
<template>
<button v-if="isReady" v-can="can.contract.edit">Edit</button>
<p v-cannot>Only editors can update this contract.</p>
</template>
<!-- Need to wrap the fallback? pass the expression explicitly -->
<div class="notice">
<p v-cannot="can.contract.edit">Only editors can update this contract.</p>
</div>Both v-cannot branches above compile to v-if="!__can__('contract', 'submit')" and v-if="!__can__('contract', 'edit')" respectively.
Keep v-cannot next to its v-can
When v-cannot omits an expression, it must immediately follow the guarded block:
<div v-if="isReady" v-can="can.foo.bar">Ready!</div>
<p v-cannot>Not allowed</p> <!-- ✅ adjacent, guard is inferred -->
<div>
<p v-cannot>Not allowed</p> <!-- ❌ wrapped, missing explicit expression -->
</div>
<div>
<p v-cannot="can.foo.bar">Not allowed</p> <!-- ✅ wrapper + explicit permission -->
</div>Usage Rules & Errors
The transformer validates every template and throws descriptive errors when:
v-canexpressions differ within the samev-if/v-else-if/v-elseblock (the guard is mirrored automatically, but mixed expressions are disallowed).v-cannotwithout an argument is separated from its originatingv-can.v-cannotmixes in modifiers or av-ifcondition (keep it standalone).- Multiple
v-cannotblocks exist for the samev-can. - The expression is not a static dotted path like
can.resource.action.
Generated Types
The permissions map feeds a generated types/nuxt-can.d.ts declaration that augments:
ComponentCustomPropertieswithcan,$can, and__can__.NuxtAppwith$canand$__can__.- Runtime typings for the
#build/nuxt-can/can-import.mjsbridge.
No extra setup is required for editors or strict TypeScript projects.
Why v-can?
Retrofitting authorization into an existing codebase often means revisiting every v-if to sprinkle permission checks alongside business logic. That makes templates harder to read, increases the risk of regressions, and couples security rules with UI state management. v-can and v-cannot isolate the permission layer: you keep your original conditions untouched while the transformer injects the __can__ guards for you. As a result, business logic stays readable, authorization lives in one place, and code reviews can focus on either concern without stepping on each other.
Playground
Run npm run dev to explore the playground app located in /playground. It demonstrates:
- Stacked
v-can/v-cannotpairs. - Interaction with existing
v-ifs andv-fors. - Template blocks that share the same permission guard.
- A live permission summary powered by the injected
__can__function.
Feel free to wire your own ~/playground/permissions/__can__.ts to mimic a real backend.
Local Development
# Install dependencies
npm install
# Prepare type stubs and the playground
npm run dev:prepare
# Playground dev server
npm run dev
# Build the playground
npm run dev:build
# Lint & tests
npm run lint
npm run test
npm run test:watch
# Type checks (module + playground)
npm run test:types
# Release pipeline
npm run releaseContributing
- Fork & clone the repo.
- Run
npm run dev:prepareonce to scaffold stubs. - Use the playground (
npm run dev) to reproduce issues. - Add tests under
test/and fixtures undertest/fixtures/*. - Open a PR following Conventional Commits (e.g.
feat:,fix:).
