物业代码生成

This commit is contained in:
2025-06-18 11:03:42 +08:00
commit 1262d4c745
1881 changed files with 249599 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View File

@@ -0,0 +1,59 @@
{
"name": "@vben/vite-config",
"version": "5.5.6",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "internal/vite-config"
},
"license": "MIT",
"type": "module",
"scripts": {
"stub": "pnpm unbuild --stub"
},
"files": [
"dist"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.mjs"
}
},
"dependencies": {
"@intlify/unplugin-vue-i18n": "catalog:",
"@jspm/generator": "catalog:",
"archiver": "catalog:",
"cheerio": "catalog:",
"get-port": "catalog:",
"html-minifier-terser": "catalog:",
"nitropack": "catalog:",
"resolve.exports": "catalog:",
"vite-plugin-pwa": "catalog:",
"vite-plugin-vue-devtools": "catalog:"
},
"devDependencies": {
"@pnpm/workspace.read-manifest": "catalog:",
"@types/archiver": "catalog:",
"@types/html-minifier-terser": "catalog:",
"@vben/node-utils": "workspace:*",
"@vitejs/plugin-vue": "catalog:",
"@vitejs/plugin-vue-jsx": "catalog:",
"dayjs": "catalog:",
"dotenv": "catalog:",
"rollup": "catalog:",
"rollup-plugin-visualizer": "catalog:",
"sass": "catalog:",
"vite": "catalog:",
"vite-plugin-compression": "catalog:",
"vite-plugin-dts": "catalog:",
"vite-plugin-html": "catalog:",
"vite-plugin-lazy-import": "catalog:"
}
}

View File

@@ -0,0 +1,125 @@
import type { CSSOptions, UserConfig } from 'vite';
import type { DefineApplicationOptions } from '../typing';
import path, { relative } from 'node:path';
import { findMonorepoRoot } from '@vben/node-utils';
import { NodePackageImporter } from 'sass';
import { defineConfig, loadEnv, mergeConfig } from 'vite';
import { defaultImportmapOptions, getDefaultPwaOptions } from '../options';
import { loadApplicationPlugins } from '../plugins';
import { loadAndConvertEnv } from '../utils/env';
import { getCommonConfig } from './common';
function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
return defineConfig(async (config) => {
const options = await userConfigPromise?.(config);
const { appTitle, base, port, ...envConfig } = await loadAndConvertEnv();
const { command, mode } = config;
const { application = {}, vite = {} } = options || {};
const root = process.cwd();
const isBuild = command === 'build';
const env = loadEnv(mode, root);
const plugins = await loadApplicationPlugins({
archiver: true,
archiverPluginOptions: {},
compress: false,
compressTypes: ['brotli', 'gzip'],
devtools: true,
env,
extraAppConfig: true,
html: true,
i18n: true,
importmapOptions: defaultImportmapOptions,
injectAppLoading: true,
injectMetadata: true,
isBuild,
license: true,
mode,
nitroMock: !isBuild,
nitroMockOptions: {},
print: !isBuild,
printInfoMap: {
'Vben Admin Docs': 'https://doc.vben.pro',
},
pwa: true,
pwaOptions: getDefaultPwaOptions(appTitle),
vxeTableLazyImport: true,
...envConfig,
...application,
});
const { injectGlobalScss = true } = application;
const applicationConfig: UserConfig = {
base,
build: {
rollupOptions: {
output: {
assetFileNames: '[ext]/[name]-[hash].[ext]',
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'jse/index-[name]-[hash].js',
},
},
target: 'es2015',
},
css: createCssOptions(injectGlobalScss),
esbuild: {
drop: isBuild
? [
// 'console',
'debugger',
]
: [],
legalComments: 'none',
},
plugins,
server: {
host: true,
port,
warmup: {
// 预热文件
clientFiles: [
'./index.html',
'./src/bootstrap.ts',
'./src/{views,layouts,router,store,api,adapter}/*',
],
},
},
};
const mergedCommonConfig = mergeConfig(
await getCommonConfig(),
applicationConfig,
);
return mergeConfig(mergedCommonConfig, vite);
});
}
function createCssOptions(injectGlobalScss = true): CSSOptions {
const root = findMonorepoRoot();
return {
preprocessorOptions: injectGlobalScss
? {
scss: {
additionalData: (content: string, filepath: string) => {
const relativePath = relative(root, filepath);
// apps下的包注入全局样式
if (relativePath.startsWith(`apps${path.sep}`)) {
return `@use "@vben/styles/global" as *;\n${content}`;
}
return content;
},
api: 'modern',
importers: [new NodePackageImporter()],
},
}
: {},
};
}
export { defineApplicationConfig };

View File

@@ -0,0 +1,13 @@
import type { UserConfig } from 'vite';
async function getCommonConfig(): Promise<UserConfig> {
return {
build: {
chunkSizeWarningLimit: 2000,
reportCompressedSize: false,
sourcemap: false,
},
};
}
export { getCommonConfig };

View File

@@ -0,0 +1,37 @@
import type { DefineConfig } from '../typing';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { defineApplicationConfig } from './application';
import { defineLibraryConfig } from './library';
export * from './application';
export * from './library';
function defineConfig(
userConfigPromise?: DefineConfig,
type: 'application' | 'auto' | 'library' = 'auto',
) {
let projectType = type;
// 根据包是否存在 index.html,自动判断类型
if (projectType === 'auto') {
const htmlPath = join(process.cwd(), 'index.html');
projectType = existsSync(htmlPath) ? 'application' : 'library';
}
switch (projectType) {
case 'application': {
return defineApplicationConfig(userConfigPromise);
}
case 'library': {
return defineLibraryConfig(userConfigPromise);
}
default: {
throw new Error(`Unsupported project type: ${projectType}`);
}
}
}
export { defineConfig };

View File

