first commit

This commit is contained in:
2025-09-22 01:39:01 +08:00
commit 8af3499484
1466 changed files with 126409 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
import { execSync } from 'node:child_process';
import { getPackagesSync } from '@vben/node-utils';
const { packages } = getPackagesSync();
const allowedScopes = [
...packages.map((pkg) => pkg.packageJson.name),
'project',
'style',
'lint',
'ci',
'dev',
'deploy',
'other',
];
// precomputed scope
const scopeComplete = execSync('git status --porcelain || true')
.toString()
.trim()
.split('\n')
.find((r) => ~r.indexOf('M src'))
?.replace(/(\/)/g, '%%')
?.match(/src%%((\w|-)*)/)?.[1]
?.replace(/s$/, '');
/**
* @type {import('cz-git').UserConfig}
*/
const userConfig = {
extends: ['@commitlint/config-conventional'],
plugins: ['commitlint-plugin-function-rules'],
prompt: {
/** @use `pnpm commit :f` */
alias: {
b: 'build: bump dependencies',
c: 'chore: update config',
f: 'docs: fix typos',
r: 'docs: update README',
s: 'style: update code format',
},
allowCustomIssuePrefixs: false,
// scopes: [...scopes, 'mock'],
allowEmptyIssuePrefixs: false,
customScopesAlign: scopeComplete ? 'bottom' : 'top',
defaultScope: scopeComplete,
// English
typesAppend: [
{ name: 'workflow: workflow improvements', value: 'workflow' },
{ name: 'types: type definition file changes', value: 'types' },
],
// 中英文对照版
// messages: {
// type: '选择你要提交的类型 :',
// scope: '选择一个提交范围 (可选):',
// customScope: '请输入自定义的提交范围 :',
// subject: '填写简短精炼的变更描述 :\n',
// body: '填写更加详细的变更描述 (可选)。使用 "|" 换行 :\n',
// breaking: '列举非兼容性重大的变更 (可选)。使用 "|" 换行 :\n',
// footerPrefixsSelect: '选择关联issue前缀 (可选):',
// customFooterPrefixs: '输入自定义issue前缀 :',
// footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
// confirmCommit: '是否提交或修改commit ?',
// },
// types: [
// { value: 'feat', name: 'feat: 新增功能' },
// { value: 'fix', name: 'fix: 修复缺陷' },
// { value: 'docs', name: 'docs: 文档变更' },
// { value: 'style', name: 'style: 代码格式' },
// { value: 'refactor', name: 'refactor: 代码重构' },
// { value: 'perf', name: 'perf: 性能优化' },
// { value: 'test', name: 'test: 添加疏漏测试或已有测试改动' },
// { value: 'build', name: 'build: 构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },
// { value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
// { value: 'revert', name: 'revert: 回滚 commit' },
// { value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },
// { value: 'wip', name: 'wip: 正在开发中' },
// { value: 'workflow', name: 'workflow: 工作流程改进' },
// { value: 'types', name: 'types: 类型定义文件修改' },
// ],
// emptyScopesAlias: 'empty: 不填写',
// customScopesAlias: 'custom: 自定义',
},
rules: {
/**
* type[scope]: [function] description
*
* ^^^^^^^^^^^^^^ empty line.
* - Something here
*/
'body-leading-blank': [2, 'always'],
/**
* type[scope]: [function] description
*
* - something here
*
* ^^^^^^^^^^^^^^
*/
'footer-leading-blank': [1, 'always'],
/**
* type[scope]: [function] description
* ^^^^^
*/
'function-rules/scope-enum': [
2, // level: error
'always',
(parsed) => {
if (!parsed.scope || allowedScopes.includes(parsed.scope)) {
return [true];
}
return [false, `scope must be one of ${allowedScopes.join(', ')}`];
},
],
/**
* type[scope]: [function] description [No more than 108 characters]
* ^^^^^
*/
'header-max-length': [2, 'always', 108],
'scope-enum': [0],
'subject-case': [0],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
/**
* type[scope]: [function] description
* ^^^^
*/
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'style',
'docs',
'test',
'refactor',
'build',
'ci',
'chore',
'revert',
'types',
'release',
],
],
},
};
export default userConfig;

View File

@@ -0,0 +1,33 @@
{
"name": "@vben/commitlint-config",
"version": "5.5.4",
"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/lint-configs/commitlint-config"
},
"license": "MIT",
"type": "module",
"files": [
"dist"
],
"main": "./index.mjs",
"module": "./index.mjs",
"exports": {
".": {
"import": "./index.mjs",
"default": "./index.mjs"
}
},
"dependencies": {
"@commitlint/cli": "catalog:",
"@commitlint/config-conventional": "catalog:",
"@vben/node-utils": "workspace:*",
"commitlint-plugin-function-rules": "catalog:",
"cz-git": "catalog:",
"czg": "catalog:"
}
}

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,56 @@
{
"name": "@vben/eslint-config",
"version": "5.0.0",
"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/lint-configs/eslint-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": "./dist/index.d.ts",
"import": "./dist/index.mjs"
}
},
"dependencies": {
"eslint-config-turbo": "catalog:",
"eslint-plugin-command": "catalog:",
"eslint-plugin-import-x": "catalog:"
},
"devDependencies": {
"@eslint/js": "catalog:",
"@types/eslint": "catalog:",
"@typescript-eslint/eslint-plugin": "catalog:",
"@typescript-eslint/parser": "catalog:",
"eslint": "catalog:",
"eslint-plugin-eslint-comments": "catalog:",
"eslint-plugin-jsdoc": "catalog:",
"eslint-plugin-jsonc": "catalog:",
"eslint-plugin-n": "catalog:",
"eslint-plugin-no-only-tests": "catalog:",
"eslint-plugin-perfectionist": "catalog:",
"eslint-plugin-prettier": "catalog:",
"eslint-plugin-regexp": "catalog:",
"eslint-plugin-unicorn": "catalog:",
"eslint-plugin-unused-imports": "catalog:",
"eslint-plugin-vitest": "catalog:",
"eslint-plugin-vue": "catalog:",
"globals": "catalog:",
"jsonc-eslint-parser": "catalog:",
"vue-eslint-parser": "catalog:"
}
}

View File

@@ -0,0 +1,10 @@
import createCommand from 'eslint-plugin-command/config';
export async function command() {
return [
{
// @ts-expect-error - no types
...createCommand(),
},
];
}

View File

@@ -0,0 +1,24 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function comments(): Promise<Linter.Config[]> {
const [pluginComments] = await Promise.all([
// @ts-expect-error - no types
interopDefault(import('eslint-plugin-eslint-comments')),
] as const);
return [
{
plugins: {
'eslint-comments': pluginComments,
},
rules: {
'eslint-comments/no-aggregating-enable': 'error',
'eslint-comments/no-duplicate-disable': 'error',
'eslint-comments/no-unlimited-disable': 'error',
'eslint-comments/no-unused-enable': 'error',
},
},
];
}

View File

@@ -0,0 +1,28 @@
import type { Linter } from 'eslint';
export async function disableds(): Promise<Linter.Config[]> {
return [
{
files: ['**/__tests__/**/*.?([cm])[jt]s?(x)'],
name: 'disables/test',
rules: {
'@typescript-eslint/ban-ts-comment': 'off',
'no-console': 'off',
},
},
{
files: ['**/*.d.ts'],
name: 'disables/dts',
rules: {
'@typescript-eslint/triple-slash-reference': 'off',
},
},
{
files: ['**/*.js', '**/*.mjs', '**/*.cjs'],
name: 'disables/js',
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
},
];
}

View File

@@ -0,0 +1,52 @@
import type { Linter } from 'eslint';
export async function ignores(): Promise<Linter.Config[]> {
return [
{
ignores: [
'**/node_modules',
'**/dist',
'**/dist-*',
'**/*-dist',
'**/.husky',
'**/.nitro',
'**/.output',
'**/Dockerfile',
'**/package-lock.json',
'**/yarn.lock',
'**/pnpm-lock.yaml',
'**/bun.lockb',
'**/output',
'**/coverage',
'**/temp',
'**/.temp',
'**/tmp',
'**/.tmp',
'**/.history',
'**/.turbo',
'**/.nuxt',
'**/.next',
'**/.vercel',
'**/.changeset',
'**/.idea',
'**/.cache',
'**/.output',
'**/.vite-inspect',
'**/CHANGELOG*.md',
'**/*.min.*',
'**/LICENSE*',
'**/__snapshots__',
'**/*.snap',
'**/fixtures/**',
'**/.vitepress/cache/**',
'**/auto-import?(s).d.ts',
'**/components.d.ts',
'**/vite.config.mts.*',
'**/*.sh',
'**/*.ttf',
'**/*.woff',
],
},
];
}

View File

