Automate Vue Button Permissions with a Custom Vite Plugin
This article explains how to create a Vite plugin that automatically injects permission checks into Vue button components by generating unique permission codes, handling various UI libraries, and integrating with Pinia or Vuex for seamless front‑end access control.
❝ Recently I worked on a middle‑platform system's permission control feature; routing and role permissions were simple, but implementing button permissions was cumbersome due to the sheer number of buttons. ❞
Introduction
After a long pause, I revisited the permission control feature. While routing and role permissions are straightforward, button permissions become tricky because of the large quantity of buttons. A naïve approach uses a custom directive on each button, but this leads to massive duplicated code and poor maintainability.
Therefore I decided to write a vite plugin that automatically generates the code for comparing button permissions. The implementation details follow.
Basic Idea
During project build, vite automatically inserts permission‑checking code globally and compares it with the permission list stored in a pinia store.
The concept is simple, but many details need careful handling.
1. How to generate a unique permission code for each button
The code is generated automatically following the rule: PermissionCode = Path + '_' + Suffix, ensuring uniqueness.
For example, for a new button in src/view/index.vue, the permission code becomes src/view/index_create.
Common suffix rules (customizable):
新增 : create 编辑 : edit 删除 : delete 查看 : view 导出 : export Simple example code:
// relative src path
const filePath = relative(process.cwd(), id).replace(extname(id), '')
const result = code.split('
')
// mapping table
const butTextMap: Record<string, string> = {
'新增': 'create',
'编辑': 'edit',
'删除': 'delete',
'查看': 'view',
'导出': 'export',
}
// compose permission code
const permCode = `${filePath}_${suffix}`2. Handling buttons from different UI libraries
The system may use native button, el-button, or other UI library buttons. The plugin should recognize these variants, typically by checking for the button keyword.
3. Special buttons that cannot follow the rule
Some highly customized buttons like "跳转系统" do not fit the common patterns. The solution is to allow developers to manually specify a permission code on the button; the plugin will skip insertion if a code already exists.
4. Choosing the Vite hook for insertion
Vite offers many lifecycle hooks (e.g., resolveId, load, transform, handleHotUpdate, generateBundle). For this use‑case, the transform hook is ideal because it provides the full source code and works in both development and production.
5. Specific insertion strategy
Instead of using fragile regexes, the plugin parses files into an AST using vue-eslint-parser and walks the AST with walk. This approach is safer and can accurately identify button nodes.
Example of a regex pitfall: <!-- <button> --> Comments are ignored by the parser, avoiding false positives.
6. Parameter passing
After injecting the permission code, the plugin needs to fetch the permission list from vuex or pinia. The implementation imports the store and adds a <script setup> block if necessary.
❝ Note: To prevent duplicate imports, check whether the code already exists before inserting. ❞
Code Implementation
The following is the complete, usable plugin code.
import type { Plugin } from 'vite';
import { relative, extname } from 'path';
import { parse, walk } from 'vue-eslint-parser';
import { generate } from 'escodegen';
export default function autoPermissionPlugin({ srcDir = 'src' } = {}): Plugin {
const filter = (id: string) => /\.vue$/.test(id);
return {
name: 'tty-auto-permission',
transform(code, id) {
if (!filter(id)) return;
try {
const ast = parse(code, { ecmaVersion: 2020, sourceType: 'module', loc: true });
const filePath = relative(process.cwd(), id).replace(extname(id), '');
const butTextMap: Record<string, string> = { 新增: 'create', 编辑: 'edit', 删除: 'delete', 查看: 'view', 导出: 'export' };
const templateAST = ast.templateBody;
if (templateAST) {
walk(templateAST, {
enter(node) {
if (node.type === 'VElement' && ['button', 'a-button', 'el-button'].includes(node.name)) {
let suffix: string | undefined;
const buttonText = node.children?.find(c => c.type === 'VText')?.value.trim();
if (buttonText && butTextMap[buttonText]) suffix = butTextMap[buttonText];
const clickHandler = node.attributes.find(attr => attr.key.name === '@click');
if (clickHandler?.value?.expression?.callee?.name) {
const fnName = clickHandler.value.expression.callee.name;
if (fnName.startsWith('handle')) suffix = fnName.charAt(6).toLowerCase() + fnName.slice(7);
}
if (suffix) {
const permCode = `${filePath}_${suffix}`;
const hasPermissionDirective = node.startTag.attributes.some(
attr => attr.type === 'VDirective' && attr.key.name.name === 'if' && attr.value?.value?.includes('hasPerm')
);
if (hasPermissionDirective) return;
node.startTag.attributes.push({
type: 'VDirective',
key: { name: { name: 'if' } },
argument: null,
modifiers: [],
value: { type: 'VLiteral', value: `permissionStore.hasPerm('${permCode}')` },
});
}
}
},
});
}
const hasImportStore = code.includes("import { butPermissionStore } from '@/stores/butPermission'");
const warehouseCode = `<script setup>
import { butPermissionStore } from '@/stores/butPermission'
const permissionStore = butPermissionStore()
</script>`;
if (!code.includes('<script')) {
ast.body.unshift(parse(warehouseCode).body[0]);
} else {
walk(ast, {
enter(node) {
if (node.type === 'VElement' && node.name === 'script' && node.startTag.attributes.some(attr => attr.key.name === 'setup')) {
if (!hasImportStore) {
const importNode = parse(warehouseCode).body[0];
ast.body.splice(ast.body.indexOf(node) + 1, 0, importNode);
}
this.skip();
}
},
});
}
const newCode = generate(ast);
return { code: newCode, map: null };
} catch (e) {
console.error(`Permission injection failed: ${id}`, e);
return { code, map: null };
}
},
};
}Usage in a Vite project:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import autoPermissionPlugin from './plugins/autoPermissionPlugin';
export default defineConfig({
plugins: [vue(), autoPermissionPlugin()],
});Conclusion
The custom vite permission plugin is now functional for the specific project, though it hasn't been published to npm because a generic solution would require broader compatibility considerations.
Implementing this plugin is straightforward, but many details must be addressed to avoid bugs. Feel free to comment with suggestions or corrections for further learning.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