@@ -0,0 +1,59 @@
import type { ConfigEnv, UserConfig } from 'vite';
import type { DefineLibraryOptions } from '../typing';
import { readPackageJSON } from '@vben/node-utils';
import { defineConfig, mergeConfig } from 'vite';
import { loadLibraryPlugins } from '../plugins';
import { getCommonConfig } from './common';
function defineLibraryConfig(userConfigPromise?: DefineLibraryOptions) {
return defineConfig(async (config: ConfigEnv) => {
const options = await userConfigPromise?.(config);
const { command, mode } = config;
const { library = {}, vite = {} } = options || {};
const root = process.cwd();
const isBuild = command === 'build';
const plugins = await loadLibraryPlugins({
dts: false,
injectMetadata: true,
isBuild,
mode,
...library,
});
const { dependencies = {}, peerDependencies = {} } =
await readPackageJSON(root);
const externalPackages = [
...Object.keys(dependencies),
...Object.keys(peerDependencies),
];
const packageConfig: UserConfig = {
build: {
lib: {
entry: 'src/index.ts',
fileName: () => 'index.mjs',
formats: ['es'],
},
rollupOptions: {
external: (id) => {
return externalPackages.some(
(pkg) => id === pkg || id.startsWith(`${pkg}/`),
);
},
},
},
plugins,
};
const commonConfig = await getCommonConfig();
const mergedConmonConfig = mergeConfig(commonConfig, packageConfig);
return mergeConfig(mergedConmonConfig, vite);
});
}
export { defineLibraryConfig };

View File

@@ -0,0 +1,4 @@
export * from './config';
export * from './options';
export * from './plugins';
export { loadAndConvertEnv } from './utils/env';

View File

@@ -0,0 +1,45 @@
import type { Options as PwaPluginOptions } from 'vite-plugin-pwa';
import type { ImportmapPluginOptions } from './typing';
const isDevelopment = process.env.NODE_ENV === 'development';
const getDefaultPwaOptions = (name: string): Partial<PwaPluginOptions> => ({
manifest: {
description:
'Vben Admin is a modern admin dashboard template based on Vue 3. ',
icons: [
{
sizes: '192x192',
src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-192.png',
type: 'image/png',
},
{
sizes: '512x512',
src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-512.png',
type: 'image/png',
},
],
name: `${name}${isDevelopment ? ' dev' : ''}`,
short_name: `${name}${isDevelopment ? ' dev' : ''}`,
},
});
/**
* importmap CDN 暂时不开启,因为有些包不支持,且网络不稳定
*/
const defaultImportmapOptions: ImportmapPluginOptions = {
// 通过 Importmap CDN 方式引入,
// 目前只有esm.sh源兼容性好一点jspm.io对于 esm 入口要求高
defaultProvider: 'esm.sh',
importmap: [
{ name: 'vue' },
{ name: 'pinia' },
{ name: 'vue-router' },
// { name: 'vue-i18n' },
{ name: 'dayjs' },
{ name: 'vue-demi' },
],
};
export { defaultImportmapOptions, getDefaultPwaOptions };

View File

@@ -0,0 +1,75 @@
import type { PluginOption } from 'vite';
import type { ArchiverPluginOptions } from '../typing';
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import { join } from 'node:path';
import archiver from 'archiver';
export const viteArchiverPlugin = (
options: ArchiverPluginOptions = {},
): PluginOption => {
return {
apply: 'build',
closeBundle: {
handler() {
const { name = 'dist', outputDir = '.' } = options;
setTimeout(async () => {
const folderToZip = 'dist';
const zipOutputDir = join(process.cwd(), outputDir);
const zipOutputPath = join(zipOutputDir, `${name}.zip`);
try {
await fsp.mkdir(zipOutputDir, { recursive: true });
} catch {
// ignore
}
try {
await zipFolder(folderToZip, zipOutputPath);
console.log(`Folder has been zipped to: ${zipOutputPath}`);
} catch (error) {
console.error('Error zipping folder:', error);
}
}, 0);
},
order: 'post',
},
enforce: 'post',
name: 'vite:archiver',
};
};
async function zipFolder(
folderPath: string,
outputPath: string,
): Promise<void> {
return new Promise((resolve, reject) => {
const output = fs.createWriteStream(outputPath);
const archive = archiver('zip', {
zlib: { level: 9 }, // 设置压缩级别为 9 以实现最高压缩率
});
output.on('close', () => {
console.log(
`ZIP file created: ${outputPath} (${archive.pointer()} total bytes)`,
);
resolve();
});
archive.on('error', (err) => {
reject(err);
});
archive.pipe(output);
// 使用 directory 方法以流的方式压缩文件夹,减少内存消耗
archive.directory(folderPath, false);
// 流式处理完成
archive.finalize();
});
}

View File

@@ -0,0 +1,92 @@
import type { PluginOption } from 'vite';
import {
colors,
generatorContentHash,
readPackageJSON,
} from '@vben/node-utils';
import { loadEnv } from '../utils/env';
interface PluginOptions {
isBuild: boolean;
root: string;
}
const GLOBAL_CONFIG_FILE_NAME = '_app.config.js';
const VBEN_ADMIN_PRO_APP_CONF = '_VBEN_ADMIN_PRO_APP_CONF_';
/**
* 用于将配置文件抽离出来并注入到项目中
* @returns
*/
async function viteExtraAppConfigPlugin({
isBuild,
root,
}: PluginOptions): Promise<PluginOption | undefined> {
let publicPath: string;
let source: string;
if (!isBuild) {
return;
}
const { version = '' } = await readPackageJSON(root);
return {
async configResolved(config) {
publicPath = ensureTrailingSlash(config.base);
source = await getConfigSource();
},
async generateBundle() {
try {
this.emitFile({
fileName: GLOBAL_CONFIG_FILE_NAME,
source,
type: 'asset',
});
console.log(colors.cyan(`✨configuration file is build successfully!`));
} catch (error) {
console.log(
colors.red(
`configuration file configuration file failed to package:\n${error}`,
),
);
}
},
name: 'vite:extra-app-config',
async transformIndexHtml(html) {
const hash = `v=${version}-${generatorContentHash(source, 8)}`;
const appConfigSrc = `${publicPath}${GLOBAL_CONFIG_FILE_NAME}?${hash}`;
return {
html,
tags: [{ attrs: { src: appConfigSrc }, tag: 'script' }],
};
},
};
}
async function getConfigSource() {
const config = await loadEnv();
const windowVariable = `window.${VBEN_ADMIN_PRO_APP_CONF}`;
// 确保变量不会被修改
let source = `${windowVariable}=${JSON.stringify(config)};`;
source += `
Object.freeze(${windowVariable});
Object.defineProperty(window, "${VBEN_ADMIN_PRO_APP_CONF}", {
configurable: false,
writable: false,
});
`.replaceAll(/\s/g, '');
return source;
}
function ensureTrailingSlash(path: string) {
return path.endsWith('/') ? path : `${path}/`;
}
export { viteExtraAppConfigPlugin };

