chore: init project

This commit is contained in:
vben
2024-05-19 21:20:42 +08:00
commit 399334ac57
630 changed files with 45623 additions and 0 deletions

3
scripts/vsh/README.md Normal file
View File

@@ -0,0 +1,3 @@
# @vben/vsh
shell 脚本工具集合

3
scripts/vsh/bin/vsh.mjs Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
import('../dist/index.mjs');

View File

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

35
scripts/vsh/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "@vben/vsh",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "MIT",
"scripts": {
"#build": "pnpm unbuild",
"stub": "pnpm unbuild --stub"
},
"files": [
"dist"
],
"bin": {
"vsh": "./bin/vsh.mjs"
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"imports": {
"#*": "./src/*"
},
"exports": {
".": {
"default": "./dist/index.mjs"
},
"./package.json": "./package.json"
},
"dependencies": {
"@vben/node-utils": "workspace:*",
"cac": "^6.7.14",
"circular-dependency-scanner": "^2.2.2",
"depcheck": "^1.4.7",
"publint": "^0.2.8"
}
}

View File

@@ -0,0 +1,70 @@
import type { CAC } from 'cac';
import { extname } from 'node:path';
import { getStagedFiles } from '@vben/node-utils';
import { circularDepsDetect, printCircles } from 'circular-dependency-scanner';
const IGNORE_DIR = [
'dist',
'.turbo',
'output',
'.cache',
'scripts',
'internal',
// 'packages/@vben-core/shared/shadcn-ui/',
'packages/@vben-core/uikit/menu-ui/src/',
].join(',');
const IGNORE = [`**/{${IGNORE_DIR}}/**`];
interface CommandOptions {
staged: boolean;
verbose: boolean;
}
async function checkCircular({ staged, verbose }: CommandOptions) {
const results = await circularDepsDetect({
absolute: staged,
cwd: process.cwd(),
ignore: IGNORE,
});
if (staged) {
let files = await getStagedFiles();
files = files.filter((file) =>
['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx', '.vue'].includes(
extname(file),
),
);
const circularFiles: string[][] = [];
for (const file of files) {
for (const result of results) {
const resultFiles = result.flat();
if (resultFiles.includes(file)) {
circularFiles.push(result);
}
}
}
verbose && printCircles(circularFiles);
} else {
verbose && printCircles(results);
}
}
function defineCheckCircularCommand(cac: CAC) {
cac
.command('check-circular')
.option(
'--staged',
'Whether it is the staged commit mode, in which mode, if there is a circular dependency, an alarm will be given.',
)
.usage(`Analysis of project circular dependencies.`)
.action(async ({ staged }) => {
await checkCircular({ staged, verbose: true });
});
}
export { defineCheckCircularCommand };

View File

@@ -0,0 +1,70 @@
import type { CAC } from 'cac';
import { getPackages } from '@vben/node-utils';
import depcheck from 'depcheck';
async function runDepcheck() {
const { packages } = await getPackages();
await Promise.all(
packages.map(async (pkg) => {
if (
[
'@vben/commitlint-config',
'@vben/eslint-config',
'@vben/lint-staged-config',
'@vben/node-utils',
'@vben/prettier-config',
'@vben/stylelint-config',
'@vben/tailwind-config',
'@vben/tsconfig',
'@vben/vite-config',
].includes(pkg.packageJson.name)
) {
return;
}
const unused = await depcheck(pkg.dir, {
ignoreMatches: [
'vite',
'vitest',
'unbuild',
'@vben/tsconfig',
'@vben/vite-config',
'@vben/tailwind-config',
'@types/*',
'@vben-core/design',
],
ignorePatterns: ['dist', 'node_modules', 'public'],
});
if (
Object.keys(unused.missing).length === 0 &&
unused.dependencies.length === 0 &&
unused.devDependencies.length === 0
) {
return;
}
console.error(
'\n',
pkg.packageJson.name,
'\n missing:',
unused.missing,
'\n dependencies:',
unused.dependencies,
'\n devDependencies:',
unused.devDependencies,
);
}),
);
}
function defineDepcheckCommand(cac: CAC) {
cac
.command('check-dep')
.usage(`Analysis of project circular dependencies.`)
.action(async () => {
await runDepcheck();
});
}
export { defineDepcheckCommand };

View File

