mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-18 05:39:15 +01:00
329 lines
9.1 KiB
JavaScript
329 lines
9.1 KiB
JavaScript
// Copyright 2024 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// @ts-check
|
|
import { join } from 'node:path';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { styleText } from 'node:util';
|
|
import semver from 'semver';
|
|
import { got, HTTPError } from 'got';
|
|
import enquirer from 'enquirer';
|
|
import { execa } from 'execa';
|
|
import { assert } from './utils/assert.mjs';
|
|
|
|
const rootDir = join(import.meta.dirname, '..');
|
|
|
|
/**
|
|
* @param {string} path
|
|
* @returns {Promise<any>}
|
|
*/
|
|
async function readJsonFile(path) {
|
|
return JSON.parse(await readFile(path, 'utf-8'));
|
|
}
|
|
|
|
/**
|
|
* @param {string | number | null | undefined} value
|
|
* @returns {number | null}
|
|
*/
|
|
function parseNumberField(value) {
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
if (typeof value === 'number') {
|
|
return value;
|
|
}
|
|
const trimmed = value.trim();
|
|
if (trimmed === '') {
|
|
return null;
|
|
}
|
|
const parsed = Number(value);
|
|
if (!Number.isFinite(parsed)) {
|
|
return null;
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
const npm = got.extend({
|
|
prefixUrl: 'https://registry.npmjs.org/',
|
|
responseType: 'json',
|
|
retry: {
|
|
calculateDelay: retry => {
|
|
if (
|
|
retry.error instanceof HTTPError &&
|
|
retry.error.response.statusCode === 429
|
|
) {
|
|
const retryAfter = parseNumberField(
|
|
retry.error.response.headers['retry-after']
|
|
);
|
|
if (retryAfter != null) {
|
|
console.log(
|
|
styleText(
|
|
'gray',
|
|
`Rate limited, retrying after ${retryAfter} seconds`
|
|
)
|
|
);
|
|
return retryAfter * 1000;
|
|
}
|
|
}
|
|
|
|
return retry.computedValue;
|
|
},
|
|
},
|
|
});
|
|
|
|
const DependencyTypes = /** @type {const} */ ([
|
|
'dependencies',
|
|
'devDependencies',
|
|
'optionalDependencies',
|
|
'peerDependencies',
|
|
]);
|
|
|
|
/**
|
|
* @typedef {object} LocalDependency
|
|
* @prop {string} name
|
|
* @prop {(typeof DependencyTypes)[number]} depType
|
|
* @prop {string} requestedVersion
|
|
* @prop {string} resolvedVersion
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} FetchedDependencyProps
|
|
* @prop {string} latestVersion
|
|
* @prop {'commonjs' | 'esm'} moduleType
|
|
* @prop {semver.ReleaseType | null} diff
|
|
*
|
|
* @typedef {LocalDependency & FetchedDependencyProps} FetchedDependency
|
|
*/
|
|
|
|
const packageJson = await readJsonFile(join(rootDir, 'package.json'));
|
|
const packageLock = await readJsonFile(join(rootDir, 'package-lock.json'));
|
|
|
|
/** @type {ReadonlyArray<LocalDependency>} */
|
|
const localDeps = DependencyTypes.flatMap(depType => {
|
|
return Object.keys(packageJson[depType] ?? {}).map(name => {
|
|
const requestedVersion = packageJson[depType][name];
|
|
const resolvedVersion =
|
|
packageLock.packages[`node_modules/${name}`]?.version;
|
|
assert(resolvedVersion, `Could not find resolved version for ${name}`);
|
|
|
|
return { name, depType, requestedVersion, resolvedVersion };
|
|
});
|
|
});
|
|
|
|
console.log(
|
|
`Found ${styleText('cyan', `${localDeps.length}`)} local dependencies`
|
|
);
|
|
|
|
/** @type {ReadonlyArray<FetchedDependency>} */
|
|
const fetchedDeps = await Promise.all(
|
|
localDeps.map(async dep => {
|
|
/** @type {any} */
|
|
const info = await npm(`${dep.name}/latest`).json();
|
|
const latestVersion = info.version;
|
|
const moduleType = info.type ?? 'commonjs';
|
|
assert(
|
|
moduleType === 'commonjs' || moduleType === 'module',
|
|
`Unexpected module type for ${dep.name}: ${moduleType}`
|
|
);
|
|
const diff = semver.lt(dep.resolvedVersion, latestVersion)
|
|
? semver.diff(dep.resolvedVersion, latestVersion)
|
|
: null;
|
|
return { ...dep, latestVersion, moduleType, diff };
|
|
})
|
|
);
|
|
|
|
const outdatedDeps = fetchedDeps.filter(dep => dep.diff != null);
|
|
console.log(
|
|
`Found ${styleText('cyan', `${outdatedDeps.length}`)} outdated dependencies`
|
|
);
|
|
|
|
const upgradeableDeps = outdatedDeps.filter(dep => {
|
|
return dep.moduleType === 'commonjs';
|
|
});
|
|
|
|
console.log(
|
|
`Found ${styleText('cyan', `${upgradeableDeps.length}`)} upgradeable dependencies`
|
|
);
|
|
|
|
/** @type {Map<string, Set<string>>} */
|
|
const upgradeableDepsByDiff = new Map();
|
|
|
|
for (const dep of upgradeableDeps) {
|
|
assert(dep.diff != null, 'Expected diff to be non-null');
|
|
|
|
let group = upgradeableDepsByDiff.get(dep.diff);
|
|
if (group == null) {
|
|
group = new Set();
|
|
upgradeableDepsByDiff.set(dep.diff, group);
|
|
}
|
|
|
|
group.add(dep.name);
|
|
}
|
|
|
|
for (const [diff, deps] of upgradeableDepsByDiff) {
|
|
console.log(` - ${diff}: ${styleText('cyan', `${deps.size}`)}`);
|
|
}
|
|
|
|
let longestNameLength = 0;
|
|
for (const dep of upgradeableDeps) {
|
|
longestNameLength = Math.max(longestNameLength, dep.name.length);
|
|
}
|
|
|
|
/** @type {{ approvedDeps: ReadonlyArray<string>; }} */
|
|
const { approvedDeps } = await enquirer.prompt({
|
|
type: 'multiselect',
|
|
name: 'approvedDeps',
|
|
message: 'Select which dependencies to upgrade',
|
|
choices: upgradeableDeps.map(deps => {
|
|
/** @type {'red' | 'green' | 'yellow'} */
|
|
let color = 'red';
|
|
if (deps.diff === 'patch') {
|
|
color = 'green';
|
|
} else if (deps.diff === 'minor') {
|
|
color = 'yellow';
|
|
}
|
|
|
|
return {
|
|
name: deps.name,
|
|
message: deps.name.padEnd(longestNameLength),
|
|
hint: `(${styleText(color, `${deps.diff}`)}: ${deps.resolvedVersion} -> ${styleText(color, deps.latestVersion)})`,
|
|
};
|
|
}),
|
|
});
|
|
|
|
console.log(
|
|
`Starting upgrade of ${styleText('cyan', `${approvedDeps.length}`)} dependencies`
|
|
);
|
|
|
|
for (const dep of upgradeableDeps) {
|
|
try {
|
|
if (!approvedDeps.includes(dep.name)) {
|
|
console.log(`Skipping ${dep.name}`);
|
|
continue;
|
|
}
|
|
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
const gitStatusBefore = await execa('git', ['status', '--porcelain']);
|
|
if (gitStatusBefore.stdout.trim() !== '') {
|
|
console.error(styleText('red', 'Found uncommitted changes, exiting'));
|
|
console.error(styleText('red', gitStatusBefore.stdout));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
`Upgrading ${styleText('cyan', dep.name)} ` +
|
|
`from ${styleText('yellow', dep.resolvedVersion)} ` +
|
|
`to ${styleText('magenta', dep.latestVersion)}`
|
|
);
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
await execa(
|
|
'npm',
|
|
['install', '--save-exact', `${dep.name}@${dep.latestVersion}`],
|
|
{ stdio: 'inherit' }
|
|
);
|
|
|
|
/** @type {{ npmScriptsToRun: Array<string> }} */
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
const { npmScriptsToRun } = await enquirer.prompt({
|
|
type: 'multiselect',
|
|
name: 'npmScriptsToRun',
|
|
message: 'Select which scripts to run',
|
|
choices: [
|
|
// Fast and common
|
|
{ name: 'oxlint' },
|
|
{ name: 'test-node' },
|
|
{ name: 'test-electron' },
|
|
// Long
|
|
{ name: 'test-mock' },
|
|
// Uncommon
|
|
{ name: 'test-oxlint' },
|
|
{ name: 'test-lint-intl' },
|
|
],
|
|
});
|
|
|
|
const allNpmScriptToRun = [
|
|
// Mandatory
|
|
'generate',
|
|
'check:types',
|
|
'lint-deps',
|
|
// Optional
|
|
...npmScriptsToRun,
|
|
];
|
|
|
|
for (const script of allNpmScriptToRun) {
|
|
console.log(`Running ${styleText('cyan', `npm run ${script}`)}`);
|
|
|
|
// oxlint-disable-next-line no-constant-condition
|
|
while (true) {
|
|
try {
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
await execa('npm', ['run', script], { stdio: 'inherit' });
|
|
break;
|
|
} catch (error) {
|
|
console.log(
|
|
styleText(
|
|
'red',
|
|
`Failed to run ${script}, you could go make changes and try again`
|
|
)
|
|
);
|
|
|
|
/** @type {{ retry: boolean }} */
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
const { retry } = await enquirer.prompt({
|
|
type: 'confirm',
|
|
name: 'retry',
|
|
message: 'Retry running script?',
|
|
initial: true,
|
|
});
|
|
|
|
if (!retry) {
|
|
throw error;
|
|
} else {
|
|
console.log(`Retrying ${styleText('cyan', `npm run ${script}`)}`);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Changes after upgrade:');
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
await execa('git', ['status', '--porcelain'], { stdio: 'inherit' });
|
|
|
|
/** @type {{ commitChanges: boolean }} */
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
const { commitChanges } = await enquirer.prompt({
|
|
type: 'select',
|
|
name: 'commitChanges',
|
|
message: 'Commit these changes?',
|
|
choices: [
|
|
{ name: 'commit', message: 'Commit and continue', value: true },
|
|
{ name: 'revert', message: 'Revert and skip', value: false },
|
|
],
|
|
});
|
|
|
|
if (!commitChanges) {
|
|
console.log('Reverting changes, and skipping');
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
await execa('git', ['checkout', '.']);
|
|
continue;
|
|
}
|
|
|
|
console.log('Committing changes');
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
await execa('git', ['add', '.']);
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
await execa('git', [
|
|
'commit',
|
|
'-m',
|
|
`Upgrade ${dep.name} ${dep.depType} from ${dep.requestedVersion} to ${dep.latestVersion}`,
|
|
]);
|
|
} catch (error) {
|
|
console.error(styleText('red', error));
|
|
console.log(
|
|
styleText('red', `Failed to upgrade ${dep.name}, reverting and skipping`)
|
|
);
|
|
// oxlint-disable-next-line no-await-in-loop
|
|
await execa('git', ['checkout', '.']);
|
|
}
|
|
}
|