@@ -0,0 +1,25 @@
import type { Linter } from 'eslint';
import * as pluginImport from 'eslint-plugin-import-x';
export async function importPluginConfig(): Promise<Linter.Config[]> {
return [
{
plugins: {
// @ts-expect-error - This is a dynamic import
import: pluginImport,
},
rules: {
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-duplicates': 'error',
'import/no-mutable-exports': 'error',
'import/no-named-default': 'error',
'import/no-self-import': 'error',
'import/no-unresolved': 'off',
'import/no-webpack-loader-syntax': 'error',
},
},
];
}

View File

@@ -0,0 +1,17 @@
export * from './command';
export * from './comments';
export * from './disableds';
export * from './ignores';
export * from './import';
export * from './javascript';
export * from './jsdoc';
export * from './jsonc';
export * from './node';
export * from './perfectionist';
export * from './prettier';
export * from './regexp';
export * from './test';
export * from './turbo';
export * from './typescript';
export * from './unicorn';
export * from './vue';

View File

@@ -0,0 +1,241 @@
import type { Linter } from 'eslint';
import js from '@eslint/js';
import pluginUnusedImports from 'eslint-plugin-unused-imports';
import globals from 'globals';
export async function javascript(): Promise<Linter.Config[]> {
return [
{
languageOptions: {
ecmaVersion: 'latest',
globals: {
...globals.browser,
...globals.es2021,
...globals.node,
document: 'readonly',
navigator: 'readonly',
window: 'readonly',
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
sourceType: 'module',
},
linterOptions: {
reportUnusedDisableDirectives: true,
},
plugins: {
'unused-imports': pluginUnusedImports,
},
rules: {
...js.configs.recommended.rules,
'accessor-pairs': [
'error',
{ enforceForClassMembers: true, setWithoutGet: true },
],
'array-callback-return': 'error',
'block-scoped-var': 'error',
'constructor-super': 'error',
'default-case-last': 'error',
'dot-notation': ['error', { allowKeywords: true }],
eqeqeq: ['error', 'always'],
'keyword-spacing': 'off',
'new-cap': [
'error',
{ capIsNew: false, newIsCap: true, properties: true },
],
'no-alert': 'error',
'no-array-constructor': 'error',
'no-async-promise-executor': 'error',
'no-caller': 'error',
'no-case-declarations': 'error',
'no-class-assign': 'error',
'no-compare-neg-zero': 'error',
'no-cond-assign': ['error', 'always'],
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-const-assign': 'error',
'no-control-regex': 'error',
'no-debugger': 'error',
'no-delete-var': 'error',
'no-dupe-args': 'error',
'no-dupe-class-members': 'error',
'no-dupe-keys': 'error',
'no-duplicate-case': 'error',
'no-empty': ['error', { allowEmptyCatch: true }],
'no-empty-character-class': 'error',
'no-empty-function': 'off',
'no-empty-pattern': 'error',
'no-eval': 'error',
'no-ex-assign': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-extra-boolean-cast': 'error',
'no-fallthrough': 'error',
'no-func-assign': 'error',
'no-global-assign': 'error',
'no-implied-eval': 'error',
'no-import-assign': 'error',
'no-invalid-regexp': 'error',
'no-irregular-whitespace': 'error',
'no-iterator': 'error',
'no-labels': ['error', { allowLoop: false, allowSwitch: false }],
'no-lone-blocks': 'error',
'no-loss-of-precision': 'error',
'no-misleading-character-class': 'error',
'no-multi-str': 'error',
'no-new': 'error',
'no-new-func': 'error',
'no-new-object': 'error',
'no-new-symbol': 'error',
'no-new-wrappers': 'error',
'no-obj-calls': 'error',
'no-octal': 'error',
'no-octal-escape': 'error',
'no-proto': 'error',
'no-prototype-builtins': 'error',
'no-redeclare': ['error', { builtinGlobals: false }],
'no-regex-spaces': 'error',
'no-restricted-globals': [
'error',
{ message: 'Use `globalThis` instead.', name: 'global' },
{ message: 'Use `globalThis` instead.', name: 'self' },
],
'no-restricted-properties': [
'error',
{
message:
'Use `Object.getPrototypeOf` or `Object.setPrototypeOf` instead.',
property: '__proto__',
},
{
message: 'Use `Object.defineProperty` instead.',
property: '__defineGetter__',
},
{
message: 'Use `Object.defineProperty` instead.',
property: '__defineSetter__',
},
{
message: 'Use `Object.getOwnPropertyDescriptor` instead.',
property: '__lookupGetter__',
},
{
message: 'Use `Object.getOwnPropertyDescriptor` instead.',
property: '__lookupSetter__',
},
],
'no-restricted-syntax': [
'error',
'DebuggerStatement',
'LabeledStatement',
'WithStatement',
'TSEnumDeclaration[const=true]',
'TSExportAssignment',
],
'no-self-assign': ['error', { props: true }],
'no-self-compare': 'error',
'no-sequences': 'error',
'no-shadow-restricted-names': 'error',
'no-sparse-arrays': 'error',
'no-template-curly-in-string': 'error',
'no-this-before-super': 'error',
'no-throw-literal': 'error',
'no-undef': 'off',
'no-undef-init': 'error',
'no-unexpected-multiline': 'error',
'no-unmodified-loop-condition': 'error',
'no-unneeded-ternary': ['error', { defaultAssignment: false }],
'no-unreachable': 'error',
'no-unreachable-loop': 'error',
'no-unsafe-finally': 'error',
'no-unsafe-negation': 'error',
'no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTaggedTemplates: true,
allowTernary: true,
},
],
'no-unused-vars': [
'error',
{
args: 'none',
caughtErrors: 'none',
ignoreRestSiblings: true,
vars: 'all',
},
],
'no-use-before-define': [
'error',
{ classes: false, functions: false, variables: false },
],
'no-useless-backreference': 'error',
'no-useless-call': 'error',
'no-useless-catch': 'error',
'no-useless-computed-key': 'error',
'no-useless-constructor': 'error',
'no-useless-rename': 'error',
'no-useless-return': 'error',
'no-var': 'error',
'no-with': 'error',
'object-shorthand': [
'error',
'always',
{ avoidQuotes: true, ignoreConstructors: false },
],
'one-var': ['error', { initialized: 'never' }],
'prefer-arrow-callback': [
'error',
{
allowNamedFunctions: false,
allowUnboundThis: true,
},
],
'prefer-const': [
'error',
{
destructuring: 'all',
ignoreReadBeforeAssign: true,
},
],
'prefer-exponentiation-operator': 'error',
'prefer-promise-reject-errors': 'error',
'prefer-regex-literals': ['error', { disallowRedundantWrapping: true }],
'prefer-rest-params': 'error',
'prefer-spread': 'error',
'prefer-template': 'error',
'space-before-function-paren': 'off',
'spaced-comment': 'error',
'symbol-description': 'error',
'unicode-bom': ['error', 'never'],
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'error',
{
args: 'after-used',
argsIgnorePattern: '^_',
vars: 'all',
varsIgnorePattern: '^_',
},
],
'use-isnan': [
'error',
{ enforceForIndexOf: true, enforceForSwitchCase: true },
],
'valid-typeof': ['error', { requireStringLiterals: true }],
'vars-on-top': 'error',
yoda: ['error', 'never'],
},
},
];
}

View File

@@ -0,0 +1,34 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function jsdoc(): Promise<Linter.Config[]> {
const [pluginJsdoc] = await Promise.all([
interopDefault(import('eslint-plugin-jsdoc')),
] as const);
return [
{
plugins: {
jsdoc: pluginJsdoc,
},
rules: {
'jsdoc/check-access': 'warn',
'jsdoc/check-param-names': 'warn',
'jsdoc/check-property-names': 'warn',
'jsdoc/check-types': 'warn',
'jsdoc/empty-tags': 'warn',
'jsdoc/implements-on-classes': 'warn',
'jsdoc/no-defaults': 'warn',
'jsdoc/no-multi-asterisks': 'warn',
'jsdoc/require-param-name': 'warn',
'jsdoc/require-property': 'warn',
'jsdoc/require-property-description': 'warn',
'jsdoc/require-property-name': 'warn',
'jsdoc/require-returns-check': 'warn',
'jsdoc/require-returns-description': 'warn',
'jsdoc/require-yields-check': 'warn',
},
},
];
}

View File