@@ -0,0 +1,89 @@
import type { CAC } from 'cac';
import { join } from 'node:path';
import {
colors,
consola,
getPackages,
rimraf,
spinner,
} from '@vben/node-utils';
const CLEAN_DIRS = ['dist', 'node_modules', '.turbo'];
interface CleanCommandOptions {
/**
* Whether to delete the project pnpm-lock.yaml file.
* @default true
*/
delLock?: boolean;
/**
* Files that need to be cleared.
*/
dirs?: string[];
/**
* recursive clear.
* @default true
*/
recursive?: boolean;
}
async function runClean({
delLock = false,
dirs = [],
recursive,
}: CleanCommandOptions) {
const cleanDirs = dirs.length === 0 ? CLEAN_DIRS : dirs;
const cleanDirsText = JSON.stringify(cleanDirs);
spinner(`${colors.dim(cleanDirsText)} cleaning in progress...`, async () => {
await clean({ delLock, dirs: cleanDirs, recursive });
consola.success(colors.green(`clean up all \`${cleanDirsText}\` success.`));
});
}
async function clean({ delLock, dirs = [], recursive }: CleanCommandOptions) {
const { packages, rootDir } = await getPackages();
// Delete the project pnpm-lock.yaml file
if (delLock) {
await rimraf(join(rootDir, 'pnpm-lock.yaml'));
}
// Recursively delete the specified folders under all package directories
if (recursive) {
await Promise.all(
packages.map((pkg) => {
const pkgRoot = dirs.map((dir) => join(pkg.dir, dir));
return rimraf(pkgRoot, { preserveRoot: true });
}),
);
}
// Only delete the specified folders in the root directory
await Promise.all(
dirs.map((dir) => rimraf(join(process.cwd(), dir), { preserveRoot: true })),
);
}
function defineCleanCommand(cac: CAC) {
cac
.command('clean [dirs...]')
.usage(
`Delete all ['dist', 'node_modules', '.turbo'] directories under the project.`,
)
.option('-r,--recursive', 'Recursively clean all packages in a monorepo.', {
default: true,
})
.option('--del-lock', 'Delete the project pnpm-lock.yaml file.', {
default: true,
})
.action(
async (dirs, { delLock, recursive }) =>
await runClean({ delLock, dirs, recursive }),
);
}
export { defineCleanCommand };

View File

@@ -0,0 +1,77 @@
import type { CAC } from 'cac';
import { join, relative } from 'node:path';
import {
colors,
consola,
findMonorepoRoot,
fs,
getPackages,
gitAdd,
prettierFormat,
} from '@vben/node-utils';
const CODE_WORKSPACE_FILE = join('vben-admin.code-workspace');
interface CodeWorkspaceCommandOptions {
autoCommit?: boolean;
spaces?: number;
}
async function createCodeWorkspace({
autoCommit = false,
spaces = 2,
}: CodeWorkspaceCommandOptions) {
const { packages, rootDir } = await getPackages();
let folders = packages.map((pkg) => {
const { dir, packageJson } = pkg;
return {
name: packageJson.name,
path: relative(rootDir, dir),
};
});
folders = folders.filter(Boolean);
const monorepoRoot = findMonorepoRoot();
const outputPath = join(monorepoRoot, CODE_WORKSPACE_FILE);
await fs.outputJSON(outputPath, { folders }, { encoding: 'utf8', spaces });
await prettierFormat(outputPath);
if (autoCommit) {
await gitAdd(CODE_WORKSPACE_FILE, monorepoRoot);
}
}
async function runCodeWorkspace({
autoCommit,
spaces,
}: CodeWorkspaceCommandOptions) {
await createCodeWorkspace({
autoCommit,
spaces,
});
if (autoCommit) {
return;
}
consola.log('');
consola.success(colors.green(`${CODE_WORKSPACE_FILE} is updated!`));
consola.log('');
}
function defineCodeWorkspaceCommand(cac: CAC) {
cac
.command('code-workspace')
.usage('Update the `.code-workspace` file')
.option('--spaces [number]', '.code-workspace JSON file spaces.', {
default: 2,
})
.option('--auto-commit', 'auto commit .code-workspace JSON file.', {
default: false,
})
.action(runCodeWorkspace);
}
export { defineCodeWorkspaceCommand };

44
scripts/vsh/src/index.ts Normal file
View File

@@ -0,0 +1,44 @@
import { colors, consola } from '@vben/node-utils';
import { cac } from 'cac';
import { defineCheckCircularCommand } from './check-circular';
import { defineDepcheckCommand } from './check-dep';
import { defineCleanCommand } from './clean';
import { defineCodeWorkspaceCommand } from './code-workspace';
import { defineLintCommand } from './lint';
import { definePubLintCommand } from './publint';
try {
const vsh = cac('vsh');
// vsh lint
defineLintCommand(vsh);
// vsh publint
definePubLintCommand(vsh);
// vsh clean
defineCleanCommand(vsh);
// vsh code-workspace
defineCodeWorkspaceCommand(vsh);
// vsh check-circular
defineCheckCircularCommand(vsh);
// vsh check-dep
defineDepcheckCommand(vsh);
// Invalid command
vsh.on('command:*', () => {
consola.error(colors.red('Invalid command!'));
process.exit(1);
});
vsh.usage('vsh');
vsh.help();
vsh.parse();
} catch (error) {
consola.error(error);
process.exit(1);
}

View File

