更新最新代码

This commit is contained in:
luob
2025-12-24 23:48:38 +08:00
parent e728cf2c5e
commit 1fd17ef73a
1320 changed files with 83513 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,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,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,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,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,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,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,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,141 @@
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-deprecated': null,
'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,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

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

View File

@@ -0,0 +1,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"]
}