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