View File

@@ -0,0 +1,245 @@
/**
* 参考 https://github.com/jspm/vite-plugin-jspm调整为需要的功能
*/
import type { GeneratorOptions } from '@jspm/generator';
import type { Plugin } from 'vite';
import { Generator } from '@jspm/generator';
import { load } from 'cheerio';
import { minify } from 'html-minifier-terser';
const DEFAULT_PROVIDER = 'jspm.io';
type pluginOptions = {
debug?: boolean;
defaultProvider?: 'esm.sh' | 'jsdelivr' | 'jspm.io';
importmap?: Array<{ name: string; range?: string }>;
} & GeneratorOptions;
// async function getLatestVersionOfShims() {
// const result = await fetch('https://ga.jspm.io/npm:es-module-shims');
// const version = result.text();
// return version;
// }
async function getShimsUrl(provide: string) {
// const version = await getLatestVersionOfShims();
const version = '1.10.0';
const shimsSubpath = `dist/es-module-shims.js`;
const providerShimsMap: Record<string, string> = {
'esm.sh': `https://esm.sh/es-module-shims@${version}/${shimsSubpath}`,
// unpkg: `https://unpkg.com/es-module-shims@${version}/${shimsSubpath}`,
jsdelivr: `https://cdn.jsdelivr.net/npm/es-module-shims@${version}/${shimsSubpath}`,
// 下面两个CDN不稳定暂时不用
'jspm.io': `https://ga.jspm.io/npm:es-module-shims@${version}/${shimsSubpath}`,
};
return providerShimsMap[provide] || providerShimsMap[DEFAULT_PROVIDER];
}
let generator: Generator;
async function viteImportMapPlugin(
pluginOptions?: pluginOptions,
): Promise<Plugin[]> {
const { importmap } = pluginOptions || {};
let isSSR = false;
let isBuild = false;
let installed = false;
let installError: Error | null = null;
const options: pluginOptions = Object.assign(
{},
{
debug: false,
defaultProvider: 'jspm.io',
env: ['production', 'browser', 'module'],
importmap: [],
},
pluginOptions,
);
generator = new Generator({
...options,
baseUrl: process.cwd(),
});
if (options?.debug) {
(async () => {
for await (const { message, type } of generator.logStream()) {
console.log(`${type}: ${message}`);
}
})();
}
const imports = options.inputMap?.imports ?? {};
const scopes = options.inputMap?.scopes ?? {};
const firstLayerKeys = Object.keys(scopes);
const inputMapScopes: string[] = [];
firstLayerKeys.forEach((key) => {
inputMapScopes.push(...Object.keys(scopes[key] || {}));
});
const inputMapImports = Object.keys(imports);
const allDepNames: string[] = [
...(importmap?.map((item) => item.name) || []),
...inputMapImports,
...inputMapScopes,
];
const depNames = new Set<string>(allDepNames);
const installDeps = importmap?.map((item) => ({
range: item.range,
target: item.name,
}));
return [
{
async config(_, { command, isSsrBuild }) {
isBuild = command === 'build';
isSSR = !!isSsrBuild;
},
enforce: 'pre',
name: 'importmap:external',
resolveId(id) {
if (isSSR || !isBuild) {
return null;
}
if (!depNames.has(id)) {
return null;
}
return { external: true, id };
},
},
{
enforce: 'post',
name: 'importmap:install',
async resolveId() {
if (isSSR || !isBuild || installed) {
return null;
}
try {
installed = true;
await Promise.allSettled(
(installDeps || []).map((dep) => generator.install(dep)),
);
} catch (error: any) {
installError = error;
installed = false;
}
return null;
},
},
{
buildEnd() {
// 未生成importmap时抛出错误防止被turbo缓存
if (!installed && !isSSR) {
installError && console.error(installError);
throw new Error('Importmap installation failed.');
}
},
enforce: 'post',
name: 'importmap:html',
transformIndexHtml: {
async handler(html) {
if (isSSR || !isBuild) {
return html;
}
const importmapJson = generator.getMap();
if (!importmapJson) {
return html;
}
const esModuleShimsSrc = await getShimsUrl(
options.defaultProvider || DEFAULT_PROVIDER,
);
const resultHtml = await injectShimsToHtml(
html,
esModuleShimsSrc || '',
);
html = await minify(resultHtml || html, {
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true,
removeComments: false,
});
return {
html,
tags: [
{
attrs: {
type: 'importmap',
},
injectTo: 'head-prepend',
tag: 'script',
children: `${JSON.stringify(importmapJson)}`,
},
],
};
},
order: 'post',
},
},
];
}
async function injectShimsToHtml(html: string, esModuleShimUrl: string) {
const $ = load(html);
const $script = $(`script[type='module']`);
if (!$script) {
return;
}
const entry = $script.attr('src');
$script.removeAttr('type');
$script.removeAttr('crossorigin');
$script.removeAttr('src');
$script.html(`
if (!HTMLScriptElement.supports || !HTMLScriptElement.supports('importmap')) {
self.importShim = function () {
const promise = new Promise((resolve, reject) => {
document.head.appendChild(
Object.assign(document.createElement('script'), {
src: '${esModuleShimUrl}',
crossorigin: 'anonymous',
async: true,
onload() {
if (!importShim.$proxy) {
resolve(importShim);
} else {
reject(new Error('No globalThis.importShim found:' + esModuleShimUrl));
}
},
onerror(error) {
reject(error);
},
}),
);
});
importShim.$proxy = true;
return promise.then((importShim) => importShim(...arguments));
};
}
var modules = ['${entry}'];
typeof importShim === 'function'
? modules.forEach((moduleName) => importShim(moduleName))
: modules.forEach((moduleName) => import(moduleName));
`);
$('body').after($script);
$('head').remove(`script[type='module']`);
return $.html();
}
export { viteImportMapPlugin };

