UNPKG

mkver

Version:

Node.js access to your app's version and release metadata

247 lines (244 loc) 9.34 kB
#!/usr/bin/env node "use strict"; 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