This commit is contained in:
Your Name
2026-08-27 14:04:28 +08:00
parent f7720831be
commit 334890171e
3016 changed files with 263403 additions and 27971 deletions
@@ -0,0 +1,42 @@
{
"name": "@vben/node-utils",
"version": "5.7.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/node-utils"
},
"license": "MIT",
"type": "module",
"scripts": {
"stub": "node ./scripts/build.mjs"
},
"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",
"default": "./dist/index.mjs"
}
},
"dependencies": {
"@changesets/git": "catalog:",
"@manypkg/get-packages": "catalog:",
"chalk": "catalog:",
"consola": "catalog:",
"dayjs": "catalog:",
"execa": "catalog:",
"find-up": "catalog:",
"ora": "catalog:",
"pkg-types": "catalog:",
"rimraf": "catalog:"
}
}
@@ -0,0 +1,40 @@
import { spawnSync } from 'node:child_process';
const pnpmCommand =
process.env.npm_execpath && process.env.npm_execpath.endsWith('.cjs')
? [process.execPath, process.env.npm_execpath]
: ['pnpm'];
const steps = [
['exec', 'tsdown', '--no-dts'],
[
'exec',
'tsc',
'-p',
'tsconfig.build.json',
'--emitDeclarationOnly',
'--declaration',
'--outDir',
'dist',
],
];
for (const args of steps) {
const [command, ...commandArgs] = pnpmCommand;
let cmd = command;
if (cmd.includes(' ')) {
cmd = `"${command}"`;
}
const result = spawnSync(cmd, [...commandArgs, ...args], {
shell: true,
stdio: 'inherit',
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
@@ -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);
});
});
@@ -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);
});
});
@@ -0,0 +1,6 @@
enum UNICODE {
FAILURE = '\u2716', // ✖
SUCCESS = '\u2714', // ✔
}
export { UNICODE };
+12
View File
@@ -0,0 +1,12 @@
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone.js';
import utc from 'dayjs/plugin/utc.js';
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault('Asia/Shanghai');
const dateUtil = dayjs;
export { dateUtil };
@@ -0,0 +1,13 @@
import fs from 'node:fs/promises';
import { execa } from 'execa';
async function formatFile(filepath: string) {
await execa('oxfmt', [filepath], {
stdio: 'inherit',
});
return await fs.readFile(filepath, 'utf8');
}
export { formatFile };
+39
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;
}
}
+40
View File
@@ -0,0 +1,40 @@
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',
]);
const nullSeparator = '\u0000';
const normalizedStdout = stdout.endsWith(nullSeparator)
? stdout.slice(0, -1)
: stdout;
let changedList = normalizedStdout
? normalizedStdout.split(nullSeparator)
: [];
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 };
+18
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 };
@@ -0,0 +1,19 @@
export * from './constants';
export * from './date';
export { formatFile } from './formatter';
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 * 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';
@@ -0,0 +1,56 @@
import type { Package } from '@manypkg/get-packages';
import { existsSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import * as manypkg from '@manypkg/get-packages';
const { getPackages: getPackagesFunc, getPackagesSync: getPackagesSyncFunc } =
manypkg;
/**
* 查找大仓的根目录
* @param cwd
*/
function findMonorepoRoot(cwd: string = process.cwd()) {
let currentDir = resolve(cwd);
while (true) {
if (existsSync(join(currentDir, 'pnpm-lock.yaml'))) {
return currentDir;
}
const parentDir = dirname(currentDir);
if (parentDir === currentDir) {
return '';
}
currentDir = parentDir;
}
}
/**
* 获取大仓的所有包
*/
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: Package) => pkg.packageJson.name === pkgName);
}
export { findMonorepoRoot, getPackage, getPackages, getPackagesSync };
+11
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 };
@@ -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();
}
}
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"noEmit": false
},
"exclude": ["node_modules", "src/__tests__"]
}
@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"include": ["src"],
"exclude": ["node_modules"]
}
@@ -0,0 +1,10 @@
import { defineConfig } from 'tsdown';
export default defineConfig({
clean: false,
deps: {
skipNodeModulesBundle: true,
},
entry: ['src/index.ts'],
format: ['esm'],
});