@@ -0,0 +1,258 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function jsonc(): Promise<Linter.Config[]> {
const [pluginJsonc, parserJsonc] = await Promise.all([
interopDefault(import('eslint-plugin-jsonc')),
interopDefault(import('jsonc-eslint-parser')),
] as const);
return [
{
files: ['**/*.json', '**/*.json5', '**/*.jsonc', '*.code-workspace'],
languageOptions: {
parser: parserJsonc as any,
},
plugins: {
jsonc: pluginJsonc as any,
},
rules: {
'jsonc/no-bigint-literals': 'error',
'jsonc/no-binary-expression': 'error',
'jsonc/no-binary-numeric-literals': 'error',
'jsonc/no-dupe-keys': 'error',
'jsonc/no-escape-sequence-in-identifier': 'error',
'jsonc/no-floating-decimal': 'error',
'jsonc/no-hexadecimal-numeric-literals': 'error',
'jsonc/no-infinity': 'error',
'jsonc/no-multi-str': 'error',
'jsonc/no-nan': 'error',
'jsonc/no-number-props': 'error',
'jsonc/no-numeric-separators': 'error',
'jsonc/no-octal': 'error',
'jsonc/no-octal-escape': 'error',
'jsonc/no-octal-numeric-literals': 'error',
'jsonc/no-parenthesized': 'error',
'jsonc/no-plus-sign': 'error',
'jsonc/no-regexp-literals': 'error',
'jsonc/no-sparse-arrays': 'error',
'jsonc/no-template-literals': 'error',
'jsonc/no-undefined-value': 'error',
'jsonc/no-unicode-codepoint-escapes': 'error',
'jsonc/no-useless-escape': 'error',
'jsonc/space-unary-ops': 'error',
'jsonc/valid-json-number': 'error',
'jsonc/vue-custom-block/no-parsing-error': 'error',
},
},
sortTsconfig(),
sortPackageJson(),
];
}
function sortPackageJson(): Linter.Config {
return {
files: ['**/package.json'],
rules: {
'jsonc/sort-array-values': [
'error',
{
order: { type: 'asc' },
pathPattern: '^files$|^pnpm.neverBuiltDependencies$',
},
],
'jsonc/sort-keys': [
'error',
{
order: [
'name',
'version',
'description',
'private',
'keywords',
'homepage',
'bugs',
'repository',
'license',
'author',
'contributors',
'categories',
'funding',
'type',
'scripts',
'files',
'sideEffects',
'bin',
'main',
'module',
'unpkg',
'jsdelivr',
'types',
'typesVersions',
'imports',
'exports',
'publishConfig',
'icon',
'activationEvents',
'contributes',
'peerDependencies',
'peerDependenciesMeta',
'dependencies',
'optionalDependencies',
'devDependencies',
'engines',
'packageManager',
'pnpm',
'overrides',
'resolutions',
'husky',
'simple-git-hooks',
'lint-staged',
'eslintConfig',
],
pathPattern: '^$',
},
{
order: { type: 'asc' },
pathPattern: '^(?:dev|peer|optional|bundled)?[Dd]ependencies(Meta)?$',
},
{
order: { type: 'asc' },
pathPattern: '^(?:resolutions|overrides|pnpm.overrides)$',
},
{
order: ['types', 'import', 'require', 'default'],
pathPattern: '^exports.*$',
},
],
},
};
}
function sortTsconfig(): Linter.Config {
return {
files: [
'**/tsconfig.json',
'**/tsconfig.*.json',
'internal/tsconfig/*.json',
],
rules: {
'jsonc/sort-keys': [
'error',
{
order: [
'extends',
'compilerOptions',
'references',
'files',
'include',
'exclude',
],
pathPattern: '^$',
},
{
order: [
/* Projects */
'incremental',
'composite',
'tsBuildInfoFile',
'disableSourceOfProjectReferenceRedirect',
'disableSolutionSearching',
'disableReferencedProjectLoad',
/* Language and Environment */
'target',
'jsx',
'jsxFactory',
'jsxFragmentFactory',
'jsxImportSource',
'lib',
'moduleDetection',
'noLib',
'reactNamespace',
'useDefineForClassFields',
'emitDecoratorMetadata',
'experimentalDecorators',
/* Modules */
'baseUrl',
'rootDir',
'rootDirs',
'customConditions',
'module',
'moduleResolution',
'moduleSuffixes',
'noResolve',
'paths',
'resolveJsonModule',
'resolvePackageJsonExports',
'resolvePackageJsonImports',
'typeRoots',
'types',
'allowArbitraryExtensions',
'allowImportingTsExtensions',
'allowUmdGlobalAccess',
/* JavaScript Support */
'allowJs',
'checkJs',
'maxNodeModuleJsDepth',
/* Type Checking */
'strict',
'strictBindCallApply',
'strictFunctionTypes',
'strictNullChecks',
'strictPropertyInitialization',
'allowUnreachableCode',
'allowUnusedLabels',
'alwaysStrict',
'exactOptionalPropertyTypes',
'noFallthroughCasesInSwitch',
'noImplicitAny',
'noImplicitOverride',
'noImplicitReturns',
'noImplicitThis',
'noPropertyAccessFromIndexSignature',
'noUncheckedIndexedAccess',
'noUnusedLocals',
'noUnusedParameters',
'useUnknownInCatchVariables',
/* Emit */
'declaration',
'declarationDir',
'declarationMap',
'downlevelIteration',
'emitBOM',
'emitDeclarationOnly',
'importHelpers',
'importsNotUsedAsValues',
'inlineSourceMap',
'inlineSources',
'mapRoot',
'newLine',
'noEmit',
'noEmitHelpers',
'noEmitOnError',
'outDir',
'outFile',
'preserveConstEnums',
'preserveValueImports',
'removeComments',
'sourceMap',
'sourceRoot',
'stripInternal',
/* Interop Constraints */
'allowSyntheticDefaultImports',
'esModuleInterop',
'forceConsistentCasingInFileNames',
'isolatedModules',
'preserveSymlinks',
'verbatimModuleSyntax',
/* Completeness */
'skipDefaultLibCheck',
'skipLibCheck',
],
pathPattern: '^compilerOptions$',
},
],
},
};
}

View File

@@ -0,0 +1,57 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function node(): Promise<Linter.Config[]> {
const pluginNode = await interopDefault(import('eslint-plugin-n'));
return [
{
plugins: {
n: pluginNode,
},
rules: {
'n/handle-callback-err': ['error', '^(err|error)$'],
'n/no-deprecated-api': 'error',
'n/no-exports-assign': 'error',
'n/no-extraneous-import': [
'error',
{
allowModules: [
'unbuild',
'@vben/vite-config',
'vitest',
'vite',
'@vue/test-utils',
'@vben/tailwind-config',
'@playwright/test',
],
},
],
'n/no-new-require': 'error',
'n/no-path-concat': 'error',
// 'n/no-unpublished-import': 'off',
'n/no-unsupported-features/es-syntax': [
'error',
{
ignores: [],
version: '>=18.0.0',
},
],
'n/prefer-global/buffer': ['error', 'never'],
// 'n/no-missing-import': 'off',
'n/prefer-global/process': ['error', 'never'],
'n/process-exit-as-throw': 'error',
},
},
{
files: [
'scripts/**/*.?([cm])[jt]s?(x)',
'internal/**/*.?([cm])[jt]s?(x)',
],
rules: {
'n/prefer-global/process': 'off',
},
},
];
}

View File

@@ -0,0 +1,89 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function perfectionist(): Promise<Linter.Config[]> {
const perfectionistPlugin = await interopDefault(
// @ts-expect-error - no types
import('eslint-plugin-perfectionist'),
);
return [
perfectionistPlugin.configs['recommended-natural'],
{
rules: {
'perfectionist/sort-exports': [
'error',
{
order: 'asc',
type: 'natural',
},
],
'perfectionist/sort-imports': [
'error',
{
customGroups: {
type: {
'vben-core-type': ['^@vben-core/.+'],
'vben-type': ['^@vben/.+'],
'vue-type': ['^vue$', '^vue-.+', '^@vue/.+'],
},
value: {
vben: ['^@vben/.+'],
'vben-core': ['^@vben-core/.+'],
vue: ['^vue$', '^vue-.+', '^@vue/.+'],
},
},
environment: 'node',
groups: [
['external-type', 'builtin-type', 'type'],
'vue-type',
'vben-type',
'vben-core-type',
['parent-type', 'sibling-type', 'index-type'],
['internal-type'],
'builtin',
'vue',
'vben',
'vben-core',
'external',
'internal',
['parent', 'sibling', 'index'],
'side-effect',
'side-effect-style',
'style',
'object',
'unknown',
],
internalPattern: ['^#/.+'],
newlinesBetween: 'always',
order: 'asc',
type: 'natural',
},
],
'perfectionist/sort-modules': 'off',
'perfectionist/sort-named-exports': [
'error',
{
order: 'asc',
type: 'natural',
},
],
'perfectionist/sort-objects': [
'error',
{
customGroups: {
items: 'items',
list: 'list',
children: 'children',
},
groups: ['unknown', 'items', 'list', 'children'],
ignorePattern: ['children'],
order: 'asc',
type: 'natural',
},
],
},
},
];
}

View File

@@ -0,0 +1,19 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function prettier(): Promise<Linter.Config[]> {
const [pluginPrettier] = await Promise.all([
interopDefault(import('eslint-plugin-prettier')),
] as const);
return [
{
plugins: {
prettier: pluginPrettier,
},
rules: {
'prettier/prettier': 'error',
},
},
];
}

View File

@@ -0,0 +1,20 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function regexp(): Promise<Linter.Config[]> {
const [pluginRegexp] = await Promise.all([
interopDefault(import('eslint-plugin-regexp')),
] as const);
return [
{
plugins: {
regexp: pluginRegexp,
},
rules: {
...pluginRegexp.configs.recommended.rules,
},
},
];
}