View File

@@ -0,0 +1,247 @@
import type { PluginOption } from 'vite';
import type {
ApplicationPluginOptions,
CommonPluginOptions,
ConditionPlugin,
LibraryPluginOptions,
} from '../typing';
import viteVueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import viteVue from '@vitejs/plugin-vue';
import viteVueJsx from '@vitejs/plugin-vue-jsx';
import { visualizer as viteVisualizerPlugin } from 'rollup-plugin-visualizer';
import viteCompressPlugin from 'vite-plugin-compression';
import viteDtsPlugin from 'vite-plugin-dts';
import { createHtmlPlugin as viteHtmlPlugin } from 'vite-plugin-html';
import { VitePWA } from 'vite-plugin-pwa';
import viteVueDevTools from 'vite-plugin-vue-devtools';
import { viteArchiverPlugin } from './archiver';
import { viteExtraAppConfigPlugin } from './extra-app-config';
import { viteImportMapPlugin } from './importmap';
import { viteInjectAppLoadingPlugin } from './inject-app-loading';
import { viteMetadataPlugin } from './inject-metadata';
import { viteLicensePlugin } from './license';
import { viteNitroMockPlugin } from './nitro-mock';
import { vitePrintPlugin } from './print';
import { viteVxeTableImportsPlugin } from './vxe-table';
/**
* 获取条件成立的 vite 插件
* @param conditionPlugins
*/
async function loadConditionPlugins(conditionPlugins: ConditionPlugin[]) {
const plugins: PluginOption[] = [];
for (const conditionPlugin of conditionPlugins) {
if (conditionPlugin.condition) {
const realPlugins = await conditionPlugin.plugins();
plugins.push(...realPlugins);
}
}
return plugins.flat();
}
/**
* 根据条件获取通用的vite插件
*/
async function loadCommonPlugins(
options: CommonPluginOptions,
): Promise<ConditionPlugin[]> {
const { devtools, injectMetadata, isBuild, visualizer } = options;
return [
{
condition: true,
plugins: () => [
viteVue({
script: {
defineModel: true,
// propsDestructure: true,
},
}),
viteVueJsx(),
],
},
{
condition: !isBuild && devtools,
plugins: () => [viteVueDevTools()],
},
{
condition: injectMetadata,
plugins: async () => [await viteMetadataPlugin()],
},
{
condition: isBuild && !!visualizer,
plugins: () => [<PluginOption>viteVisualizerPlugin({
filename: './node_modules/.cache/visualizer/stats.html',
gzipSize: true,
open: true,
})],
},
];
}
/**
* 根据条件获取应用类型的vite插件
*/
async function loadApplicationPlugins(
options: ApplicationPluginOptions,
): Promise<PluginOption[]> {
// 单独取否则commonOptions拿不到
const isBuild = options.isBuild;
const env = options.env;
const {
archiver,
archiverPluginOptions,
compress,
compressTypes,
extraAppConfig,
html,
i18n,
importmap,
importmapOptions,
injectAppLoading,
license,
nitroMock,
nitroMockOptions,
print,
printInfoMap,
pwa,
pwaOptions,
vxeTableLazyImport,
...commonOptions
} = options;
const commonPlugins = await loadCommonPlugins(commonOptions);
return await loadConditionPlugins([
...commonPlugins,
{
condition: i18n,
plugins: async () => {
return [
viteVueI18nPlugin({
compositionOnly: true,
fullInstall: true,
runtimeOnly: true,
}),
];
},
},
{
condition: print,
plugins: async () => {
return [await vitePrintPlugin({ infoMap: printInfoMap })];
},
},
{
condition: vxeTableLazyImport,
plugins: async () => {
return [await viteVxeTableImportsPlugin()];
},
},
{
condition: nitroMock,
plugins: async () => {
return [await viteNitroMockPlugin(nitroMockOptions)];
},
},
{
condition: injectAppLoading,
plugins: async () => [await viteInjectAppLoadingPlugin(!!isBuild, env)],
},
{
condition: license,
plugins: async () => [await viteLicensePlugin()],
},
{
condition: pwa,
plugins: () =>
VitePWA({
injectRegister: false,
workbox: {
globPatterns: [],
},
...pwaOptions,
manifest: {
display: 'standalone',
start_url: '/',
theme_color: '#ffffff',
...pwaOptions?.manifest,
},
}),
},
{
condition: isBuild && !!compress,
plugins: () => {
const compressPlugins: PluginOption[] = [];
if (compressTypes?.includes('brotli')) {
compressPlugins.push(
viteCompressPlugin({ deleteOriginFile: false, ext: '.br' }),
);
}
if (compressTypes?.includes('gzip')) {
compressPlugins.push(
viteCompressPlugin({ deleteOriginFile: false, ext: '.gz' }),
);
}
return compressPlugins;
},
},
{
condition: !!html,
plugins: () => [viteHtmlPlugin({ minify: true })],
},
{
condition: isBuild && importmap,
plugins: () => {
return [viteImportMapPlugin(importmapOptions)];
},
},
{
condition: isBuild && extraAppConfig,
plugins: async () => [
await viteExtraAppConfigPlugin({ isBuild: true, root: process.cwd() }),
],
},
{
condition: archiver,
plugins: async () => {
return [await viteArchiverPlugin(archiverPluginOptions)];
},
},
]);
}
/**
* 根据条件获取库类型的vite插件
*/
async function loadLibraryPlugins(
options: LibraryPluginOptions,
): Promise<PluginOption[]> {
// 单独取否则commonOptions拿不到
const isBuild = options.isBuild;
const { dts, ...commonOptions } = options;
const commonPlugins = await loadCommonPlugins(commonOptions);
return await loadConditionPlugins([
...commonPlugins,
{
condition: isBuild && !!dts,
plugins: () => [viteDtsPlugin({ logLevel: 'error' })],
},
]);
}
export {
loadApplicationPlugins,
loadLibraryPlugins,
viteArchiverPlugin,
viteCompressPlugin,
viteDtsPlugin,
viteHtmlPlugin,
viteVisualizerPlugin,
viteVxeTableImportsPlugin,
};

