Files
2026-07-13 13:39:47 +08:00

266 lines
6.0 KiB
TypeScript

import {
type ChildProcess,
exec as execCallback,
spawn,
} from "node:child_process";
import { promisify } from "node:util";
import {
existsSync,
mkdirSync,
rmSync,
cpSync,
writeFileSync,
readFileSync,
chmodSync,
readdirSync,
} from "node:fs";
import { join } from "node:path";
import { platform as osPlatform } from "node:os";
import { config, templateConfig, npmConfig } from "./config";
const exec = promisify(execCallback);
// ============================================================
// Utility Functions
// ============================================================
/**
* Get version number
* Priority: environment variable VERSION > Git Tag > 'dev'
*
* @example
* # Specify version via environment variable
* VERSION=1.2.3 gulp npmBuild
*
* # Or in package.json scripts
* "npm:build": "VERSION=1.2.3 gulp npmBuild"
*/
export async function getVersion(): Promise<string> {
// 1. Prefer environment variable
if (process.env.VERSION) {
console.log(`📌 使用指定版本: ${process.env.VERSION}`);
return process.env.VERSION;
}
// 2. Try to get from git
try {
const { stdout } = await exec("git describe --tags --always --dirty");
const version = stdout.trim();
if (version) {
console.log(`📌 使用 Git 版本: ${version}`);
return version;
}
} catch {
// Git command failed, continue
}
// 3. Default version
console.log("⚠️ 未找到版本信息,使用默认版本: dev");
return "dev";
}
/**
* Get executable file extension
*/
export function getExeExt(os: string = osPlatform()): string {
return os === "win32" ? ".exe" : "";
}
/**
* Create directory
*/
export function mkdir(dir: string): void {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
/**
* Recursively delete a file or directory
*/
export function rmrf(path: string): void {
if (existsSync(path)) {
rmSync(path, { recursive: true, force: true });
}
}
/**
* Copy a file or directory
*/
export function copyFile(src: string, dst: string): void {
cpSync(src, dst, { recursive: true });
}
/**
* Render a template file
*/
export function renderTemplate(
templateName: string,
context: Record<string, string> = {},
): string {
const templatePath = join(templateConfig.dir, templateName);
let template = readFileSync(templatePath, "utf-8");
for (const [key, value] of Object.entries(context)) {
const pattern = new RegExp(`{{\\s*${key}\\s*}}`, "g");
template = template.replace(pattern, value);
}
return template;
}
/**
* Render and parse a JSON template
*/
export function renderJsonTemplate<T = any>(
templateName: string,
context: Record<string, string> = {},
): T {
return JSON.parse(renderTemplate(templateName, context)) as T;
}
/**
* Indent multi-line text
*/
export function indentMultiline(content: string, spaces: number): string {
const padding = " ".repeat(spaces);
return content
.split("\n")
.map((line, index) => (index === 0 ? line : padding + line))
.join("\n");
}
/**
* Resolve a path under the release directory
*/
export function resolveReleasePath(...segments: string[]): string {
return join(config.RELEASE_DIR, ...segments);
}
/**
* Resolve a path under the npm scope directory
*/
export function resolveNpmScopePath(...segments: string[]): string {
return join(npmConfig.rootDir, npmConfig.scope, ...segments);
}
/**
* Write a JSON file
*/
export function writeJsonFile(filePath: string, data: any): void {
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
}
/**
* Write a text file
*/
export function writeTextFile(filePath: string, content: string): void {
writeFileSync(filePath, content, "utf-8");
}
export interface RunCommandOptions {
description?: string;
env?: Record<string, string>;
cwd?: string;
}
const childProcesses = new Set<ChildProcess>();
let cleanupRegistered = false;
function registerCleanup() {
if (cleanupRegistered) {
return;
}
cleanupRegistered = true;
const cleanup = () => {
for (const child of childProcesses) {
if (!child.killed) {
try {
child.kill();
} catch {
// Ignore cleanup errors
}
}
}
};
process.on("exit", cleanup);
process.on("SIGINT", () => {
cleanup();
process.exit(130);
});
process.on("SIGTERM", () => {
cleanup();
process.exit(143);
});
}
/**
* Run a command (with live output)
* @param command The command to execute
* @param args Command arguments
* @param options Options (description, env, cwd)
*/
export async function runCommand(
command: string,
args: string[] = [],
options: RunCommandOptions = {},
): Promise<void> {
registerCleanup();
if (options.description) {
console.log(`\n▶ ${options.description}: ${command} ${args.join(" ")}`);
}
return new Promise((resolve, reject) => {
const isWindows = process.platform === "win32";
// On Windows with shell mode, args containing spaces must be quoted
// because Node.js joins them with spaces without escaping
const escapedArgs = isWindows
? args.map((arg) => (arg.includes(" ") ? `"${arg}"` : arg))
: args;
const child = spawn(command, escapedArgs, {
cwd: options.cwd,
stdio: "inherit",
env: {
...process.env,
...options.env,
},
shell: isWindows,
});
childProcesses.add(child);
child.on("error", (error) => {
childProcesses.delete(child);
console.error(`执行命令失败: ${error.message}`);
reject(error);
});
child.on("close", (code) => {
childProcesses.delete(child);
if (code !== 0) {
const error = new Error(`命令执行失败,退出码: ${code}`);
reject(error);
} else {
resolve();
}
});
});
}
/**
* Set executable permissions on all files in a directory (Unix only)
*/
export function chmodExecutable(dir: string): void {
if (osPlatform() === "win32" || !existsSync(dir)) {
return;
}
const entries = readdirSync(dir);
for (const entry of entries) {
chmodSync(join(dir, entry), 0o755);
}
}