View File

@@ -0,0 +1,45 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function test(): Promise<Linter.Config[]> {
const [pluginTest, pluginNoOnlyTests] = await Promise.all([
interopDefault(import('eslint-plugin-vitest')),
// @ts-expect-error - no types
interopDefault(import('eslint-plugin-no-only-tests')),
] as const);
return [
{
files: [
`**/__tests__/**/*.?([cm])[jt]s?(x)`,
`**/*.spec.?([cm])[jt]s?(x)`,
`**/*.test.?([cm])[jt]s?(x)`,
`**/*.bench.?([cm])[jt]s?(x)`,
`**/*.benchmark.?([cm])[jt]s?(x)`,
],
plugins: {
test: {
...pluginTest,
rules: {
...pluginTest.rules,
...pluginNoOnlyTests.rules,
},
},
},
rules: {
'no-console': 'off',
'node/prefer-global/process': 'off',
'test/consistent-test-it': [
'error',
{ fn: 'it', withinDescribe: 'it' },
],
'test/no-identical-title': 'error',
'test/no-import-node-test': 'error',
'test/no-only-tests': 'error',
'test/prefer-hooks-in-order': 'error',
'test/prefer-lowercase-title': 'error',
},
},
];
}

View File

@@ -0,0 +1,18 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function turbo(): Promise<Linter.Config[]> {
const [pluginTurbo] = await Promise.all([
// @ts-expect-error - no types
interopDefault(import('eslint-config-turbo')),
] as const);
return [
{
plugins: {
turbo: pluginTurbo,
},
},
];
}

View File

@@ -0,0 +1,72 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function typescript(): Promise<Linter.Config[]> {
const [pluginTs, parserTs] = await Promise.all([
interopDefault(import('@typescript-eslint/eslint-plugin')),
// @ts-expect-error missing types
interopDefault(import('@typescript-eslint/parser')),
] as const);
return [
{
files: ['**/*.?([cm])[jt]s?(x)'],
languageOptions: {
parser: parserTs,
parserOptions: {
createDefaultProgram: false,
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
extraFileExtensions: ['.vue'],
jsxPragma: 'React',
project: './tsconfig.*.json',
sourceType: 'module',
},
},
plugins: {
'@typescript-eslint': pluginTs,
},
rules: {
...pluginTs.configs['eslint-recommended'].overrides?.[0].rules,
...pluginTs.configs.strict.rules,
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-check': false,
'ts-expect-error': 'allow-with-description',
'ts-ignore': 'allow-with-description',
'ts-nocheck': 'allow-with-description',
},
],
// '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': [
'error',
{
allow: ['arrowFunctions', 'functions', 'methods'],
},
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-var-requires': 'error',
'unused-imports/no-unused-vars': 'off',
},
},
];
}

View File

@@ -0,0 +1,45 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function unicorn(): Promise<Linter.Config[]> {
const [pluginUnicorn] = await Promise.all([
interopDefault(import('eslint-plugin-unicorn')),
] as const);
return [
{
plugins: {
unicorn: pluginUnicorn,
},
rules: {
...pluginUnicorn.configs.recommended.rules,
'unicorn/better-regex': 'off',
'unicorn/consistent-destructuring': 'off',
'unicorn/consistent-function-scoping': 'off',
'unicorn/expiring-todo-comments': 'off',
'unicorn/filename-case': 'off',
'unicorn/import-style': 'off',
'unicorn/no-array-for-each': 'off',
'unicorn/no-null': 'off',
'unicorn/no-useless-undefined': 'off',
'unicorn/prefer-at': 'off',
'unicorn/prefer-dom-node-text-content': 'off',
'unicorn/prefer-export-from': ['error', { ignoreUsedVariables: true }],
'unicorn/prefer-global-this': 'off',
'unicorn/prefer-top-level-await': 'off',
'unicorn/prevent-abbreviations': 'off',
},
},
{
files: [
'scripts/**/*.?([cm])[jt]s?(x)',
'internal/**/*.?([cm])[jt]s?(x)',
],
rules: {
'unicorn/no-process-exit': 'off',
},
},
];
}

View File

@@ -0,0 +1,149 @@
import type { Linter } from 'eslint';
import { interopDefault } from '../util';
export async function vue(): Promise<Linter.Config[]> {
const [pluginVue, parserVue, parserTs] = await Promise.all([
interopDefault(import('eslint-plugin-vue')),
interopDefault(import('vue-eslint-parser')),
// @ts-expect-error missing types
interopDefault(import('@typescript-eslint/parser')),
] as const);
return [
{
files: ['**/*.vue'],
languageOptions: {
// globals: {
// computed: 'readonly',
// defineEmits: 'readonly',
// defineExpose: 'readonly',
// defineProps: 'readonly',
// onMounted: 'readonly',
// onUnmounted: 'readonly',
// reactive: 'readonly',
// ref: 'readonly',
// shallowReactive: 'readonly',
// shallowRef: 'readonly',
// toRef: 'readonly',
// toRefs: 'readonly',
// watch: 'readonly',
// watchEffect: 'readonly',
// },
parser: parserVue,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
extraFileExtensions: ['.vue'],
parser: parserTs,
sourceType: 'module',
},
},
plugins: {
vue: pluginVue,
},
processor: pluginVue.processors['.vue'],
rules: {
...pluginVue.configs.base.rules,
...pluginVue.configs['vue3-essential'].rules,
...pluginVue.configs['vue3-strongly-recommended'].rules,
...pluginVue.configs['vue3-recommended'].rules,
'vue/attribute-hyphenation': [
'error',
'always',
{
ignore: [],
},
],
'vue/attributes-order': 'off',
'vue/block-order': [
'error',
{
order: ['script', 'template', 'style'],
},
],
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
'vue/component-options-name-casing': ['error', 'PascalCase'],
'vue/custom-event-name-casing': ['error', 'camelCase'],
'vue/define-macros-order': [
'error',
{
order: [
'defineOptions',
'defineProps',
'defineEmits',
'defineSlots',
],
},
],
'vue/dot-location': ['error', 'property'],
'vue/dot-notation': ['error', { allowKeywords: true }],
'vue/eqeqeq': ['error', 'smart'],
'vue/html-closing-bracket-newline': 'error',
'vue/html-indent': 'off',
// 'vue/html-indent': ['error', 2],
'vue/html-quotes': ['error', 'double'],
'vue/html-self-closing': [
'error',
{
html: {
component: 'always',
normal: 'never',
void: 'always',
},
math: 'always',
svg: 'always',
},
],
'vue/max-attributes-per-line': 'off',
'vue/multi-word-component-names': 'off',
'vue/multiline-html-element-content-newline': 'error',
'vue/no-empty-pattern': 'error',
'vue/no-extra-parens': ['error', 'functions'],
'vue/no-irregular-whitespace': 'error',
'vue/no-loss-of-precision': 'error',
'vue/no-reserved-component-names': 'off',
'vue/no-restricted-syntax': [
'error',
'DebuggerStatement',
'LabeledStatement',
'WithStatement',
],
'vue/no-restricted-v-bind': ['error', '/^v-/'],
'vue/no-sparse-arrays': 'error',
'vue/no-unused-refs': 'error',
'vue/no-useless-v-bind': 'error',
'vue/object-shorthand': [
'error',
'always',
{
avoidQuotes: true,
ignoreConstructors: false,
},
],
'vue/one-component-per-file': 'error',
'vue/prefer-import-from-vue': 'error',
'vue/prefer-separate-static-class': 'error',
'vue/prefer-template': 'error',
'vue/prop-name-casing': ['error', 'camelCase'],
'vue/require-default-prop': 'error',
'vue/require-explicit-emits': 'error',
'vue/require-prop-types': 'off',
'vue/script-setup-uses-vars': 'error',
'vue/singleline-html-element-content-newline': 'off',
'vue/space-infix-ops': 'error',
'vue/space-unary-ops': ['error', { nonwords: false, words: true }],
'vue/v-on-event-hyphenation': [
'error',
'always',
{
autofix: true,
ignore: [],
},
],
},
},
];
}

View File