View File

@@ -0,0 +1,3 @@
# inject-app-loading
用于在应用加载时显示加载动画的插件,可自行选择加载动画的样式。

View File

@@ -0,0 +1,107 @@
<style data-app-loading="inject-css">
html {
/* same as ant-design-vue/dist/reset.css setting, avoid the title line-height changed */
line-height: 1.15;
}
.dark .loading {
background-color: #0d0d10;
}
.dark .loading .title {
color: rgb(255 255 255 / 85%);
}
.loading {
position: fixed;
top: 0;
left: 0;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
background-color: #f4f7f9;
}
.loading.hidden {
visibility: hidden;
opacity: 0;
transition: all 0.6s ease-out;
}
.loading .title {
margin-top: 36px;
font-size: 30px;
font-weight: 600;
color: rgb(0 0 0 / 85%);
}
.dot {
position: relative;
box-sizing: border-box;
display: inline-block;
width: 48px;
height: 48px;
margin-top: 30px;
font-size: 32px;
transform: rotate(45deg);
animation: rotate-ani 1.2s infinite linear;
}
.dot i {
position: absolute;
display: block;
width: 20px;
height: 20px;
background-color: hsl(var(--primary, 210 100% 50%));
border-radius: 100%;
opacity: 0.3;
transform: scale(0.75);
transform-origin: 50% 50%;
animation: spin-move-ani 1s infinite linear alternate;
}
.dot i:nth-child(1) {
top: 0;
left: 0;
}
.dot i:nth-child(2) {
top: 0;
right: 0;
animation-delay: 0.4s;
}
.dot i:nth-child(3) {
right: 0;
bottom: 0;
animation-delay: 0.8s;
}
.dot i:nth-child(4) {
bottom: 0;
left: 0;
animation-delay: 1.2s;
}
@keyframes rotate-ani {
to {
transform: rotate(405deg);
}
}
@keyframes spin-move-ani {
to {
opacity: 1;
}
}
</style>
<div class="loading" id="__app-loading__">
<span class="dot"><i></i><i></i><i></i><i></i></span>
<div class="title"><%= VITE_APP_TITLE %></div>
</div>

View File

@@ -0,0 +1,113 @@
<style data-app-loading="inject-css">
html {
/* same as ant-design-vue/dist/reset.css setting, avoid the title line-height changed */
line-height: 1.15;
}
.loading {
position: fixed;
top: 0;
left: 0;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow: hidden;
background-color: #f4f7f9;
/* transition: all 0.8s ease-out; */
}
.loading.hidden {
pointer-events: none;
visibility: hidden;
opacity: 0;
transition: all 0.8s ease-out;
}
.dark .loading {
background: #0d0d10;
}
.title {
margin-top: 66px;
font-size: 28px;
font-weight: 600;
color: rgb(0 0 0 / 85%);
}
.dark .title {
color: #fff;
}
.loader {
position: relative;
width: 48px;
height: 48px;
}
.loader::before {
position: absolute;
top: 60px;
left: 0;
width: 48px;
height: 5px;
content: '';
background: hsl(var(--primary, 210 100% 50%) / 50%);
border-radius: 50%;
animation: shadow-ani 0.5s linear infinite;
}
.loader::after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
background: hsl(var(--primary, 210 100% 50%));
border-radius: 4px;
animation: jump-ani 0.5s linear infinite;
}
@keyframes jump-ani {
15% {
border-bottom-right-radius: 3px;
}
25% {
transform: translateY(9px) rotate(22.5deg);
}
50% {
border-bottom-right-radius: 40px;
transform: translateY(18px) scale(1, 0.9) rotate(45deg);
}
75% {
transform: translateY(9px) rotate(67.5deg);
}
100% {
transform: translateY(0) rotate(90deg);
}
}
@keyframes shadow-ani {
0%,
100% {
transform: scale(1, 1);
}
50% {
transform: scale(1.2, 1);
}
}
</style>
<div class="loading" id="__app-loading__">
<div class="loader"></div>
<div class="title"><%= VITE_APP_TITLE %></div>
</div>

View File

@@ -0,0 +1,66 @@
import type { PluginOption } from 'vite';
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readPackageJSON } from '@vben/node-utils';
/**
* 用于生成将loading样式注入到项目中
* 为多app提供loading样式无需在每个 app -> index.html单独引入
*/
async function viteInjectAppLoadingPlugin(
isBuild: boolean,
env: Record<string, any> = {},
loadingTemplate = 'loading.html',
): Promise<PluginOption | undefined> {
const loadingHtml = await getLoadingRawByHtmlTemplate(loadingTemplate);
const { version } = await readPackageJSON(process.cwd());
const envRaw = isBuild ? 'prod' : 'dev';
const cacheName = `'${env.VITE_APP_NAMESPACE}-${version}-${envRaw}-preferences-theme'`;
// 获取缓存的主题
// 保证黑暗主题下刷新页面时loading也是黑暗主题
const injectScript = `
<script data-app-loading="inject-js">
var theme = localStorage.getItem(${cacheName});
document.documentElement.classList.toggle('dark', /dark/.test(theme));
</script>
`;
if (!loadingHtml) {
return;
}
return {
enforce: 'pre',
name: 'vite:inject-app-loading',
transformIndexHtml: {
handler(html) {
const re = /<body\s*>/;
html = html.replace(re, `<body>${injectScript}${loadingHtml}`);
return html;
},
order: 'pre',
},
};
}
/**
* 用于获取loading的html模板
*/
async function getLoadingRawByHtmlTemplate(loadingTemplate: string) {
// 支持在app内自定义loading模板模版参考default-loading.html即可
let appLoadingPath = join(process.cwd(), loadingTemplate);
if (!fs.existsSync(appLoadingPath)) {
const __dirname = fileURLToPath(new URL('.', import.meta.url));
appLoadingPath = join(__dirname, './default-loading.html');
}
return await fsp.readFile(appLoadingPath, 'utf8');
}
export { viteInjectAppLoadingPlugin };

