mkver
Version:
Node.js access to your app's version and release metadata
247 lines (244 loc) • 9.34 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.mkver = mkver;
const node_child_process_1 = require("node:child_process");
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const node_process_1 = require("node:process");
const semver_1 = __importDefault(require("semver"));
const date_1 = require("./date");
async function runGit(args, cwd) {
return new Promise((resolve, reject) => {
const gitCmd = process.env.GIT ?? "git";
const child = (0, node_child_process_1.spawn)(gitCmd, args, { cwd, stdio: "pipe" });
let stdout = "";
let stderr = "";
child.stdout?.on("data", (data) => (stdout += data));
child.stderr?.on("data", (data) => (stderr += data));
child.on("error", (error) => {
reject(gitError(error, cwd, args));
});
child.on("close", (code) => {
if (code === 0) {
resolve(stdout);
}
else {
reject(gitError({ code, stderr }, cwd, args));
}
});
});
}
function gitError(error, cwd, args) {
if (typeof error === "object" && error != null) {
const err = error;
if (err.code === "ENOENT") {
return new Error("mkver requires git but it was not found on your PATH. Install git and try again.");
}
const stderr = err.stderr?.trim() ?? "";
if (/not a git repository/i.test(stderr)) {
return new Error(`Directory ${cwd} is not inside a git repository. Run mkver from within a git repo or initialize one and retry.`);
}
if (/does not have any commits yet/i.test(stderr) ||
/ambiguous argument ['"]HEAD['"]/i.test(stderr) ||
/unknown revision or path not in the working tree/i.test(stderr)) {
return new Error(`The git repository at ${cwd} has no commits yet. Create an initial commit before running mkver.`);
}
if (stderr.length > 0) {
return new Error(`git ${args.join(" ")} failed: ${stderr}`);
}
if (typeof err.message === "string" &&
err.message.length > 0) {
return new Error(`git ${args.join(" ")} failed: ${err.message}`);
}
}
return new Error(`git ${args.join(" ")} failed: ${String(error)}`);
}
function notBlank(s) {
return s != null && String(s).trim().length > 0;
}
/**
* Recursively searches for package.json starting from the given directory,
* moving up the directory tree until found or reaching the filesystem root.
*
* @param dir - The directory to start searching from
* @returns Promise resolving to version and directory info, or undefined if not found
* @throws Error if package.json is found but has no version field
*/
async function findPackageVersion(dir) {
const path = (0, node_path_1.resolve)((0, node_path_1.join)(dir, "package.json"));
try {
const json = JSON.parse((await (0, promises_1.readFile)(path)).toString());
if (json != null) {
if (notBlank(json.version)) {
return { version: json.version, dir };
}
else {
throw new Error("No version field found in " + path);
}
}
}
catch (err) {
const parent = (0, node_path_1.resolve)((0, node_path_1.join)(dir, ".."));
if ((0, node_path_1.resolve)(dir) !== parent) {
return findPackageVersion(parent);
}
else {
throw err;
}
}
}
/**
* Retrieves the current git commit SHA from the specified directory.
*
* @param cwd - The working directory to run git command in
* @returns Promise resolving to the 40-character commit SHA
* @throws Error if git command fails or returns invalid SHA
*/
async function headSha(cwd) {
const gitSha = (await runGit(["rev-parse", "-q", "HEAD"], cwd)).trim();
if (gitSha.length !== 40 || !/^[a-f0-9]{40}$/i.test(gitSha)) {
throw new Error("Invalid git SHA: " + gitSha);
}
else {
return gitSha;
}
}
/**
* Retrieves the commit date of the current git HEAD as a Date object.
*
* @param cwd - The working directory to run git command in
* @returns Promise resolving to the Date of the commit
* @throws Error if git command fails or returns invalid timestamp
*/
async function headUnixtime(cwd) {
const unixtimeStr = await runGit(["log", "-1", "--pretty=format:%ct"], cwd);
const unixtime = parseInt(unixtimeStr);
const date = new Date(unixtime * 1000);
if (date > new Date() || date < new Date(2000, 0, 1)) {
throw new Error("Invalid commit timestamp: " + unixtime);
}
return date;
}
/**
* Renders version information into the appropriate format based on file extension.
* Supports TypeScript (.ts), ES modules (.mjs), and CommonJS (.js/.cjs) formats.
*
* @param o - The version information object to render
* @returns The formatted code as a string
* @throws Error if the file extension is not supported
*/
function renderVersionInfo(o) {
const msg = [];
const ext = o.path.ext.toLowerCase();
// .js maintains CommonJS for backward compatibility, .cjs is explicit CommonJS
const cjs = ext === ".js" || ext === ".cjs";
const mjs = ext === ".mjs";
const ts = ext === ".ts";
if (!cjs && !mjs && !ts) {
throw new Error(`Unsupported file extension: expected ${JSON.stringify(o.path)} to end in .ts, .js, .mjs, or .cjs`);
}
if (cjs) {
msg.push(`"use strict";`, `Object.defineProperty(exports, "__esModule", { value: true });`);
}
const parsed = semver_1.default.parse(o.version);
const fields = [];
for (const { field, value } of [
{ field: "version", value: o.version },
{ field: "versionMajor", value: parsed?.major },
{ field: "versionMinor", value: parsed?.minor },
{ field: "versionPatch", value: parsed?.patch },
{ field: "versionPrerelease", value: parsed?.prerelease },
{ field: "release", value: o.release },
{ field: "gitSha", value: o.gitSha },
{ field: "gitDate", value: o.gitDate },
]) {
if (value != null) {
fields.push(field);
const strVal = value instanceof Date
? `new Date(${value.getTime()})`
: JSON.stringify(value);
const ea = `${field} = ${strVal}`;
msg.push(cjs ? `exports.${ea};` : `export const ${ea};`);
}
}
if (ts || mjs) {
msg.push(`export default {\n ${fields.join(",\n ")},\n};`);
}
return msg.join("\n") + "\n";
}
/**
* Writes a file with version and release metadata to `outputFilePath`
*
* @param outputFilePath - The file to write to. Defaults to "./Version.ts" if
* the parameter is not provided or "". The file format is determined by the
* file extension. Supported extensions are ".ts", ".js", ".mjs", and ".cjs".
*
* @returns The version and release metadata written to the file.
*/
async function mkver(outputFilePath) {
if (outputFilePath == null || outputFilePath.trim().length === 0) {
outputFilePath = "./Version.ts";
}
const file = (0, node_path_1.resolve)((0, node_path_1.normalize)(outputFilePath));
const parsed = (0, node_path_1.parse)(file);
const v = await findPackageVersion(parsed.dir);
if (v == null) {
throw new Error("No package.json found in " + parsed.dir + " or parent directories");
}
const gitSha = await headSha(v.dir);
const gitDate = await headUnixtime(v.dir);
const versionInfo = {
path: parsed,
version: v.version,
release: `${v.version}+${(0, date_1.fmtYMDHMS)(gitDate)}`,
gitSha,
gitDate,
};
const buf = renderVersionInfo(versionInfo);
await (0, promises_1.mkdir)(parsed.dir, { recursive: true });
await (0, promises_1.writeFile)(file, buf);
return versionInfo;
}
async function main() {
const arg = node_process_1.argv[2] ?? "";
if (["--help", "-h"].includes(arg)) {
// Show them usage instructions:
console.log(`Usage: mkver [FILE]
Provides Node.js access to your app's version and release metadata.
With no FILE, default output is "./Version.ts".
Options:
-h, --help Show this help message
-v, --version Show version number
See <https://github.com/photostructure/mkver> for more information.`);
}
else if (["--version", "-v"].includes(arg)) {
// Show version information
try {
const packageInfo = await findPackageVersion(__dirname);
if (packageInfo) {
console.log(packageInfo.version);
}
else {
console.log("Unknown version");
}
}
catch {
console.log("Unknown version");
}
}
else {
return mkver(arg);
}
}
// Entry point check - if this module is run directly. (`require` may not be defined!)
if (require?.main === module) {
main().catch((error) => {
console.error("Failed: " + error);
(0, node_process_1.exit)(1);
});
}
//# sourceMappingURL=mkver.js.map