@@ -0,0 +1,161 @@
import type { Linter } from 'eslint';
const restrictedImportIgnores = [
'**/vite.config.mts',
'**/tailwind.config.mjs',
'**/postcss.config.mjs',
];
const customConfig: Linter.Config[] = [
// shadcn-ui 内部组件是自动生成的,不做太多限制
{
files: ['packages/@core/ui-kit/shadcn-ui/**/**'],
rules: {
'vue/require-default-prop': 'off',
},
},
{
files: [
'apps/**/**',
'packages/effects/**/**',
'packages/utils/**/**',
'packages/types/**/**',
'packages/locales/**/**',
],
ignores: restrictedImportIgnores,
rules: {
'perfectionist/sort-interfaces': 'off',
'perfectionist/sort-objects': 'off',
},
},
{
// apps内部的一些基础规则
files: ['apps/**/**'],
ignores: restrictedImportIgnores,
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['#/api/*'],
message:
'The #/api package cannot be imported, please use the @core package itself',
},
{
group: ['#/layouts/*'],
message:
'The #/layouts package cannot be imported, please use the @core package itself',
},
{
group: ['#/locales/*'],
message:
'The #/locales package cannot be imported, please use the @core package itself',
},
{
group: ['#/stores/*'],
message:
'The #/stores package cannot be imported, please use the @core package itself',
},
],
},
],
'perfectionist/sort-interfaces': 'off',
},
},
{
// @core内部组件不能引入@vben/* 里面的包
files: ['packages/@core/**/**'],
ignores: restrictedImportIgnores,
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@vben/*'],
message:
'The @core package cannot import the @vben package, please use the @core package itself',
},
],
},
],
},
},
{
// @core/shared内部组件不能引入@vben/* 或者 @vben-core/* 里面的包
files: ['packages/@core/base/**/**'],
ignores: restrictedImportIgnores,
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@vben/*', '@vben-core/*'],
message:
'The @vben-core/shared package cannot import the @vben package, please use the @core/shared package itself',
},
],
},
],
},
},
{
// 不能引入@vben/*里面的包
files: [
'packages/types/**/**',
'packages/utils/**/**',
'packages/icons/**/**',
'packages/constants/**/**',
'packages/styles/**/**',
'packages/stores/**/**',
'packages/preferences/**/**',
'packages/locales/**/**',
],
ignores: restrictedImportIgnores,
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@vben/*'],
message:
'The @vben package cannot be imported, please use the @core package itself',
},
],
},
],
},
},
// 后端模拟代码,不需要太多规则
{
files: ['apps/backend-mock/**/**', 'docs/**/**'],
rules: {
'@typescript-eslint/no-extraneous-class': 'off',
'n/no-extraneous-import': 'off',
'n/prefer-global/buffer': 'off',
'n/prefer-global/process': 'off',
'no-console': 'off',
'unicorn/prefer-module': 'off',
},
},
{
files: ['**/**/playwright.config.ts'],
rules: {
'n/prefer-global/buffer': 'off',
'n/prefer-global/process': 'off',
'no-console': 'off',
},
},
{
files: ['internal/**/**', 'scripts/**/**'],
rules: {
'no-console': 'off',
},
},
];
export { customConfig };

View File

@@ -0,0 +1,60 @@
import type { Linter } from 'eslint';
import {
command,
comments,
disableds,
ignores,
importPluginConfig,
javascript,
jsdoc,
jsonc,
node,
perfectionist,
prettier,
regexp,
test,
turbo,
typescript,
unicorn,
vue,
} from './configs';
import { customConfig } from './custom-config';
type FlatConfig = Linter.Config;
type FlatConfigPromise =
| FlatConfig
| FlatConfig[]
| Promise<FlatConfig>
| Promise<FlatConfig[]>;
async function defineConfig(config: FlatConfig[] = []) {
const configs: FlatConfigPromise[] = [
vue(),
javascript(),
ignores(),
prettier(),
typescript(),
jsonc(),
disableds(),
importPluginConfig(),
node(),
perfectionist(),
comments(),
jsdoc(),
unicorn(),
test(),
regexp(),
command(),
turbo(),
...customConfig,
...config,
];
const resolved = await Promise.all(configs);
return resolved.flat();
}
export { defineConfig };

View File

@@ -0,0 +1,8 @@
export type Awaitable<T> = Promise<T> | T;
export async function interopDefault<T>(
m: Awaitable<T>,
): Promise<T extends { default: infer U } ? U : T> {
const resolved = await m;
return (resolved as any).default || resolved;
}

View File

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

View File

@@ -0,0 +1,18 @@
export default {
endOfLine: 'auto',
overrides: [
{
files: ['*.json5'],
options: {
quoteProps: 'preserve',
singleQuote: false,
},
},
],
plugins: ['prettier-plugin-tailwindcss'],
printWidth: 80,
proseWrap: 'never',
semi: true,
singleQuote: true,
trailingComma: 'all',
};

View File

@@ -0,0 +1,28 @@
{
"name": "@vben/prettier-config",
"version": "5.0.0",
"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/lint-configs/prettier-config"
},
"license": "MIT",
"type": "module",
"files": [
"dist"
],
"main": "./index.mjs",
"module": "./index.mjs",
"exports": {
".": {
"default": "./index.mjs"
}
},
"dependencies": {
"prettier": "catalog:",
"prettier-plugin-tailwindcss": "catalog:"
}
}

View File

@@ -0,0 +1,140 @@
export default {
extends: ['stylelint-config-standard', 'stylelint-config-recess-order'],
ignoreFiles: [
'**/*.js',
'**/*.jsx',
'**/*.tsx',
'**/*.ts',
'**/*.json',
'**/*.md',
],
overrides: [
{
customSyntax: 'postcss-html',
files: ['*.(html|vue)', '**/*.(html|vue)'],
rules: {
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global', 'deep'],
},
],
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'],
},
],
},
},
{
customSyntax: 'postcss-scss',
extends: [
'stylelint-config-recommended-scss',
'stylelint-config-recommended-vue/scss',
],
files: ['*.scss', '**/*.scss'],
},
],
plugins: [
'stylelint-order',
'@stylistic/stylelint-plugin',
'stylelint-prettier',
'stylelint-scss',
],
rules: {
'at-rule-no-unknown': [
true,
{
ignoreAtRules: [
'extends',
'ignores',
'include',
'mixin',
'if',
'else',
'media',
'for',
'at-root',
'tailwind',
'apply',
'variants',
'responsive',
'screen',
'function',
'each',
'use',
'forward',
'return',
],
},
],
'font-family-no-missing-generic-family-keyword': null,
'function-no-unknown': null,
'import-notation': null,
'media-feature-range-notation': null,
'named-grid-areas-no-invalid': null,
'no-descending-specificity': null,
'no-empty-source': null,
'order/order': [
[
'dollar-variables',
'custom-properties',
'at-rules',
'declarations',
{
name: 'supports',
type: 'at-rule',
},
{
name: 'media',
type: 'at-rule',
},
{
name: 'include',
type: 'at-rule',
},
'rules',
],
{ severity: 'error' },
],
'prettier/prettier': true,
'rule-empty-line-before': [
'always',
{
ignore: ['after-comment', 'first-nested'],
},
],
'scss/at-rule-no-unknown': [
true,
{
ignoreAtRules: [
'extends',
'ignores',
'include',
'mixin',
'if',
'else',
'media',
'for',
'at-root',
'tailwind',
'apply',
'variants',
'responsive',
'screen',
'function',
'each',
'use',
'forward',
'return',
],
},
],
'scss/operator-no-newline-after': null,
'selector-class-pattern':
'^(?:(?:o|c|u|t|s|is|has|_|js|qa)-)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*(?:__[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:--[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:[.+])?$',
'selector-not-notation': null,
},
};

View File

@@ -0,0 +1,43 @@
{
"name": "@vben/stylelint-config",
"version": "5.5.4",
"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/lint-configs/stylelint-config"
},
"license": "MIT",
"type": "module",
"files": [
"dist"
],
"main": "./index.mjs",
"module": "./index.mjs",
"exports": {
".": {
"import": "./index.mjs",
"default": "./index.mjs"
}
},
"dependencies": {
"@stylistic/stylelint-plugin": "catalog:",
"stylelint-config-recess-order": "catalog:",
"stylelint-scss": "catalog:"
},
"devDependencies": {
"postcss": "catalog:",
"postcss-html": "catalog:",
"postcss-scss": "catalog:",
"prettier": "catalog:",
"stylelint": "catalog:",
"stylelint-config-recommended": "catalog:",
"stylelint-config-recommended-scss": "catalog:",
"stylelint-config-recommended-vue": "catalog:",
"stylelint-config-standard": "catalog:",
"stylelint-order": "catalog:",
"stylelint-prettier": "catalog:"
}
}

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,43 @@
{
"name": "@vben/node-utils",
"version": "5.5.4",
"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/node-utils"
},
"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",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
}
},
"dependencies": {
"@changesets/git": "catalog:",
"@manypkg/get-packages": "catalog:",
"chalk": "catalog:",
"consola": "catalog:",
"dayjs": "catalog:",
"execa": "catalog:",
"find-up": "catalog:",
"ora": "catalog:",
"pkg-types": "catalog:",
"prettier": "catalog:",
"rimraf": "catalog:"
}
}

View File

