Skip to content

Instantly share code, notes, and snippets.

@mdmunir
Last active August 18, 2025 11:22
Show Gist options
  • Select an option

  • Save mdmunir/082cfc65195e44b1d356a480e0871ecc to your computer and use it in GitHub Desktop.

Select an option

Save mdmunir/082cfc65195e44b1d356a480e0871ecc to your computer and use it in GitHub Desktop.
Vue Vite Auto Routes
import { readdirSync, writeFileSync } from "fs";
import { dirname, resolve, extname, sep } from "path";
function listFiles(dir, prefix = '/') {
const fullPath = resolve(dir);
const entries = readdirSync(fullPath, { withFileTypes: true });
const results = {};
for (const entry of entries) {
const res = resolve(fullPath, entry.name);
if (entry.isDirectory()) {
Object.assign(results, listFiles(res, prefix + entry.name + '/'));
} else if (extname(res) == ".vue") {
results[(prefix + entry.name).replace(/\.vue$/, '')] = res;
}
}
return results;
}
function buildRoutes(pages) {
const routes = {};
Object.entries(pages).forEach(([path, page]) => {
var matches = path.match(/^(?<key>[^\@]+)(\@(?<view>[\w\-]+))?$/);
if (matches) {
var { key, view } = matches.groups;
view = view || 'default';
if (routes[key]) {
routes[key].components[view] = `<<${page}>>`;
routes[key].props[view] = true;
} else {
var p = key.replace(/\[\[([\w\-]+)\]\]\+/g, ':$1*')
.replace(/\[\[([\w\-]+)\]\]/g, ':$1?')
.replace(/\[([\w\-]+)\](\+)?/g, ':$1$2')
.replace(/\[\.\.\.([\w\-]+)\]/g, ':$1(+*)')
.replace(/\./g, '/')
.replace('(+*)', '(.*)')
.replace(/\([\w\-]+\)/g, '')
.replace(/\/index$/, '')
.replace(/\/+/g, '/')
.replace(/\/+$/, '');
if (p == '') {
p = '/';
}
routes[key] = {
path: p,
name: key,
components: { [view]: `<<${page}>>` },
props: { [view]: true },
};
}
}
});
const keys = Object.keys(routes).sort();
const REGEX_ANY = /^(.*)\:[\w\-]+\(\.\*\)$/;
/**
*
* @param {string} key
* @returns Object[]
*/
function getChildren(key) {
const children = [];
keys.filter(s => s.startsWith(key + '/')).sort().forEach(k => {
if (routes[k]) {
const child = routes[k];
const subChildren = getChildren(k);
if (subChildren.length) {
child.children = subChildren;
}
children.push(child);
delete routes[k];
}
});
return children.sort((a, b) => {
let [p1, p2] = [a.path, b.path];
let matches = p1.match(REGEX_ANY);
if (matches) {
return p2.startsWith(matches[1]) ? 1 : 0;
}
matches = p2.match(REGEX_ANY);
if (matches) {
return p1.startsWith(matches[1]) ? -1 : 0;
}
return 0;
});
}
return getChildren('');
}
export default function AutoRoute(config) {
if (typeof config === 'string') {
config = { sourcePath: config };
}
const virtualModuleId = 'virtual:auto-route';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
const sourcePaths = Array.isArray(config.sourcePath) ? config.sourcePath : [config.sourcePath];
var routes = [];
sourcePaths.forEach(source => {
if (typeof source === 'string') {
source = { path: source, prefix: config.prefix || '/' };
}
const pages = listFiles(source.path, source.prefix || '/');
routes = routes.concat(buildRoutes(pages));
});
function applyImport(content, aliases) {
function toRelative(f) {
for (let i = 0; i < aliases.length; i++) {
if (f.startsWith(aliases[i].path)) {
return aliases[i].alias + f.substring(aliases[i].length);
}
}
return f;
}
const REGEX_IMPORT1 = /import (\w+) from \"(.+)\";$/gm;
const REGEX_IMPORT2 = /\"\<\<([^\>]+)\>\>(\<\<LAYOUT\>\>)?\"/g;
return content.replace(REGEX_IMPORT1, (_, m, f) => {
let file = aliases ? toRelative(f) : f;
return `import ${m} from "${file}";`;
}).replace(REGEX_IMPORT2, (_, f, ly) => {
let file = aliases ? toRelative(f) : f;
return `() => import("${file}")` + (ly ? '.then(m => wrapLayout(m))' : '');
});
}
let content = '';
if (config.layout) {
const layouts = [];
const imports = [];
Object.entries(config.layout).forEach(([k, f], ix) => {
imports.push(`import m_layout_${ix} from ${JSON.stringify(resolve(f))};`);
k = /^\w+$/.test(k) ? k : JSON.stringify(k);
layouts.push(` ${k}: m_layout_${ix}`);
});
let s = JSON.stringify(layouts, null, 2).replace(/\"(\w+)\"\:/g, '$1:');
content += `import {h, defineComponent} from 'vue';
${imports.join('\n')}
const Layouts = {
${layouts.join(',\n')}
};
function wrapLayout(module) {
const child = module.default;
var layout = child.layout;
if (layout === undefined) {
layout = Layouts.default;
} else if (typeof layout === 'string') {
layout = Layouts[layout];
}
if (layout) {
return {default: defineComponent({
name: 'LayoutWrapped',
props: child.props,
setup(props, { attrs, slots }) {
return () => h(layout, attrs, { default: () => h(child, props, slots) });
}
})};
}
return module;
}
`;
routes.forEach(route => {
if (route.components.default) {
route.components.default = route.components.default + '<<LAYOUT>>';
}
});
}
let s = JSON.stringify(routes, null, 2).replace(/\"(\w+)\"\:/g, '$1:');
content += `export const routes = ${s};
export default routes;`;
if (config.out) {
const fullPath = resolve(config.out);
let p = dirname(fullPath);
const aliases = [
{ path: p + sep, alias: './', length: p.length + 1 }
];
let alias = '../';
while (true) {
let p2 = dirname(p);
if (p2 == p) {
break;
}
p = p2;
p2 = (p + sep).replace(/\/+/g, '/');
aliases.push({ path: p2, alias, length: p2.length });
alias += '../';
}
writeFileSync(fullPath, applyImport(content, aliases), 'utf8');
}
return {
name: 'auto-route', // required, will show up in warnings and errors
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return applyImport(content);
}
},
}
}
@mdmunir
Copy link
Author

mdmunir commented Aug 18, 2025

usage
vite.config.js

    plugins: [
            AutoRoute({
                sourcePath:'./src/pages',
                layout:{
                    default:'./src/layouts/Default.vue',
                    main:'./src/layouts/Main.vue',
                },
                out:'./src/auto-routes.js', // optional
            }),

src/app.js

import routes from 'virtual:auto-route';
import { createRouter, createWebHashHistory, RouterView } from 'vue-router';
import { createApp, h } from 'vue';

const router = createRouter({
    history: createWebHashHistory(),
    routes,
});

const App = { render: () => h(RouterView) };
createApp(App)
    .use(router)
    .mount('#app');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment