chore: init project
This commit is contained in:
3
scripts/vsh/README.md
Normal file
3
scripts/vsh/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @vben/vsh
|
||||
|
||||
shell 脚本工具集合
|
3
scripts/vsh/bin/vsh.mjs
Executable file
3
scripts/vsh/bin/vsh.mjs
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import('../dist/index.mjs');
|
7
scripts/vsh/build.config.ts
Normal file
7
scripts/vsh/build.config.ts
Normal 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
35
scripts/vsh/package.json
Normal 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"
|
||||
}
|
||||
}
|
70
scripts/vsh/src/check-circular/index.ts
Normal file
70
scripts/vsh/src/check-circular/index.ts
Normal 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 };
|
70
scripts/vsh/src/check-dep/index.ts
Normal file
70
scripts/vsh/src/check-dep/index.ts
Normal 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 };
|
89
scripts/vsh/src/clean/index.ts
Normal file
89
scripts/vsh/src/clean/index.ts
Normal 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 };
|
77
scripts/vsh/src/code-workspace/index.ts
Normal file
77
scripts/vsh/src/code-workspace/index.ts
Normal 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
44
scripts/vsh/src/index.ts
Normal 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);
|
||||
}
|
39
scripts/vsh/src/lint/index.ts
Normal file
39
scripts/vsh/src/lint/index.ts
Normal 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 };
|
182
scripts/vsh/src/publint/index.ts
Normal file
182
scripts/vsh/src/publint/index.ts
Normal 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 };
|
5
scripts/vsh/tsconfig.json
Normal file
5
scripts/vsh/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/node.json",
|
||||
"include": ["src"]
|
||||
}
|
Reference in New Issue
Block a user