View File

@@ -0,0 +1,111 @@
import type { PluginOption } from 'vite';
import {
dateUtil,
findMonorepoRoot,
getPackages,
readPackageJSON,
} from '@vben/node-utils';
import { readWorkspaceManifest } from '@pnpm/workspace.read-manifest';
function resolvePackageVersion(
pkgsMeta: Record<string, string>,
name: string,
value: string,
catalog: Record<string, string>,
) {
if (value.includes('catalog:')) {
return catalog[name];
}
if (value.includes('workspace')) {
return pkgsMeta[name];
}
return value;
}
async function resolveMonorepoDependencies() {
const { packages } = await getPackages();
const manifest = await readWorkspaceManifest(findMonorepoRoot());
const catalog = manifest?.catalog || {};
const resultDevDependencies: Record<string, string | undefined> = {};
const resultDependencies: Record<string, string | undefined> = {};
const pkgsMeta: Record<string, string> = {};
for (const { packageJson } of packages) {
pkgsMeta[packageJson.name] = packageJson.version;
}
for (const { packageJson } of packages) {
const { dependencies = {}, devDependencies = {} } = packageJson;
for (const [key, value] of Object.entries(dependencies)) {
resultDependencies[key] = resolvePackageVersion(
pkgsMeta,
key,
value,
catalog,
);
}
for (const [key, value] of Object.entries(devDependencies)) {
resultDevDependencies[key] = resolvePackageVersion(
pkgsMeta,
key,
value,
catalog,
);
}
}
return {
dependencies: resultDependencies,
devDependencies: resultDevDependencies,
};
}
/**
* 用于注入项目信息
*/
async function viteMetadataPlugin(
root = process.cwd(),
): Promise<PluginOption | undefined> {
const { author, description, homepage, license, version } =
await readPackageJSON(root);
const buildTime = dateUtil().format('YYYY-MM-DD HH:mm:ss');
return {
async config() {
const { dependencies, devDependencies } =
await resolveMonorepoDependencies();
const isAuthorObject = typeof author === 'object';
const authorName = isAuthorObject ? author.name : author;
const authorEmail = isAuthorObject ? author.email : null;
const authorUrl = isAuthorObject ? author.url : null;
return {
define: {
__VBEN_ADMIN_METADATA__: JSON.stringify({
authorEmail,
authorName,
authorUrl,
buildTime,
dependencies,
description,
devDependencies,
homepage,
license,
version,
}),
'import.meta.env.VITE_APP_VERSION': JSON.stringify(version),
},
};
},
enforce: 'post',
name: 'vite:inject-metadata',
};
}
export { viteMetadataPlugin };

View File

@@ -0,0 +1,63 @@
import type {
NormalizedOutputOptions,
OutputBundle,
OutputChunk,
} from 'rollup';
import type { PluginOption } from 'vite';
import { EOL } from 'node:os';
import { dateUtil, readPackageJSON } from '@vben/node-utils';
/**
* 用于注入版权信息
* @returns
*/
async function viteLicensePlugin(
root = process.cwd(),
): Promise<PluginOption | undefined> {
const {
description = '',
homepage = '',
version = '',
} = await readPackageJSON(root);
return {
apply: 'build',
enforce: 'post',
generateBundle: {
handler: (_options: NormalizedOutputOptions, bundle: OutputBundle) => {
const date = dateUtil().format('YYYY-MM-DD ');
const copyrightText = `/*!
* Vben Admin
* Version: ${version}
* Author: vben
* Copyright (C) 2024 Vben
* License: MIT License
* Description: ${description}
* Date Created: ${date}
* Homepage: ${homepage}
* Contact: ann.vben@gmail.com
*/
`.trim();
for (const [, fileContent] of Object.entries(bundle)) {
if (fileContent.type === 'chunk' && fileContent.isEntry) {
const chunkContent = fileContent as OutputChunk;
// 插入版权信息
const content = chunkContent.code;
const updatedContent = `${copyrightText}${EOL}${content}`;
// 更新bundle
(fileContent as OutputChunk).code = updatedContent;
}
}
},
order: 'post',
},
name: 'vite:license',
};
}
export { viteLicensePlugin };

View File

@@ -0,0 +1,98 @@
import type { PluginOption } from 'vite';
import type { NitroMockPluginOptions } from '../typing';
import { colors, consola, getPackage } from '@vben/node-utils';
import getPort from 'get-port';
import { build, createDevServer, createNitro, prepare } from 'nitropack';
const hmrKeyRe = /^runtimeConfig\.|routeRules\./;
export const viteNitroMockPlugin = ({
mockServerPackage = '@vben/backend-mock',
port = 5320,
verbose = true,
}: NitroMockPluginOptions = {}): PluginOption => {
return {
async configureServer(server) {
const availablePort = await getPort({ port });
if (availablePort !== port) {
return;
}
const pkg = await getPackage(mockServerPackage);
if (!pkg) {
consola.log(
`Package ${mockServerPackage} not found. Skip mock server.`,
);
return;
}
runNitroServer(pkg.dir, port, verbose);
const _printUrls = server.printUrls;
server.printUrls = () => {
_printUrls();
consola.log(
` ${colors.green('➜')} ${colors.bold('Nitro Mock Server')}: ${colors.cyan(`http://localhost:${port}/api`)}`,
);
};
},
enforce: 'pre',
name: 'vite:mock-server',
};
};
async function runNitroServer(rootDir: string, port: number, verbose: boolean) {
let nitro: any;
const reload = async () => {
if (nitro) {
consola.info('Restarting dev server...');
if ('unwatch' in nitro.options._c12) {
await nitro.options._c12.unwatch();
}
await nitro.close();
}
nitro = await createNitro(
{
dev: true,
preset: 'nitro-dev',
rootDir,
},
{
c12: {
async onUpdate({ getDiff, newConfig }) {
const diff = getDiff();
if (diff.length === 0) {
return;
}
verbose &&
consola.info(
`Nitro config updated:\n${diff
.map((entry) => ` ${entry.toString()}`)
.join('\n')}`,
);
await (diff.every((e) => hmrKeyRe.test(e.key))
? nitro.updateConfig(newConfig.config)
: reload());
},
},
watch: true,
},
);
nitro.hooks.hookOnce('restart', reload);
const server = createDevServer(nitro);
await server.listen(port, { showURL: false });
await prepare(nitro);
await build(nitro);
if (verbose) {
console.log('');
consola.success(colors.bold(colors.green('Nitro Mock Server started.')));
}
};
return await reload();
}

