1import { normalize, relative } from "path"; 2import assert from "assert"; 3import { readFileSync, writeFileSync } from "fs"; 4import url from "url"; 5 6const __filename = url.fileURLToPath(new URL(import.meta.url)); 7 8/** 9 * A minimal description for a parsed package.json object. 10 * @typedef {{ 11 name: string; 12 version: string; 13 keywords: string[]; 14}} PackageJson 15 */ 16 17function main() { 18 const args = process.argv.slice(2); 19 if (args.length < 3) { 20 const thisProgramName = relative(process.cwd(), __filename); 21 console.log("Usage:"); 22 console.log(`\tnode ${thisProgramName} <dev|insiders> <package.json location> <file containing version>`); 23 return; 24 } 25 26 const tag = args[0]; 27 if (tag !== "dev" && tag !== "insiders" && tag !== "experimental") { 28 throw new Error(`Unexpected tag name '${tag}'.`); 29 } 30 31 // Acquire the version from the package.json file and modify it appropriately. 32 const packageJsonFilePath = normalize(args[1]); 33 /** @type {PackageJson} */ 34 const packageJsonValue = JSON.parse(readFileSync(packageJsonFilePath).toString()); 35 36 const { majorMinor, patch } = parsePackageJsonVersion(packageJsonValue.version); 37 const prereleasePatch = getPrereleasePatch(tag, patch); 38 39 // Acquire and modify the source file that exposes the version string. 40 const tsFilePath = normalize(args[2]); 41 const tsFileContents = readFileSync(tsFilePath).toString(); 42 const modifiedTsFileContents = updateTsFile(tsFilePath, tsFileContents, majorMinor, patch, prereleasePatch); 43 44 // Ensure we are actually changing something - the user probably wants to know that the update failed. 45 if (tsFileContents === modifiedTsFileContents) { 46 let err = `\n '${tsFilePath}' was not updated while configuring for a prerelease publish for '${tag}'.\n `; 47 err += `Ensure that you have not already run this script; otherwise, erase your changes using 'git checkout -- "${tsFilePath}"'.`; 48 throw new Error(err + "\n"); 49 } 50 51 // Finally write the changes to disk. 52 // Modify the package.json structure 53 packageJsonValue.version = `${majorMinor}.${prereleasePatch}`; 54 writeFileSync(packageJsonFilePath, JSON.stringify(packageJsonValue, /*replacer:*/ undefined, /*space:*/ 4)); 55 writeFileSync(tsFilePath, modifiedTsFileContents); 56} 57 58/* eslint-disable no-null/no-null */ 59/** 60 * @param {string} tsFilePath 61 * @param {string} tsFileContents 62 * @param {string} majorMinor 63 * @param {string} patch 64 * @param {string} nightlyPatch 65 * @returns {string} 66 */ 67function updateTsFile(tsFilePath, tsFileContents, majorMinor, patch, nightlyPatch) { 68 const majorMinorRgx = /export const versionMajorMinor = "(\d+\.\d+)"/; 69 const majorMinorMatch = majorMinorRgx.exec(tsFileContents); 70 assert(majorMinorMatch !== null, `The file '${tsFilePath}' seems to no longer have a string matching '${majorMinorRgx}'.`); 71 const parsedMajorMinor = majorMinorMatch[1]; 72 assert(parsedMajorMinor === majorMinor, `versionMajorMinor does not match. ${tsFilePath}: '${parsedMajorMinor}'; package.json: '${majorMinor}'`); 73 74 const versionRgx = /export const version(?:: string)? = `\$\{versionMajorMinor\}\.(\d)(-\w+)?`;/; 75 const patchMatch = versionRgx.exec(tsFileContents); 76 assert(patchMatch !== null, `The file '${tsFilePath}' seems to no longer have a string matching '${versionRgx.toString()}'.`); 77 const parsedPatch = patchMatch[1]; 78 if (parsedPatch !== patch) { 79 throw new Error(`patch does not match. ${tsFilePath}: '${parsedPatch}; package.json: '${patch}'`); 80 } 81 82 return tsFileContents.replace(versionRgx, `export const version: string = \`\${versionMajorMinor}.${nightlyPatch}\`;`); 83} 84 85/** 86 * @param {string} versionString 87 * @returns {{ majorMinor: string, patch: string }} 88 */ 89function parsePackageJsonVersion(versionString) { 90 const versionRgx = /(\d+\.\d+)\.(\d+)($|\-)/; 91 const match = versionString.match(versionRgx); 92 assert(match !== null, "package.json 'version' should match " + versionRgx.toString()); 93 return { majorMinor: match[1], patch: match[2] }; 94} 95/* eslint-enable no-null/no-null */ 96 97/** 98 * e.g. 0-dev.20170707 99 * @param {string} tag 100 * @param {string} plainPatch 101 * @returns {string} 102 */ 103function getPrereleasePatch(tag, plainPatch) { 104 // We're going to append a representation of the current time at the end of the current version. 105 // String.prototype.toISOString() returns a 24-character string formatted as 'YYYY-MM-DDTHH:mm:ss.sssZ', 106 // but we'd prefer to just remove separators and limit ourselves to YYYYMMDD. 107 // UTC time will always be implicit here. 108 const now = new Date(); 109 const timeStr = now.toISOString().replace(/:|T|\.|-/g, "").slice(0, 8); 110 111 return `${plainPatch}-${tag}.${timeStr}`; 112} 113 114main(); 115