@@ -0,0 +1,52 @@
import { createHash } from 'node:crypto';
import { describe, expect, it } from 'vitest';
import { generatorContentHash } from '../hash';
describe('generatorContentHash', () => {
it('should generate an MD5 hash for the content', () => {
const content = 'example content';
const expectedHash = createHash('md5')
.update(content, 'utf8')
.digest('hex');
const actualHash = generatorContentHash(content);
expect(actualHash).toBe(expectedHash);
});
it('should generate an MD5 hash with specified length', () => {
const content = 'example content';
const hashLength = 10;
const generatedHash = generatorContentHash(content, hashLength);
expect(generatedHash).toHaveLength(hashLength);
});
it('should correctly generate the hash with specified length', () => {
const content = 'example content';
const hashLength = 8;
const expectedHash = createHash('md5')
.update(content, 'utf8')
.digest('hex')
.slice(0, hashLength);
const generatedHash = generatorContentHash(content, hashLength);
expect(generatedHash).toBe(expectedHash);
});
it('should return full hash if hash length parameter is not provided', () => {
const content = 'example content';
const expectedHash = createHash('md5')
.update(content, 'utf8')
.digest('hex');
const actualHash = generatorContentHash(content);
expect(actualHash).toBe(expectedHash);
});
it('should handle empty content', () => {
const content = '';
const expectedHash = createHash('md5')
.update(content, 'utf8')
.digest('hex');
const actualHash = generatorContentHash(content);
expect(actualHash).toBe(expectedHash);
});
});

View File

@@ -0,0 +1,67 @@
// pathUtils.test.ts
import { describe, expect, it } from 'vitest';
import { toPosixPath } from '../path';
describe('toPosixPath', () => {
// 测试 Windows 风格路径到 POSIX 风格路径的转换
it('converts Windows-style paths to POSIX paths', () => {
const windowsPath = String.raw`C:\Users\Example\file.txt`;
const expectedPosixPath = 'C:/Users/Example/file.txt';
expect(toPosixPath(windowsPath)).toBe(expectedPosixPath);
});
// 确认 POSIX 风格路径不会被改变
it('leaves POSIX-style paths unchanged', () => {
const posixPath = '/home/user/file.txt';
expect(toPosixPath(posixPath)).toBe(posixPath);
});
// 测试带有多个分隔符的路径
it('converts paths with mixed separators', () => {
const mixedPath = String.raw`C:/Users\Example\file.txt`;
const expectedPosixPath = 'C:/Users/Example/file.txt';
expect(toPosixPath(mixedPath)).toBe(expectedPosixPath);
});
// 测试空字符串
it('handles empty strings', () => {
const emptyPath = '';
expect(toPosixPath(emptyPath)).toBe('');
});
// 测试仅包含分隔符的路径
it('handles path with only separators', () => {
const separatorsPath = '\\\\\\';
const expectedPosixPath = '///';
expect(toPosixPath(separatorsPath)).toBe(expectedPosixPath);
});
// 测试不包含任何分隔符的路径
it('handles path without separators', () => {
const noSeparatorPath = 'file.txt';
expect(toPosixPath(noSeparatorPath)).toBe('file.txt');
});
// 测试以分隔符结尾的路径
it('handles path ending with a separator', () => {
const endingSeparatorPath = 'C:\\Users\\Example\\';
const expectedPosixPath = 'C:/Users/Example/';
expect(toPosixPath(endingSeparatorPath)).toBe(expectedPosixPath);
});
// 测试以分隔符开头的路径
it('handles path starting with a separator', () => {
const startingSeparatorPath = String.raw`\Users\Example`;
const expectedPosixPath = '/Users/Example';
expect(toPosixPath(startingSeparatorPath)).toBe(expectedPosixPath);
});
// 测试包含非法字符的路径
it('handles path with invalid characters', () => {
const invalidCharsPath = String.raw`C:\Us*?ers\Ex<ample>|file.txt`;
const expectedPosixPath = 'C:/Us*?ers/Ex<ample>|file.txt';
expect(toPosixPath(invalidCharsPath)).toBe(expectedPosixPath);
});
});

View File

@@ -0,0 +1,6 @@
enum UNICODE {
FAILURE = '\u2716', // ✖
SUCCESS = '\u2714', // ✔
}
export { UNICODE };

View File

@@ -0,0 +1,12 @@
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault('Asia/Shanghai');
const dateUtil = dayjs;
export { dateUtil };

View File

@@ -0,0 +1,39 @@
import { promises as fs } from 'node:fs';
import { dirname } from 'node:path';
export async function outputJSON(
filePath: string,
data: any,
spaces: number = 2,
) {
try {
const dir = dirname(filePath);
await fs.mkdir(dir, { recursive: true });
const jsonData = JSON.stringify(data, null, spaces);
await fs.writeFile(filePath, jsonData, 'utf8');
} catch (error) {
console.error('Error writing JSON file:', error);
throw error;
}
}
export async function ensureFile(filePath: string) {
try {
const dir = dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, '', { flag: 'a' });
} catch (error) {
console.error('Error ensuring file:', error);
throw error;
}
}
export async function readJSON(filePath: string) {
try {
const data = await fs.readFile(filePath, 'utf8');
return JSON.parse(data);
} catch (error) {
console.error('Error reading JSON file:', error);
throw error;
}
}

View File

@@ -0,0 +1,34 @@
import path from 'node:path';
import { execa } from 'execa';
export * from '@changesets/git';
/**
* 获取暂存区文件
*/
async function getStagedFiles(): Promise<string[]> {
try {
const { stdout } = await execa('git', [
'-c',
'submodule.recurse=false',
'diff',
'--staged',
'--diff-filter=ACMR',
'--name-only',
'--ignore-submodules',
'-z',
]);
let changedList = stdout ? stdout.replace(/\0$/, '').split('\0') : [];
changedList = changedList.map((item) => path.resolve(process.cwd(), item));
const changedSet = new Set(changedList);
changedSet.delete('');
return [...changedSet];
} catch (error) {
console.error('Failed to get staged files:', error);
return [];
}
}
export { getStagedFiles };

View File

@@ -0,0 +1,18 @@
import { createHash } from 'node:crypto';
/**
* 生产基于内容的 hash可自定义长度
* @param content
* @param hashLSize
*/
function generatorContentHash(content: string, hashLSize?: number) {
const hash = createHash('md5').update(content, 'utf8').digest('hex');
if (hashLSize) {
return hash.slice(0, hashLSize);
}
return hash;
}
export { generatorContentHash };

View File

@@ -0,0 +1,19 @@
export * from './constants';
export * from './date';
export * from './fs';
export * from './git';
export { getStagedFiles, add as gitAdd } from './git';
export { generatorContentHash } from './hash';
export * from './monorepo';
export { toPosixPath } from './path';
export { prettierFormat } from './prettier';
export * from './spinner';
export type { Package } from '@manypkg/get-packages';
export { default as colors } from 'chalk';
export { consola } from 'consola';
export * from 'execa';
export { default as fs } from 'node:fs/promises';
export { type PackageJson, readPackageJSON } from 'pkg-types';
export { rimraf } from 'rimraf';

View File

@@ -0,0 +1,46 @@
import { dirname } from 'node:path';
import {
getPackages as getPackagesFunc,
getPackagesSync as getPackagesSyncFunc,
} from '@manypkg/get-packages';
import { findUpSync } from 'find-up';
/**
* 查找大仓的根目录
* @param cwd
*/
function findMonorepoRoot(cwd: string = process.cwd()) {
const lockFile = findUpSync('pnpm-lock.yaml', {
cwd,
type: 'file',
});
return dirname(lockFile || '');
}
/**
* 获取大仓的所有包
*/
function getPackagesSync() {
const root = findMonorepoRoot();
return getPackagesSyncFunc(root);
}
/**
* 获取大仓的所有包
*/
async function getPackages() {
const root = findMonorepoRoot();
return await getPackagesFunc(root);
}
/**
* 获取大仓指定的包
*/
async function getPackage(pkgName: string) {
const { packages } = await getPackages();
return packages.find((pkg) => pkg.packageJson.name === pkgName);
}
export { findMonorepoRoot, getPackage, getPackages, getPackagesSync };

View File

@@ -0,0 +1,11 @@
import { posix } from 'node:path';
/**
* 将给定的文件路径转换为 POSIX 风格。
* @param {string} pathname - 原始文件路径。
*/
function toPosixPath(pathname: string) {
return pathname.split(`\\`).join(posix.sep);
}
export { toPosixPath };

View File

@@ -0,0 +1,21 @@
import fs from 'node:fs/promises';
import { format, getFileInfo, resolveConfig } from 'prettier';
async function prettierFormat(filepath: string) {
const prettierOptions = await resolveConfig(filepath, {});
const fileInfo = await getFileInfo(filepath);
const input = await fs.readFile(filepath, 'utf8');
const output = await format(input, {
...prettierOptions,
parser: fileInfo.inferredParser as any,
});
if (output !== input) {
await fs.writeFile(filepath, output, 'utf8');
}
return output;
}
export { prettierFormat };

View File