View File

@@ -0,0 +1,28 @@
import type { PluginOption } from 'vite';
import type { PrintPluginOptions } from '../typing';
import { colors } from '@vben/node-utils';
export const vitePrintPlugin = (
options: PrintPluginOptions = {},
): PluginOption => {
const { infoMap = {} } = options;
return {
configureServer(server) {
const _printUrls = server.printUrls;
server.printUrls = () => {
_printUrls();
for (const [key, value] of Object.entries(infoMap)) {
console.log(
` ${colors.green('➜')} ${colors.bold(key)}: ${colors.cyan(value)}`,
);
}
};
},
enforce: 'pre',
name: 'vite:print-info',
};
};

View File

@@ -0,0 +1,20 @@
import type { PluginOption } from 'vite';
import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import';
async function viteVxeTableImportsPlugin(): Promise<PluginOption> {
return [
lazyImport({
resolvers: [
VxeResolver({
libraryName: 'vxe-table',
}),
VxeResolver({
libraryName: 'vxe-pc-ui',
}),
],
}),
];
}
export { viteVxeTableImportsPlugin };

View File

@@ -0,0 +1,343 @@
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
import type { ConfigEnv, PluginOption, UserConfig } from 'vite';
import type { PluginOptions } from 'vite-plugin-dts';
import type { Options as PwaPluginOptions } from 'vite-plugin-pwa';
/**
* ImportMap 配置接口
* @description 用于配置模块导入映射,支持自定义导入路径和范围
* @example
* ```typescript
* {
* imports: {
* 'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
* },
* scopes: {
* 'https://site.com/': {
* 'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
* }
* }
* }
* ```
*/
interface IImportMap {
/** 模块导入映射 */
imports?: Record<string, string>;
/** 作用域特定的导入映射 */
scopes?: {
[scope: string]: Record<string, string>;
};
}
/**
* 打印插件配置选项
* @description 用于配置控制台打印信息
*/
interface PrintPluginOptions {
/**
* 打印的数据映射
* @description 键值对形式的数据,将在控制台打印
* @example
* ```typescript
* {
* 'App Version': '1.0.0',
* 'Build Time': '2024-01-01'
* }
* ```
*/
infoMap?: Record<string, string | undefined>;
}
/**
* Nitro Mock 插件配置选项
* @description 用于配置 Nitro Mock 服务器的行为
*/
interface NitroMockPluginOptions {
/**
* Mock 服务器包名
* @default '@vbenjs/nitro-mock'
*/
mockServerPackage?: string;
/**
* Mock 服务端口
* @default 3000
*/
port?: number;
/**
* 是否打印 Mock 日志
* @default false
*/
verbose?: boolean;
}
/**
* 归档插件配置选项
* @description 用于配置构建产物的压缩归档
*/
interface ArchiverPluginOptions {
/**
* 输出文件名
* @default 'dist'
*/
name?: string;
/**
* 输出目录
* @default '.'
*/
outputDir?: string;
}
/**
* ImportMap 插件配置
* @description 用于配置模块的 CDN 导入
*/
interface ImportmapPluginOptions {
/**
* CDN 供应商
* @default 'jspm.io'
* @description 支持 esm.sh 和 jspm.io 两种 CDN 供应商
*/
defaultProvider?: 'esm.sh' | 'jspm.io';
/**
* ImportMap 配置数组
* @description 配置需要从 CDN 导入的包
* @example
* ```typescript
* [
* { name: 'vue' },
* { name: 'pinia', range: '^2.0.0' }
* ]
* ```
*/
importmap?: Array<{ name: string; range?: string }>;
/**
* 手动配置 ImportMap
* @description 自定义 ImportMap 配置
*/
inputMap?: IImportMap;
}
/**
* 条件插件配置
* @description 用于根据条件动态加载插件
*/
interface ConditionPlugin {
/**
* 判断条件
* @description 当条件为 true 时加载插件
*/
condition?: boolean;
/**
* 插件对象
* @description 返回插件数组或 Promise
*/
plugins: () => PluginOption[] | PromiseLike<PluginOption[]>;
}
/**
* 通用插件配置选项
* @description 所有插件共用的基础配置
*/
interface CommonPluginOptions {
/**
* 是否开启开发工具
* @default false
*/
devtools?: boolean;
/**
* 环境变量
* @description 自定义环境变量
*/
env?: Record<string, any>;
/**
* 是否注入元数据
* @default true
*/
injectMetadata?: boolean;
/**
* 是否为构建模式
* @default false
*/
isBuild?: boolean;
/**
* 构建模式
* @default 'development'
*/
mode?: string;
/**
* 是否开启依赖分析
* @default false
* @description 使用 rollup-plugin-visualizer 分析依赖
*/
visualizer?: boolean | PluginVisualizerOptions;
}
/**
* 应用插件配置选项
* @description 用于配置应用构建时的插件选项
*/
interface ApplicationPluginOptions extends CommonPluginOptions {
/**
* 是否开启压缩归档
* @default false
* @description 开启后会在打包目录生成 zip 文件
*/
archiver?: boolean;
/**
* 压缩归档插件配置
* @description 配置压缩归档的行为
*/
archiverPluginOptions?: ArchiverPluginOptions;
/**
* 是否开启压缩
* @default false
* @description 支持 gzip 和 brotli 压缩
*/
compress?: boolean;
/**
* 压缩类型
* @default ['gzip']
* @description 可选的压缩类型
*/
compressTypes?: ('brotli' | 'gzip')[];
/**
* 是否抽离配置文件
* @default false
* @description 在构建时抽离配置文件
*/
extraAppConfig?: boolean;
/**
* 是否开启 HTML 插件
* @default true
*/
html?: boolean;
/**
* 是否开启国际化
* @default false
*/
i18n?: boolean;
/**
* 是否开启 ImportMap CDN
* @default false
*/
importmap?: boolean;
/**
* ImportMap 插件配置
*/
importmapOptions?: ImportmapPluginOptions;
/**
* 是否注入应用加载动画
* @default true
*/
injectAppLoading?: boolean;
/**
* 是否注入全局 SCSS
* @default true
*/
injectGlobalScss?: boolean;
/**
* 是否注入版权信息
* @default true
*/
license?: boolean;
/**
* 是否开启 Nitro Mock
* @default false
*/
nitroMock?: boolean;
/**
* Nitro Mock 插件配置
*/
nitroMockOptions?: NitroMockPluginOptions;
/**
* 是否开启控制台打印
* @default false
*/
print?: boolean;
/**
* 打印插件配置
*/
printInfoMap?: PrintPluginOptions['infoMap'];
/**
* 是否开启 PWA
* @default false
*/
pwa?: boolean;
/**
* PWA 插件配置
*/
pwaOptions?: Partial<PwaPluginOptions>;
/**
* 是否开启 VXE Table 懒加载
* @default false
*/
vxeTableLazyImport?: boolean;
}
/**
* 库插件配置选项
* @description 用于配置库构建时的插件选项
*/
interface LibraryPluginOptions extends CommonPluginOptions {
/**
* 是否开启 DTS 输出
* @default true
* @description 生成 TypeScript 类型声明文件
*/
dts?: boolean | PluginOptions;
}
/**
* 应用配置选项类型
*/
type ApplicationOptions = ApplicationPluginOptions;
/**
* 库配置选项类型
*/
type LibraryOptions = LibraryPluginOptions;
/**
* 应用配置定义函数类型
* @description 用于定义应用构建配置
*/
type DefineApplicationOptions = (config?: ConfigEnv) => Promise<{
/** 应用插件配置 */
application?: ApplicationOptions;
/** Vite 配置 */
vite?: UserConfig;
}>;
/**
* 库配置定义函数类型
* @description 用于定义库构建配置
*/
type DefineLibraryOptions = (config?: ConfigEnv) => Promise<{
/** 库插件配置 */
library?: LibraryOptions;
/** Vite 配置 */
vite?: UserConfig;
}>;
/**
* 配置定义类型
* @description 应用或库的配置定义
*/
type DefineConfig = DefineApplicationOptions | DefineLibraryOptions;
export type {
ApplicationPluginOptions,
ArchiverPluginOptions,
CommonPluginOptions,
ConditionPlugin,
DefineApplicationOptions,
DefineConfig,
DefineLibraryOptions,
IImportMap,
ImportmapPluginOptions,
LibraryPluginOptions,
NitroMockPluginOptions,
PrintPluginOptions,
};