@@ -0,0 +1,39 @@
import type { CAC } from 'cac';
import { $ } from '@vben/node-utils';
interface LintCommandOptions {
/**
* Format lint problem.
*/
format?: boolean;
}
async function runLint({ format }: LintCommandOptions) {
process.env.FORCE_COLOR = '3';
if (format) {
await $`stylelint "**/*.{vue,css,less.scss}" --cache --fix`;
await $`eslint . --cache --fix`;
await $`prettier . --write --cache`;
// await $`vsh publint --check`;
return;
}
await Promise.all([
$`eslint . --cache`,
// $`ls-lint`,
$`prettier . --ignore-unknown --check --cache`,
$`stylelint "**/*.{vue,css,less.scss}" --cache`,
// $`vsh publint --check`,
]);
}
function defineLintCommand(cac: CAC) {
cac
.command('lint')
.usage('Batch execute project lint check.')
.option('--format', 'Format lint problem.')
.action(runLint);
}
export { defineLintCommand };

View File

@@ -0,0 +1,182 @@
import type { CAC } from 'cac';
import type { Result } from 'publint';
import { basename, dirname, join } from 'node:path';
import {
UNICODE,
colors,
consola,
findMonorepoRoot,
fs,
generatorContentHash,
getPackages,
} from '@vben/node-utils';
import { publint } from 'publint';
import { formatMessage } from 'publint/utils';
const CACHE_FILE = join(
'node_modules',
'.cache',
'publint',
'.pkglintcache.json',
);
interface PubLintCommandOptions {
/**
* Only errors are checked, no program exit is performed
*/
check?: boolean;
}
/**
* Get files that require lint
* @param files
*/
async function getLintFiles(files: string[] = []) {
const lintFiles: string[] = [];
if (files?.length > 0) {
return files.filter((file) => basename(file) === 'package.json');
}
const { packages } = await getPackages();
for (const { dir } of packages) {
lintFiles.push(join(dir, 'package.json'));
}
return lintFiles;
}
function getCacheFile() {
const root = findMonorepoRoot();
return join(root, CACHE_FILE);
}
async function readCache(cacheFile: string) {
try {
await fs.ensureFile(cacheFile);
return await fs.readJSON(cacheFile, { encoding: 'utf8' });
} catch {
return {};
}
}
async function runPublint(files: string[], { check }: PubLintCommandOptions) {
const lintFiles = await getLintFiles(files);
const cacheFile = getCacheFile();
const cacheData = await readCache(cacheFile);
const cache: Record<string, { hash: string; result: Result }> = cacheData;
const results = await Promise.all(
lintFiles.map(async (file) => {
try {
const pkgJson = await fs.readJSON(file);
if (pkgJson.private) {
return null;
}
Reflect.deleteProperty(pkgJson, 'dependencies');
Reflect.deleteProperty(pkgJson, 'devDependencies');
Reflect.deleteProperty(pkgJson, 'peerDependencies');
const content = JSON.stringify(pkgJson);
const hash = generatorContentHash(content);
const publintResult: Result =
cache?.[file]?.hash === hash
? cache?.[file]?.result ?? []
: await publint({
level: 'suggestion',
pkgDir: dirname(file),
strict: true,
});
cache[file] = {
hash,
result: publintResult,
};
return { pkgJson, pkgPath: file, publintResult };
} catch {
return null;
}
}),
);
await fs.outputJSON(cacheFile, cache);
printResult(results, check);
}
function printResult(
results: Array<{
pkgJson: Record<string, number | string>;
pkgPath: string;
publintResult: Result;
} | null>,
check?: boolean,
) {
let errorCount = 0;
let warningCount = 0;
let suggestionsCount = 0;
for (const result of results) {
if (!result) {
continue;
}
const { pkgJson, pkgPath, publintResult } = result;
const messages = publintResult?.messages ?? [];
if (messages?.length < 1) {
continue;
}
consola.log('');
consola.log(pkgPath);
for (const message of messages) {
switch (message.type) {
case 'error': {
errorCount++;
break;
}
case 'warning': {
warningCount++;
break;
}
case 'suggestion': {
suggestionsCount++;
break;
}
// No default
}
const ruleUrl = `https://publint.dev/rules#${message.code.toLocaleLowerCase()}`;
consola.log(
` ${formatMessage(message, pkgJson)}${colors.dim(` ${ruleUrl}`)}`,
);
}
}
const totalCount = warningCount + errorCount + suggestionsCount;
if (totalCount > 0) {
consola.error(
colors.red(
`${UNICODE.FAILURE} ${totalCount} problem (${errorCount} errors, ${warningCount} warnings, ${suggestionsCount} suggestions)`,
),
);
!check && process.exit(1);
} else {
consola.log(colors.green(`${UNICODE.SUCCESS} No problem`));
}
}
function definePubLintCommand(cac: CAC) {
cac
.command('publint [...files]')
.usage('Check if the monorepo package conforms to the publint standard.')
.option('--check', 'Only errors are checked, no program exit is performed.')
.action(runPublint);
}
export { definePubLintCommand };

View File

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