@@ -0,0 +1,26 @@
import type { Ora } from 'ora';
import ora from 'ora';
interface SpinnerOptions {
failedText?: string;
successText?: string;
title: string;
}
export async function spinner<T>(
{ failedText, successText, title }: SpinnerOptions,
callback: () => Promise<T>,
): Promise<T> {
const loading: Ora = ora(title).start();
try {
const result = await callback();
loading.succeed(successText || 'Success!');
return result;
} catch (error) {
loading.fail(failedText || 'Failed!');
throw error;
} finally {
loading.stop();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,66 @@
{
"name": "@vben/tailwind-config",
"version": "5.5.4",
"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/tailwind-config"
},
"license": "MIT",
"type": "module",
"scripts": {
"stub": "pnpm unbuild"
},
"files": [
"dist"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"typesVersions": {
"*": {
"*": [
"./dist/*",
"./*"
]
}
},
"exports": {
".": {
"types": "./src/index.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./postcss": {
"types": "./src/postcss.config.ts",
"import": "./dist/postcss.config.mjs",
"require": "./dist/postcss.config.cjs",
"default": "./dist/postcss.config.mjs"
},
"./*": "./*"
},
"peerDependencies": {
"tailwindcss": "^3.4.3"
},
"dependencies": {
"@iconify/json": "catalog:",
"@iconify/tailwind": "catalog:",
"@manypkg/get-packages": "catalog:",
"@tailwindcss/nesting": "catalog:",
"@tailwindcss/typography": "catalog:",
"autoprefixer": "catalog:",
"cssnano": "catalog:",
"postcss": "catalog:",
"postcss-antd-fixes": "catalog:",
"postcss-import": "catalog:",
"postcss-preset-env": "catalog:",
"tailwindcss": "catalog:",
"tailwindcss-animate": "catalog:"
},
"devDependencies": {
"@types/postcss-import": "catalog:"
}
}

View File

@@ -0,0 +1,266 @@
import type { Config } from 'tailwindcss';
import path from 'node:path';
import { addDynamicIconSelectors } from '@iconify/tailwind';
import { getPackagesSync } from '@manypkg/get-packages';
import typographyPlugin from '@tailwindcss/typography';
import animate from 'tailwindcss-animate';
import { enterAnimationPlugin } from './plugins/entry';
// import defaultTheme from 'tailwindcss/defaultTheme';
const { packages } = getPackagesSync(process.cwd());
const tailwindPackages: string[] = [];
packages.forEach((pkg) => {
// apps目录下和 @vben-core/tailwind-ui 包需要使用到 tailwindcss ui
// if (fs.existsSync(path.join(pkg.dir, 'tailwind.config.mjs'))) {
tailwindPackages.push(pkg.dir);
// }
});
const shadcnUiColors = {
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
hover: 'hsl(var(--accent-hover))',
lighter: 'has(val(--accent-lighter))',
},
background: {
deep: 'hsl(var(--background-deep))',
DEFAULT: 'hsl(var(--background))',
},
border: {
DEFAULT: 'hsl(var(--border))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
destructive: {
...createColorsPalette('destructive'),
DEFAULT: 'hsl(var(--destructive))',
},
foreground: {
DEFAULT: 'hsl(var(--foreground))',
},
input: {
background: 'hsl(var(--input-background))',
DEFAULT: 'hsl(var(--input))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
primary: {
...createColorsPalette('primary'),
DEFAULT: 'hsl(var(--primary))',
},
ring: 'hsl(var(--ring))',
secondary: {
DEFAULT: 'hsl(var(--secondary))',
desc: 'hsl(var(--secondary-desc))',
foreground: 'hsl(var(--secondary-foreground))',
},
};
const customColors = {
green: {
...createColorsPalette('green'),
foreground: 'hsl(var(--success-foreground))',
},
header: {
DEFAULT: 'hsl(var(--header))',
},
heavy: {
DEFAULT: 'hsl(var(--heavy))',
foreground: 'hsl(var(--heavy-foreground))',
},
main: {
DEFAULT: 'hsl(var(--main))',
},
overlay: {
content: 'hsl(var(--overlay-content))',
DEFAULT: 'hsl(var(--overlay))',
},
red: {
...createColorsPalette('red'),
foreground: 'hsl(var(--destructive-foreground))',
},
sidebar: {
deep: 'hsl(var(--sidebar-deep))',
DEFAULT: 'hsl(var(--sidebar))',
},
success: {
...createColorsPalette('success'),
DEFAULT: 'hsl(var(--success))',
},
warning: {
...createColorsPalette('warning'),
DEFAULT: 'hsl(var(--warning))',
},
yellow: {
...createColorsPalette('yellow'),
foreground: 'hsl(var(--warning-foreground))',
},
};
export default {
content: [
'./index.html',
...tailwindPackages.map((item) =>
path.join(item, 'src/**/*.{vue,js,ts,jsx,tsx,svelte,astro,html}'),
),
],
darkMode: 'selector',
plugins: [
animate,
typographyPlugin,
addDynamicIconSelectors(),
enterAnimationPlugin,
],
prefix: '',
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'collapsible-down': 'collapsible-down 0.2s ease-in-out',
'collapsible-up': 'collapsible-up 0.2s ease-in-out',
float: 'float 5s linear 0ms infinite',
},
animationDuration: {
'2000': '2000ms',
'3000': '3000ms',
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
xl: 'calc(var(--radius) + 4px)',
},
boxShadow: {
float: `0 6px 16px 0 rgb(0 0 0 / 8%),
0 3px 6px -4px rgb(0 0 0 / 12%),
0 9px 28px 8px rgb(0 0 0 / 5%)`,
},
colors: {
...customColors,
...shadcnUiColors,
},
fontFamily: {
sans: [
'var(--font-family)',
// ...defaultTheme.fontFamily.sans
],
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
'collapsible-down': {
from: { height: '0' },
to: { height: 'var(--radix-collapsible-content-height)' },
},
'collapsible-up': {
from: { height: 'var(--radix-collapsible-content-height)' },
to: { height: '0' },
},
float: {
'0%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-20px)' },
'100%': { transform: 'translateY(0)' },
},
},
zIndex: {
'100': '100',
'1000': '1000',
},
},
},
safelist: ['dark'],
} as Config;
function createColorsPalette(name: string) {
// backgroundLightest: '#EFF6FF', // Tailwind CSS 默认的 `blue-50`
// backgroundLighter: '#DBEAFE', // Tailwind CSS 默认的 `blue-100`
// backgroundLight: '#BFDBFE', // Tailwind CSS 默认的 `blue-200`
// borderLight: '#93C5FD', // Tailwind CSS 默认的 `blue-300`
// border: '#60A5FA', // Tailwind CSS 默认的 `blue-400`
// main: '#3B82F6', // Tailwind CSS 默认的 `blue-500`
// hover: '#2563EB', // Tailwind CSS 默认的 `blue-600`
// active: '#1D4ED8', // Tailwind CSS 默认的 `blue-700`
// backgroundDark: '#1E40AF', // Tailwind CSS 默认的 `blue-800`
// backgroundDarker: '#1E3A8A', // Tailwind CSS 默认的 `blue-900`
// backgroundDarkest: '#172554', // Tailwind CSS 默认的 `blue-950`
// • backgroundLightest (#EFF6FF): 适用于最浅的背景色,可能用于非常轻微的阴影或卡片的背景。
// • backgroundLighter (#DBEAFE): 适用于略浅的背景色,通常用于次要背景或略浅的区域。
// • backgroundLight (#BFDBFE): 适用于浅色背景,可能用于输入框或表单区域的背景。
// • borderLight (#93C5FD): 适用于浅色边框,可能用于输入框或卡片的边框。
// • border (#60A5FA): 适用于普通边框,可能用于按钮或卡片的边框。
// • main (#3B82F6): 适用于主要的主题色,通常用于按钮、链接或主要的强调色。
// • hover (#2563EB): 适用于鼠标悬停状态下的颜色,例如按钮悬停时的背景色或边框色。
// • active (#1D4ED8): 适用于激活状态下的颜色,例如按钮按下时的背景色或边框色。
// • backgroundDark (#1E40AF): 适用于深色背景,可能用于主要按钮或深色卡片背景。
// • backgroundDarker (#1E3A8A): 适用于更深的背景,通常用于头部导航栏或页脚。
// • backgroundDarkest (#172554): 适用于最深的背景,可能用于非常深色的区域或极端对比色。
return {
50: `hsl(var(--${name}-50))`,
100: `hsl(var(--${name}-100))`,
200: `hsl(var(--${name}-200))`,
300: `hsl(var(--${name}-300))`,
400: `hsl(var(--${name}-400))`,
500: `hsl(var(--${name}-500))`,
600: `hsl(var(--${name}-600))`,
700: `hsl(var(--${name}-700))`,
// 800: `hsl(var(--${name}-800))`,
// 900: `hsl(var(--${name}-900))`,
// 950: `hsl(var(--${name}-950))`,
// 激活状态下的颜色,适用于按钮按下时的背景色或边框色。
active: `hsl(var(--${name}-700))`,
// 浅色背景,适用于输入框或表单区域的背景。
'background-light': `hsl(var(--${name}-200))`,
// 适用于略浅的背景色,通常用于次要背景或略浅的区域。
'background-lighter': `hsl(var(--${name}-100))`,
// 最浅的背景色,适用于非常轻微的阴影或卡片的背景。
'background-lightest': `hsl(var(--${name}-50))`,
// 适用于普通边框,可能用于按钮或卡片的边框。
border: `hsl(var(--${name}-400))`,
// 浅色边框,适用于输入框或卡片的边框。
'border-light': `hsl(var(--${name}-300))`,
foreground: `hsl(var(--${name}-foreground))`,
// 鼠标悬停状态下的颜色,适用于按钮悬停时的背景色或边框色。
hover: `hsl(var(--${name}-600))`,
// 主色文本
text: `hsl(var(--${name}-500))`,
// 主色文本激活态
'text-active': `hsl(var(--${name}-700))`,
// 主色文本悬浮态
'text-hover': `hsl(var(--${name}-600))`,
};
}

