Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 124 additions & 120 deletions scripts/configurePrerelease.mjs
Original file line number Diff line number Diff line change
@@ -1,120 +1,124 @@
import assert from "assert";
import {
readFileSync,
writeFileSync,
} from "fs";
import {
normalize,
relative,
} from "path";
import url from "url";

const __filename = url.fileURLToPath(new URL(import.meta.url));

/**
* A minimal description for a parsed package.json object.
* @typedef {{
name: string;
version: string;
keywords: string[];
}} PackageJson
*/

function main() {
const args = process.argv.slice(2);
if (args.length < 3) {
const thisProgramName = relative(process.cwd(), __filename);
console.log("Usage:");
console.log(`\tnode ${thisProgramName} <dev|insiders> <package.json location> <file containing version>`);
return;
}

const tag = args[0];
if (tag !== "dev" && tag !== "insiders" && tag !== "experimental") {
throw new Error(`Unexpected tag name '${tag}'.`);
}

// Acquire the version from the package.json file and modify it appropriately.
const packageJsonFilePath = normalize(args[1]);
/** @type {PackageJson} */
const packageJsonValue = JSON.parse(readFileSync(packageJsonFilePath).toString());

const { majorMinor, patch } = parsePackageJsonVersion(packageJsonValue.version);
const prereleasePatch = getPrereleasePatch(tag, patch);

// Acquire and modify the source file that exposes the version string.
const tsFilePath = normalize(args[2]);
const tsFileContents = readFileSync(tsFilePath).toString();
const modifiedTsFileContents = updateTsFile(tsFilePath, tsFileContents, majorMinor, patch, prereleasePatch);

// Ensure we are actually changing something - the user probably wants to know that the update failed.
if (tsFileContents === modifiedTsFileContents) {
let err = `\n '${tsFilePath}' was not updated while configuring for a prerelease publish for '${tag}'.\n `;
err += `Ensure that you have not already run this script; otherwise, erase your changes using 'git checkout -- "${tsFilePath}"'.`;
throw new Error(err + "\n");
}

// Finally write the changes to disk.
// Modify the package.json structure
packageJsonValue.version = `${majorMinor}.${prereleasePatch}`;
writeFileSync(packageJsonFilePath, JSON.stringify(packageJsonValue, undefined, 4));
writeFileSync(tsFilePath, modifiedTsFileContents);
}

/* eslint-disable no-restricted-syntax */
/**
* @param {string} tsFilePath
* @param {string} tsFileContents
* @param {string} majorMinor
* @param {string} patch
* @param {string} nightlyPatch
* @returns {string}
*/
function updateTsFile(tsFilePath, tsFileContents, majorMinor, patch, nightlyPatch) {
const majorMinorRgx = /export const versionMajorMinor = "(\d+\.\d+)"/;
const majorMinorMatch = majorMinorRgx.exec(tsFileContents);
assert(majorMinorMatch !== null, `The file '${tsFilePath}' seems to no longer have a string matching '${majorMinorRgx}'.`);
const parsedMajorMinor = majorMinorMatch[1];
assert(parsedMajorMinor === majorMinor, `versionMajorMinor does not match. ${tsFilePath}: '${parsedMajorMinor}'; package.json: '${majorMinor}'`);

const versionRgx = /export const version(?:: string)? = `\$\{versionMajorMinor\}\.(\d)(-\w+)?`;/;
const patchMatch = versionRgx.exec(tsFileContents);
assert(patchMatch !== null, `The file '${tsFilePath}' seems to no longer have a string matching '${versionRgx.toString()}'.`);
const parsedPatch = patchMatch[1];
if (parsedPatch !== patch) {
throw new Error(`patch does not match. ${tsFilePath}: '${parsedPatch}; package.json: '${patch}'`);
}

return tsFileContents.replace(versionRgx, `export const version: string = \`\${versionMajorMinor}.${nightlyPatch}\`;`);
}

/**
* @param {string} versionString
* @returns {{ majorMinor: string, patch: string }}
*/
function parsePackageJsonVersion(versionString) {
const versionRgx = /(\d+\.\d+)\.(\d+)($|-)/;
const match = versionString.match(versionRgx);
assert(match !== null, "package.json 'version' should match " + versionRgx.toString());
return { majorMinor: match[1], patch: match[2] };
}
/* eslint-enable no-restricted-syntax */

/**
* e.g. 0-dev.20170707
* @param {string} tag
* @param {string} plainPatch
* @returns {string}
*/
function getPrereleasePatch(tag, plainPatch) {
// We're going to append a representation of the current time at the end of the current version.
// String.prototype.toISOString() returns a 24-character string formatted as 'YYYY-MM-DDTHH:mm:ss.sssZ',
// but we'd prefer to just remove separators and limit ourselves to YYYYMMDD.
// UTC time will always be implicit here.
const now = new Date();
const timeStr = now.toISOString().replace(/[:T.-]/g, "").slice(0, 8);

return `${plainPatch}-${tag}.${timeStr}`;
}

main();
import assert from "assert";
import {
readFileSync,
writeFileSync,
} from "fs";
import {
normalize,
relative,
} from "path";
import url from "url";

const __filename = url.fileURLToPath(new URL(import.meta.url));

/**
* A minimal description for a parsed package.json object.
* @typedef {{
name: string;
version: string;
keywords: string[];
}} PackageJson
*/

