瀏覽代碼

Merge pull request #5431 from jasongrout/updatedep

Update the update-dependency script to accept regexes and version tags
Steven Silvester 6 年之前
父節點
當前提交
6e869ad222
共有 6 個文件被更改,包括 191 次插入34 次删除
  1. 1 1
      CONTRIBUTING.md
  2. 2 0
      buildutils/package.json
  3. 181 30
      buildutils/src/update-dependency.ts
  4. 2 2
      buildutils/src/utils.ts
  5. 1 1
      package.json
  6. 4 0
      yarn.lock

+ 1 - 1
CONTRIBUTING.md

@@ -304,7 +304,7 @@ jupyter lab --dev-mode --watch
 
 There is a range of build utilities for maintaining the repository.
 To get a suggested version for a library use `jlpm run get:dependency foo`.
-To update the version of a library across the repo use `jlpm run update:dependency foo@^x.x`.
+To update the version of a library across the repo use `jlpm run update:dependency foo ^latest`.
 To remove an unwanted dependency use `jlpm run remove:dependency foo`.
 
 The key utility is `jlpm run integrity`, which ensures the integrity of

+ 2 - 0
buildutils/package.json

@@ -39,11 +39,13 @@
   "dependencies": {
     "@phosphor/coreutils": "^1.3.0",
     "child_process": "~1.0.2",
+    "commander": "~2.18.0",
     "fs-extra": "~4.0.2",
     "glob": "~7.1.2",
     "inquirer": "~3.3.0",
     "package-json": "~5.0.0",
     "path": "~0.12.7",
+    "semver": "^5.5.0",
     "sort-package-json": "~1.7.1",
     "typescript": "~3.1.1"
   },

+ 181 - 30
buildutils/src/update-dependency.ts

@@ -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);
 }

+ 2 - 2
buildutils/src/utils.ts

@@ -8,8 +8,8 @@ import coreutils = require('@phosphor/coreutils');
 /**
  * Get all of the lerna package paths.
  */
-export function getLernaPaths(): string[] {
-  let basePath = path.resolve('.');
+export function getLernaPaths(basePath = '.'): string[] {
+  basePath = path.resolve(basePath);
   let baseConfig = require(path.join(basePath, 'package.json'));
   let paths: string[] = [];
   for (let config of baseConfig.workspaces) {

+ 1 - 1
package.json

@@ -51,7 +51,7 @@
     "tslint:check": "tslint -c tslint.json '**/*{.ts,.tsx}'",
     "prettier": "prettier --write '**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}'",
     "prettier:check": "prettier --list-different '**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}'",
-    "update:dependency": "node buildutils/lib/update-dependency.js",
+    "update:dependency": "node buildutils/lib/update-dependency.js --lerna",
     "update:dist-tag": "node buildutils/lib/update-dist-tag.js",
     "update:local": "node buildutils/lib/update-local.js",
     "watch": "run-p watch:dev watch:themes",

+ 4 - 0
yarn.lock

@@ -2410,6 +2410,10 @@ commander@~2.13.0:
   version "2.13.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
 
+commander@~2.18.0:
+  version "2.18.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970"
+
 comment-json@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-1.1.3.tgz#6986c3330fee0c4c9e00c2398cd61afa5d8f239e"