View File

@@ -0,0 +1,3 @@
declare module '@tailwindcss/nesting' {
export default any;
}

View File

@@ -0,0 +1,53 @@
import plugin from 'tailwindcss/plugin.js';
const enterAnimationPlugin = plugin(({ addUtilities }) => {
const maxChild = 5;
const utilities: Record<string, any> = {};
for (let i = 1; i <= maxChild; i++) {
const baseDelay = 0.1;
const delay = `${baseDelay * i}s`;
utilities[`.enter-x:nth-child(${i})`] = {
animation: `enter-x-animation 0.3s ease-in-out ${delay} forwards`,
opacity: '0',
transform: `translateX(50px)`,
};
utilities[`.enter-y:nth-child(${i})`] = {
animation: `enter-y-animation 0.3s ease-in-out ${delay} forwards`,
opacity: '0',
transform: `translateY(50px)`,
};
utilities[`.-enter-x:nth-child(${i})`] = {
animation: `enter-x-animation 0.3s ease-in-out ${delay} forwards`,
opacity: '0',
transform: `translateX(-50px)`,
};
utilities[`.-enter-y:nth-child(${i})`] = {
animation: `enter-y-animation 0.3s ease-in-out ${delay} forwards`,
opacity: '0',
transform: `translateY(-50px)`,
};
}
// 添加动画关键帧
addUtilities(utilities);
addUtilities({
'@keyframes enter-x-animation': {
to: {
opacity: '1',
transform: 'translateX(0)',
},
},
'@keyframes enter-y-animation': {
to: {
opacity: '1',
transform: 'translateY(0)',
},
},
});
});
export { enterAnimationPlugin };

View File

@@ -0,0 +1,15 @@
import config from '.';
export default {
plugins: {
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
// Specifying the config is not necessary in most cases, but it is included
autoprefixer: {},
// 修复 element-plus 和 ant-design-vue 的样式和tailwindcss冲突问题
'postcss-antd-fixes': { prefixes: ['ant', 'el'] },
'postcss-import': {},
'postcss-preset-env': {},
tailwindcss: { config },
'tailwindcss/nesting': {},
},
};

View File

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

View File

@@ -0,0 +1,40 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Base",
"compilerOptions": {
"composite": false,
"target": "ESNext",
"moduleDetection": "force",
"experimentalDecorators": true,
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"strict": true,
"strictNullChecks": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitThis": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"inlineSources": false,
"noEmit": true,
"removeComments": true,
"sourceMap": false,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"preserveWatchOutput": true
},
"exclude": ["**/node_modules/**", "**/dist/**", "**/.turbo/**"]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Web Application",
"extends": "./base.json",
"compilerOptions": {
"jsx": "preserve",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"useDefineForClassFields": true,
"moduleResolution": "bundler",
"declaration": true,
"noEmit": false
}
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Node Config",
"extends": "./base.json",
"compilerOptions": {
"composite": false,
"lib": ["ESNext"],
"baseUrl": "./",
"types": ["node"],
"noImplicitAny": true
}
}

View File

@@ -0,0 +1,25 @@
{
"name": "@vben/tsconfig",
"version": "5.5.4",
"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/tsconfig"
},
"license": "MIT",
"type": "module",
"files": [
"base.json",
"library.json",
"node.json",
"web-app.json",
"web.json"
],
"dependencies": {
"@vben/types": "workspace:*",
"vite": "catalog:"
}
}

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Web Application",
"extends": "./web.json",
"compilerOptions": {
"types": ["vite/client", "@vben/types/global"]
}
}

View File

@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Web Package",
"extends": "./base.json",
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "vue",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"useDefineForClassFields": true,
"moduleResolution": "bundler",
"types": ["vite/client"],
"declaration": false
}
}

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.4",
"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 = GeneratorOptions & {
debug?: boolean;
defaultProvider?: 'esm.sh' | 'jsdelivr' | 'jspm.io';
importmap?: Array<{ name: string; range?: string }>;
};
// 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,164 @@
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';
interface IImportMap {
imports?: Record<string, string>;
scopes?: {
[scope: string]: Record<string, string>;
};
}
interface PrintPluginOptions {
/**
* 打印的数据
*/
infoMap?: Record<string, string | undefined>;
}
interface NitroMockPluginOptions {
/**
* mock server 包名
*/
mockServerPackage?: string;
/**
* mock 服务端口
*/
port?: number;
/**
* mock 日志是否打印
*/
verbose?: boolean;
}
interface ArchiverPluginOptions {
/**
* 输出文件名
* @default dist
*/
name?: string;
/**
* 输出目录
* @default .
*/
outputDir?: string;
}
/**
* importmap 插件配置
*/
interface ImportmapPluginOptions {
/**
* CDN 供应商
* @default jspm.io
*/
defaultProvider?: 'esm.sh' | 'jspm.io';
/** importmap 配置 */
importmap?: Array<{ name: string; range?: string }>;
/** 手动配置importmap */
inputMap?: IImportMap;
}
/**
* 用于判断是否需要加载插件
*/
interface ConditionPlugin {
// 判断条件
condition?: boolean;
// 插件对象
plugins: () => PluginOption[] | PromiseLike<PluginOption[]>;
}
interface CommonPluginOptions {
/** 是否开启devtools */
devtools?: boolean;
/** 环境变量 */
env?: Record<string, any>;
/** 是否注入metadata */
injectMetadata?: boolean;
/** 是否构建模式 */
isBuild?: boolean;
/** 构建模式 */
mode?: string;
/** 开启依赖分析 */
visualizer?: boolean | PluginVisualizerOptions;
}
interface ApplicationPluginOptions extends CommonPluginOptions {
/** 开启后会在打包dist同级生成dist.zip */
archiver?: boolean;
/** 压缩归档插件配置 */
archiverPluginOptions?: ArchiverPluginOptions;
/** 开启 gzip|brotli 压缩 */
compress?: boolean;
/** 压缩类型 */
compressTypes?: ('brotli' | 'gzip')[];
/** 在构建的时候抽离配置文件 */
extraAppConfig?: boolean;
/** 是否开启html插件 */
html?: boolean;
/** 是否开启i18n */
i18n?: boolean;
/** 是否开启 importmap CDN */
importmap?: boolean;
/** importmap 插件配置 */
importmapOptions?: ImportmapPluginOptions;
/** 是否注入app loading */
injectAppLoading?: boolean;
/** 是否注入全局scss */
injectGlobalScss?: boolean;
/** 是否注入版权信息 */
license?: boolean;
/** 是否开启nitro mock */
nitroMock?: boolean;
/** nitro mock 插件配置 */
nitroMockOptions?: NitroMockPluginOptions;
/** 开启控制台自定义打印 */
print?: boolean;
/** 打印插件配置 */
printInfoMap?: PrintPluginOptions['infoMap'];
/** 是否开启pwa */
pwa?: boolean;
/** pwa 插件配置 */
pwaOptions?: Partial<PwaPluginOptions>;
/** 是否开启vxe-table懒加载 */
vxeTableLazyImport?: boolean;
}
interface LibraryPluginOptions extends CommonPluginOptions {
/** 开启 dts 输出 */
dts?: boolean | PluginOptions;
}
type ApplicationOptions = ApplicationPluginOptions;
type LibraryOptions = LibraryPluginOptions;
type DefineApplicationOptions = (config?: ConfigEnv) => Promise<{
application?: ApplicationOptions;
vite?: UserConfig;
}>;
type DefineLibraryOptions = (config?: ConfigEnv) => Promise<{
library?: LibraryOptions;
vite?: UserConfig;
}>;
type DefineConfig = DefineApplicationOptions | DefineLibraryOptions;
export type {
ApplicationPluginOptions,
ArchiverPluginOptions,
CommonPluginOptions,
ConditionPlugin,
DefineApplicationOptions,
DefineConfig,
DefineLibraryOptions,
IImportMap,
ImportmapPluginOptions,
LibraryPluginOptions,
NitroMockPluginOptions,
PrintPluginOptions,
};

View File

@@ -0,0 +1,110 @@
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<
Partial<ApplicationPluginOptions> & {
appTitle: string;
base: string;
port: number;
}
> {
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"]
}