|
@@ -6,44 +6,90 @@
|
|
|
|
|
|
import * as path from 'path';
|
|
|
import * as utils from './utils';
|
|
|
-import packageJson = require('package-json');
|
|
|
+import packageJson from 'package-json';
|
|
|
|
|
|
-// Make sure we have required command line arguments.
|
|
|
-if (process.argv.length !== 3) {
|
|
|
- let msg = '** Must supply an update specifier\n';
|
|
|
- process.stderr.write(msg);
|
|
|
- process.exit(1);
|
|
|
-}
|
|
|
+import commander from 'commander';
|
|
|
+import semver from 'semver';
|
|
|
+
|
|
|
+let versionCache = new Map();
|
|
|
+const tags = /^([~^]?)([\w.]*)$/;
|
|
|
|
|
|
-// Extract the desired library target and specifier.
|
|
|
-let parts = process.argv[2].slice(1).split('@');
|
|
|
-parts[0] = process.argv[2][0] + parts[0];
|
|
|
+async function getVersion(pkg: string, specifier: string) {
|
|
|
+ let key = JSON.stringify([pkg, specifier]);
|
|
|
+ if (versionCache.has(key)) {
|
|
|
+ return versionCache.get(key);
|
|
|
+ }
|
|
|
+ if (semver.validRange(specifier) === null) {
|
|
|
+ // We have a tag, with possibly a range specifier, such as ^latest
|
|
|
+ let match = specifier.match(tags);
|
|
|
+ if (match === null) {
|
|
|
+ throw Error(`Invalid version specifier: ${specifier}`);
|
|
|
+ }
|
|
|
|
|
|
-// Handle the version.
|
|
|
-if (parts.length === 1) {
|
|
|
- parts.push('latest');
|
|
|
+ // Look up the actual version corresponding to the tag
|
|
|
+ let { version } = await packageJson(pkg, { version: match[2] });
|
|
|
+ specifier = match[1] + version;
|
|
|
+ }
|
|
|
+ versionCache.set(key, specifier);
|
|
|
+ return specifier;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * A very simple subset comparator
|
|
|
+ *
|
|
|
+ * @returns true if we can determine if range1 is a subset of range2, otherwise false
|
|
|
+ *
|
|
|
+ * #### Notes
|
|
|
+ * This will not be able to determine if range1 is a subset of range2 in many cases.
|
|
|
+ */
|
|
|
+function subset(range1: string, range2: string): boolean {
|
|
|
+ try {
|
|
|
+ const [, r1, version1] = range1.match(tags);
|
|
|
+ const [, r2] = range2.match(tags);
|
|
|
+ return (
|
|
|
+ ['', '~', '^'].indexOf(r1) >= 0 &&
|
|
|
+ r1 === r2 &&
|
|
|
+ semver.valid(version1) &&
|
|
|
+ semver.satisfies(version1, range2)
|
|
|
+ );
|
|
|
+ } catch (e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
}
|
|
|
-packageJson(parts[0], { version: parts[1] }).then(data => {
|
|
|
- handlePackages(data.name, `~${data.version}`);
|
|
|
-});
|
|
|
|
|
|
-// Handle the packages
|
|
|
-function handlePackages(name: string, specifier: string) {
|
|
|
- utils.getLernaPaths().forEach(pkgPath => {
|
|
|
- handlePackage(name, specifier, pkgPath);
|
|
|
- });
|
|
|
- handlePackage(name, specifier, path.resolve('.'));
|
|
|
- utils.run('yarn');
|
|
|
+async function handleDependency(
|
|
|
+ dependencies: { [key: string]: string },
|
|
|
+ dep: string,
|
|
|
+ specifier: string,
|
|
|
+ minimal: boolean
|
|
|
+): Promise<{ updated: boolean; log: string[] }> {
|
|
|
+ let log = [];
|
|
|
+ let updated = false;
|
|
|
+ let newRange = await getVersion(dep, specifier);
|
|
|
+ let oldRange = dependencies[dep];
|
|
|
+ if (minimal && subset(newRange, oldRange)) {
|
|
|
+ log.push(`SKIPPING ${dep} ${oldRange} -> ${newRange}`);
|
|
|
+ } else {
|
|
|
+ log.push(`${dep} ${oldRange} -> ${newRange}`);
|
|
|
+ dependencies[dep] = newRange;
|
|
|
+ updated = true;
|
|
|
+ }
|
|
|
+ return { updated, log };
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Handle an individual package on the path - update the dependency.
|
|
|
*/
|
|
|
-function handlePackage(
|
|
|
- name: string,
|
|
|
+async function handlePackage(
|
|
|
+ name: string | RegExp,
|
|
|
specifier: string,
|
|
|
- packagePath: string
|
|
|
-): void {
|
|
|
+ packagePath: string,
|
|
|
+ dryRun = false,
|
|
|
+ minimal = false
|
|
|
+) {
|
|
|
+ let fileUpdated = false;
|
|
|
+ let fileLog: string[] = [];
|
|
|
+
|
|
|
// Read in the package.json.
|
|
|
packagePath = path.join(packagePath, 'package.json');
|
|
|
let data: any;
|
|
@@ -57,11 +103,116 @@ function handlePackage(
|
|
|
// Update dependencies as appropriate.
|
|
|
for (let dtype of ['dependencies', 'devDependencies']) {
|
|
|
let deps = data[dtype] || {};
|
|
|
- if (name in deps) {
|
|
|
- deps[name] = specifier;
|
|
|
+ if (typeof name === 'string') {
|
|
|
+ let dep = name;
|
|
|
+ if (dep in deps) {
|
|
|
+ let { updated, log } = await handleDependency(
|
|
|
+ deps,
|
|
|
+ dep,
|
|
|
+ specifier,
|
|
|
+ minimal
|
|
|
+ );
|
|
|
+ if (updated) {
|
|
|
+ fileUpdated = true;
|
|
|
+ }
|
|
|
+ fileLog.push(...log);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ let keys = Object.keys(deps);
|
|
|
+ keys.sort();
|
|
|
+ for (let dep of keys) {
|
|
|
+ if (dep.match(name)) {
|
|
|
+ let { updated, log } = await handleDependency(
|
|
|
+ deps,
|
|
|
+ dep,
|
|
|
+ specifier,
|
|
|
+ minimal
|
|
|
+ );
|
|
|
+ if (updated) {
|
|
|
+ fileUpdated = true;
|
|
|
+ }
|
|
|
+ fileLog.push(...log);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ if (fileLog.length > 0) {
|
|
|
+ console.log(packagePath);
|
|
|
+ console.log(fileLog.join('\n'));
|
|
|
+ console.log();
|
|
|
+ }
|
|
|
+
|
|
|
// Write the file back to disk.
|
|
|
- utils.writePackageData(packagePath, data);
|
|
|
+ if (!dryRun && fileUpdated) {
|
|
|
+ utils.writePackageData(packagePath, data);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+let run = false;
|
|
|
+
|
|
|
+commander
|
|
|
+ .description('Update dependency versions')
|
|
|
+ .usage('[options] <package> [versionspec], versionspec defaults to ^latest')
|
|
|
+ .option('--dry-run', 'Do not perform actions, just print output')
|
|
|
+ .option('--regex', 'Package is a regular expression')
|
|
|
+ .option('--lerna', 'Update dependencies in all lerna packages')
|
|
|
+ .option('--path [path]', 'Path to package or monorepo to update')
|
|
|
+ .option('--minimal', 'only update if the change is substantial')
|
|
|
+ .arguments('<package> [versionspec]')
|
|
|
+ .action(
|
|
|
+ async (name: string | RegExp, version: string = '^latest', args: any) => {
|
|
|
+ run = true;
|
|
|
+ let basePath = path.resolve(args.path || '.');
|
|
|
+ let pkg = args.regex ? new RegExp(name) : name;
|
|
|
+
|
|
|
+ if (args.lerna) {
|
|
|
+ let paths = utils.getLernaPaths(basePath);
|
|
|
+ paths.sort();
|
|
|
+ for (let pkgPath of paths) {
|
|
|
+ await handlePackage(pkg, version, pkgPath, args.dryRun, args.minimal);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ await handlePackage(pkg, version, basePath, args.dryRun, args.minimal);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+commander.on('--help', function() {
|
|
|
+ console.log(`
|
|
|
+Examples
|
|
|
+--------
|
|
|
+
|
|
|
+ Update the package 'webpack' to a specific version range:
|
|
|
+
|
|
|
+ update-dependency webpack ^4.0.0
|
|
|
+
|
|
|
+ Update all packages to the latest version, with a caret.
|
|
|
+ Only update if the update is substantial:
|
|
|
+
|
|
|
+ update-dependency --minimal --regex '.*' ^latest
|
|
|
+
|
|
|
+ Print the log of the above without actually making any changes.
|
|
|
+
|
|
|
+ update-dependency --dry-run --minimal --regex '.*' ^latest
|
|
|
+
|
|
|
+ Update all packages starting with '@jupyterlab/' to the version
|
|
|
+ the 'latest' tag currently points to, with a caret range:
|
|
|
+
|
|
|
+ update-dependency --regex '^@jupyterlab/' ^latest
|
|
|
+
|
|
|
+ Update all packages starting with '@jupyterlab/' in all lerna
|
|
|
+ workspaces and the root package.json to whatever version the 'next'
|
|
|
+ tag for each package currently points to (with a caret tag).
|
|
|
+ Update the version range only if the change is substantial.
|
|
|
+
|
|
|
+ update-dependency --lerna --regex --minimal '^@jupyterlab/' ^next
|
|
|
+`);
|
|
|
+});
|
|
|
+commander.parse(process.argv);
|
|
|
+
|
|
|
+if (!run) {
|
|
|
+ console.error(`
|
|
|
+ error: missing required argument 'package'
|
|
|
+ `);
|
|
|
+ process.exit(1);
|
|
|
}
|