function main() {
const args = process.argv.slice(2);
if (args.length < 3) {
const thisProgramName = relative(process.cwd(), __filename);
console.log("Usage:");
console.log(`\tnode ${thisProgramName} <dev|insiders> <package.json location> <file containing version>`);
return;
}

const tag = args[0];
if (tag !== "dev" && tag !== "insiders" && tag !== "experimental") {
throw new Error(`Unexpected tag name '${tag}'.`);
}

// Acquire the version from the package.json file and modify it appropriately.
const packageJsonFilePath = normalize(args[1]);
/** @type {PackageJson} */
const packageJsonValue = JSON.parse(readFileSync(packageJsonFilePath).toString());

const { majorMinor, patch } = parsePackageJsonVersion(packageJsonValue.version);
const prereleasePatch = getPrereleasePatch(tag, patch);

// Acquire and modify the source file that exposes the version string.
const tsFilePath = normalize(args[2]);
const tsFileContents = readFileSync(tsFilePath).toString();
const modifiedTsFileContents = updateTsFile(tsFilePath, tsFileContents, majorMinor, patch, prereleasePatch);

// Ensure we are actually changing something - the user probably wants to know that the update failed.
if (tsFileContents === modifiedTsFileContents) {
// Sanitize tsFilePath to prevent injection via untrusted input; use string
// concatenation (not template literals) so dynamic user-controlled values
// are never interpolated directly inside a template literal.
const safeTsFilePath = tsFilePath.replace(/[^\w.\-/\\: ]/g, "");
Comment on lines +52 to +55
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions a SQL injection and “parameterized queries”, but this script does not build or execute SQL. The added comments about “injection”/template literals appear to be addressing a false positive and are misleading for future maintainers; either update the PR description and inline comment to reflect the real risk being mitigated (console output / copy‑paste safety), or suppress/adjust the scanner rule for this location if it’s a known false positive.

Copilot uses AI. Check for mistakes.
let err = "\n '" + safeTsFilePath + "' was not updated while configuring for a prerelease publish for '" + tag + "'.\n ";
err += "Ensure that you have not already run this script; otherwise, erase your changes using 'git checkout -- \"" + safeTsFilePath + "\"'.";
Comment on lines +55 to +57
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safeTsFilePath is produced by stripping any character outside a narrow allowlist. Valid paths can include characters like parentheses, brackets, +, ,, @, or non-ASCII characters; the resulting error message (and suggested git checkout command) can point to a different/nonexistent path, making recovery harder. Prefer escaping/quoting for display (or use JSON.stringify / proper shell-escaping in the message) rather than altering the path string.

Copilot uses AI. Check for mistakes.
throw new Error(err + "\n");
}

// Finally write the changes to disk.
// Modify the package.json structure
packageJsonValue.version = `${majorMinor}.${prereleasePatch}`;
writeFileSync(packageJsonFilePath, JSON.stringify(packageJsonValue, undefined, 4));
writeFileSync(tsFilePath, modifiedTsFileContents);
}

/* eslint-disable no-restricted-syntax */
/**
* @param {string} tsFilePath
* @param {string} tsFileContents
* @param {string} majorMinor
* @param {string} patch
* @param {string} nightlyPatch
* @returns {string}
*/
function updateTsFile(tsFilePath, tsFileContents, majorMinor, patch, nightlyPatch) {
const majorMinorRgx = /export const versionMajorMinor = "(\d+\.\d+)"/;
const majorMinorMatch = majorMinorRgx.exec(tsFileContents);
assert(majorMinorMatch !== null, `The file '${tsFilePath}' seems to no longer have a string matching '${majorMinorRgx}'.`);
const parsedMajorMinor = majorMinorMatch[1];
assert(parsedMajorMinor === majorMinor, `versionMajorMinor does not match. ${tsFilePath}: '${parsedMajorMinor}'; package.json: '${majorMinor}'`);

const versionRgx = /export const version(?:: string)? = `\$\{versionMajorMinor\}\.(\d)(-\w+)?`;/;
const patchMatch = versionRgx.exec(tsFileContents);
assert(patchMatch !== null, `The file '${tsFilePath}' seems to no longer have a string matching '${versionRgx.toString()}'.`);
const parsedPatch = patchMatch[1];
if (parsedPatch !== patch) {
throw new Error(`patch does not match. ${tsFilePath}: '${parsedPatch}; package.json: '${patch}'`);
}

return tsFileContents.replace(versionRgx, `export const version: string = \`\${versionMajorMinor}.${nightlyPatch}\`;`);
}

/**
* @param {string} versionString
* @returns {{ majorMinor: string, patch: string }}
*/
function parsePackageJsonVersion(versionString) {
const versionRgx = /(\d+\.\d+)\.(\d+)($|-)/;
const match = versionString.match(versionRgx);
assert(match !== null, "package.json 'version' should match " + versionRgx.toString());
return { majorMinor: match[1], patch: match[2] };
}
/* eslint-enable no-restricted-syntax */

/**
* e.g. 0-dev.20170707
* @param {string} tag
* @param {string} plainPatch
* @returns {string}
*/
function getPrereleasePatch(tag, plainPatch) {
// We're going to append a representation of the current time at the end of the current version.
// String.prototype.toISOString() returns a 24-character string formatted as 'YYYY-MM-DDTHH:mm:ss.sssZ',
// but we'd prefer to just remove separators and limit ourselves to YYYYMMDD.
// UTC time will always be implicit here.
const now = new Date();
const timeStr = now.toISOString().replace(/[:T.-]/g, "").slice(0, 8);

return `${plainPatch}-${tag}.${timeStr}`;
}

main();
Loading