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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Automate Vue Button Permissions with a Custom Vite Plugin
❝ 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.

Image
Image
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontendpluginpermissionpiniaVite
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.