View File

@@ -0,0 +1,109 @@
import type { ApplicationPluginOptions } from '../typing';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { fs } from '@vben/node-utils';
import dotenv from 'dotenv';
const getBoolean = (value: string | undefined) => value === 'true';
const getString = (value: string | undefined, fallback: string) =>
value ?? fallback;
const getNumber = (value: string | undefined, fallback: number) =>
Number(value) || fallback;
/**
* 获取当前环境下生效的配置文件名
*/
function getConfFiles() {
const script = process.env.npm_lifecycle_script as string;
const reg = /--mode ([\d_a-z]+)/;
const result = reg.exec(script);
let mode = 'production';
if (result) {
mode = result[1] as string;
}
return ['.env', '.env.local', `.env.${mode}`, `.env.${mode}.local`];
}
/**
* Get the environment variables starting with the specified prefix
* @param match prefix
* @param confFiles ext
*/
async function loadEnv<T = Record<string, string>>(
match = 'VITE_GLOB_',
confFiles = getConfFiles(),
) {
let envConfig = {};
for (const confFile of confFiles) {
try {
const confFilePath = join(process.cwd(), confFile);
if (existsSync(confFilePath)) {
const envPath = await fs.readFile(confFilePath, {
encoding: 'utf8',
});
const env = dotenv.parse(envPath);
envConfig = { ...envConfig, ...env };
}
} catch (error) {
console.error(`Error while parsing ${confFile}`, error);
}
}
const reg = new RegExp(`^(${match})`);
Object.keys(envConfig).forEach((key) => {
if (!reg.test(key)) {
Reflect.deleteProperty(envConfig, key);
}
});
return envConfig as T;
}
async function loadAndConvertEnv(
match = 'VITE_',
confFiles = getConfFiles(),
): Promise<
{
appTitle: string;
base: string;
port: number;
} & Partial<ApplicationPluginOptions>
> {
const envConfig = await loadEnv(match, confFiles);
const {
VITE_APP_TITLE,
VITE_ARCHIVER,
VITE_BASE,
VITE_COMPRESS,
VITE_DEVTOOLS,
VITE_INJECT_APP_LOADING,
VITE_NITRO_MOCK,
VITE_PORT,
VITE_PWA,
VITE_VISUALIZER,
} = envConfig;
const compressTypes = (VITE_COMPRESS ?? '')
.split(',')
.filter((item) => item === 'brotli' || item === 'gzip');
return {
appTitle: getString(VITE_APP_TITLE, 'Vben Admin'),
archiver: getBoolean(VITE_ARCHIVER),
base: getString(VITE_BASE, '/'),
compress: compressTypes.length > 0,
compressTypes,
devtools: getBoolean(VITE_DEVTOOLS),
injectAppLoading: getBoolean(VITE_INJECT_APP_LOADING),
nitroMock: getBoolean(VITE_NITRO_MOCK),
port: getNumber(VITE_PORT, 5173),
pwa: getBoolean(VITE_PWA),
visualizer: getBoolean(VITE_VISUALIZER),
};
}
export { loadAndConvertEnv, loadEnv };

View File

@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"include": ["src"],
"exclude": ["node_modules"]
}