import assert from 'node:assert';
import childProcess from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import prettier from 'prettier';
import ts from 'typescript';
export function localRepoPath(...paths: ReadonlyArray<string>): string {
const repoDir = new URL('..', import.meta.url).pathname;
return path.join(repoDir, ...paths);
}
interface MakeTmpDirReturn {
tmpDirPath: (...paths: ReadonlyArray<string>) => string;
}
export function makeTmpDir(name: string): MakeTmpDirReturn {
const tmpDir = path.join(os.tmpdir(), name);
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.mkdirSync(tmpDir);
return {
tmpDirPath: (...paths) => path.join(tmpDir, ...paths),
};
}
interface NPMOptions extends SpawnOptions {
quiet?: boolean;
}
export function npm(options?: NPMOptions) {
const globalOptions = options?.quiet === true ? ['--quiet'] : [];
return {
run(...args: ReadonlyArray<string>): void {
spawn('npm', [...globalOptions, 'run', ...args], options);
},
install(...args: ReadonlyArray<string>): void {
spawn('npm', [...globalOptions, 'install', ...args], options);
},
ci(...args: ReadonlyArray<string>): void {
spawn('npm', [...globalOptions, 'ci', ...args], options);
},
exec(...args: ReadonlyArray<string>): void {
spawn('npm', [...globalOptions, 'exec', ...args], options);
},
pack(...args: ReadonlyArray<string>): string {
return spawnOutput('npm', [...globalOptions, 'pack', ...args], options);
},
diff(...args: ReadonlyArray<string>): string {
return spawnOutput('npm', [...globalOptions, 'diff', ...args], options);
},
};
}
interface GITOptions extends SpawnOptions {
quiet?: boolean;
}
export function git(options?: GITOptions) {
const cmdOptions = options?.quiet === true ? ['--quiet'] : [];
return {
clone(...args: ReadonlyArray<string>): void {
spawn('git', ['clone', ...cmdOptions, ...args], options);
},
checkout(...args: ReadonlyArray<string>): void {
spawn('git', ['checkout', ...cmdOptions, ...args], options);
},
revParse(...args: ReadonlyArray<string>): string {
return spawnOutput('git', ['rev-parse', ...cmdOptions, ...args], options);
},
revList(...args: ReadonlyArray<string>): Array<string> {
const allArgs = ['rev-list', ...cmdOptions, ...args];
const result = spawnOutput('git', allArgs, options);
return result === '' ? [] : result.split('\n');
},
catFile(...args: ReadonlyArray<string>): string {
return spawnOutput('git', ['cat-file', ...cmdOptions, ...args], options);
},
log(...args: ReadonlyArray<string>): string {
return spawnOutput('git', ['log', ...cmdOptions, ...args], options);
},
};
}
interface SpawnOptions {
cwd?: string;
env?: typeof process.env;
}
function spawnOutput(
command: string,
args: ReadonlyArray<string>,
options?: SpawnOptions,
): string {
const result = childProcess.spawnSync(command, args, {
maxBuffer: 10 * 1024 * 1024,
stdio: ['inherit', 'pipe', 'inherit'],
encoding: 'utf-8',
...options,
});
if (result.status !== 0) {
throw new Error(`Command failed: ${command} ${args.join(' ')}`);
}
return result.stdout.toString().trimEnd();
}
function spawn(
command: string,
args: ReadonlyArray<string>,
options?: SpawnOptions,
): void {
const result = childProcess.spawnSync(command, args, {
stdio: 'inherit',
...options,
});
if (result.status !== 0) {
throw new Error(`Command failed: ${command} ${args.join(' ')}`);
}
}
function* readdirRecursive(dirPath: string): Generator<{
name: string;
filepath: string;
stats: fs.Stats;
}> {
for (const name of fs.readdirSync(dirPath)) {
const filepath = path.join(dirPath, name);
const stats = fs.lstatSync(filepath);
if (stats.isDirectory()) {
yield* readdirRecursive(filepath);
} else {
yield { name, filepath, stats };
}
}
}
export function showDirStats(dirPath: string): void {
const fileTypes: {
[filetype: string]: { filepaths: Array<string>; size: number };
} = {};
let totalSize = 0;
for (const { name, filepath, stats } of readdirRecursive(dirPath)) {
const ext = name.split('.').slice(1).join('.');
const filetype = ext ? '*.' + ext : name;
fileTypes[filetype] ??= { filepaths: [], size: 0 };
totalSize += stats.size;
fileTypes[filetype].size += stats.size;
fileTypes[filetype].filepaths.push(filepath);
}
const stats: Array<[string, number]> = [];
for (const [filetype, typeStats] of Object.entries(fileTypes)) {
const numFiles = typeStats.filepaths.length;
if (numFiles > 1) {
stats.push([filetype + ' x' + numFiles, typeStats.size]);
} else {
const relativePath = path.relative(dirPath, typeStats.filepaths[0]);
stats.push([relativePath, typeStats.size]);
}
}
stats.sort((a, b) => b[1] - a[1]);
const prettyStats = stats.map(([type, size]) => [
type,
(size / 1024).toFixed(2) + ' KB',
]);
const typeMaxLength = Math.max(...prettyStats.map((x) => x[0].length));
const sizeMaxLength = Math.max(...prettyStats.map((x) => x[1].length));
for (const [type, size] of prettyStats) {
console.log(
type.padStart(typeMaxLength) + ' | ' + size.padStart(sizeMaxLength),
);
}
console.log('-'.repeat(typeMaxLength + 3 + sizeMaxLength));
const totalMB = (totalSize / 1024 / 1024).toFixed(2) + ' MB';
console.log(
'Total'.padStart(typeMaxLength) + ' | ' + totalMB.padStart(sizeMaxLength),
);
}
const prettierConfig = JSON.parse(
fs.readFileSync(localRepoPath('.prettierrc'), 'utf-8'),
);
export function writeGeneratedFile(filepath: string, body: string): void {
const formatted = prettier.format(body, { filepath, ...prettierConfig });
fs.mkdirSync(path.dirname(filepath), { recursive: true });
fs.writeFileSync(filepath, formatted);
}
interface PackageJSON {
description: string;
version: string;
private?: boolean;
repository?: { url?: string };
scripts?: { [name: string]: string };
type?: string;
exports: { [path: string]: string };
types?: string;
typesVersions: { [ranges: string]: { [path: string]: Array<string> } };
devDependencies?: { [name: string]: string };
publishConfig: { tag: string };
main?: string;
module?: string;
}
export function readPackageJSON(
dirPath: string = localRepoPath(),
): PackageJSON {
const filepath = path.join(dirPath, 'package.json');
return JSON.parse(fs.readFileSync(filepath, 'utf-8'));
}
export function readTSConfig(overrides?: any): ts.CompilerOptions {
const tsConfigPath = localRepoPath('tsconfig.json');
const { config: tsConfig, error: tsConfigError } =
ts.parseConfigFileTextToJson(
tsConfigPath,
fs.readFileSync(tsConfigPath, 'utf-8'),
);
assert(tsConfigError === undefined, 'Fail to parse config: ' + tsConfigError);
assert(
tsConfig.compilerOptions,
'"tsconfig.json" should have `compilerOptions`',
);
const { options: tsOptions, errors: tsOptionsErrors } =
ts.convertCompilerOptionsFromJson(
{ ...tsConfig.compilerOptions, ...overrides },
process.cwd(),
);
assert(
tsOptionsErrors.length === 0,
'Fail to parse options: ' + tsOptionsErrors,
);
return tsOptions;
}