• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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