UNPKG

8.97 kBJavaScriptView Raw
1#!/usr/bin/env node
2"use strict";
3/* -----------------------------------------------------------------------------
4| Copyright (c) Jupyter Development Team.
5| Distributed under the terms of the Modified BSD License.
6|----------------------------------------------------------------------------*/
7var __importStar = (this && this.__importStar) || function (mod) {
8 if (mod && mod.__esModule) return mod;
9 var result = {};
10 if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
11 result["default"] = mod;
12 return result;
13};
14var __importDefault = (this && this.__importDefault) || function (mod) {
15 return (mod && mod.__esModule) ? mod : { "default": mod };
16};
17Object.defineProperty(exports, "__esModule", { value: true });
18const path = __importStar(require("path"));
19const utils = __importStar(require("./utils"));
20const package_json_1 = __importDefault(require("package-json"));
21const commander_1 = __importDefault(require("commander"));
22const semver_1 = __importDefault(require("semver"));
23const versionCache = new Map();
24/**
25 * Matches a simple semver range, where the version number could be an npm tag.
26 */
27const SEMVER_RANGE = /^(~|\^|=|<|>|<=|>=)?([\w\-.]*)$/;
28/**
29 * Get the specifier we should use
30 *
31 * @param currentSpecifier - The current package version.
32 * @param suggestedSpecifier - The package version we would like to use.
33 *
34 * #### Notes
35 * If the suggested specifier is not a valid range, we assume it is of the
36 * form ${RANGE}${TAG}, where TAG is an npm tag (such as 'latest') and RANGE
37 * is either a semver range indicator (one of `~, ^, >, <, =, >=, <=`), or is
38 * not given (in which case the current specifier range prefix is used).
39 */
40async function getSpecifier(currentSpecifier, suggestedSpecifier) {
41 var _a;
42 if (semver_1.default.validRange(suggestedSpecifier)) {
43 return suggestedSpecifier;
44 }
45 // The suggested specifier is not a valid range, so we assume it
46 // references a tag
47 let [, suggestedSigil, suggestedTag] = (_a = suggestedSpecifier.match(SEMVER_RANGE), (_a !== null && _a !== void 0 ? _a : []));
48 if (!suggestedTag) {
49 throw Error(`Invalid version specifier: ${suggestedSpecifier}`);
50 }
51 // A tag with no sigil adopts the sigil from the current specification
52 if (!suggestedSigil) {
53 const match = currentSpecifier.match(SEMVER_RANGE);
54 if (match === null) {
55 throw Error(`Current version range is not recognized: ${currentSpecifier}`);
56 }
57 const [, currentSigil] = match;
58 if (currentSigil) {
59 suggestedSigil = currentSigil;
60 }
61 }
62 return `${suggestedSigil}${suggestedTag}`;
63}
64async function getVersion(pkg, specifier) {
65 const key = JSON.stringify([pkg, specifier]);
66 if (versionCache.has(key)) {
67 return versionCache.get(key);
68 }
69 if (semver_1.default.validRange(specifier) === null) {
70 // We have a tag, with possibly a range specifier, such as ^latest
71 const match = specifier.match(SEMVER_RANGE);
72 if (match === null) {
73 throw Error(`Invalid version specifier: ${specifier}`);
74 }
75 // Look up the actual version corresponding to the tag
76 const { version } = await package_json_1.default(pkg, { version: match[2] });
77 specifier = match[1] + version;
78 }
79 versionCache.set(key, specifier);
80 return specifier;
81}
82/**
83 * A very simple subset comparator
84 *
85 * @returns true if we can determine if range1 is a subset of range2, otherwise false
86 *
87 * #### Notes
88 * This will not be able to determine if range1 is a subset of range2 in many cases.
89 */
90function subset(range1, range2) {
91 var _a, _b;
92 try {
93 const [, r1, version1] = (_a = range1.match(SEMVER_RANGE), (_a !== null && _a !== void 0 ? _a : []));
94 const [, r2] = (_b = range2.match(SEMVER_RANGE), (_b !== null && _b !== void 0 ? _b : []));
95 return (['', '>=', '=', '~', '^'].includes(r1) &&
96 r1 === r2 &&
97 !!semver_1.default.valid(version1) &&
98 semver_1.default.satisfies(version1, range2));
99 }
100 catch (e) {
101 return false;
102 }
103}
104async function handleDependency(dependencies, dep, suggestedSpecifier, minimal) {
105 const log = [];
106 let updated = false;
107 const oldRange = dependencies[dep];
108 const specifier = await getSpecifier(oldRange, suggestedSpecifier);
109 const newRange = await getVersion(dep, specifier);
110 if (minimal && subset(newRange, oldRange)) {
111 log.push(`SKIPPING ${dep} ${oldRange} -> ${newRange}`);
112 }
113 else {
114 log.push(`${dep} ${oldRange} -> ${newRange}`);
115 dependencies[dep] = newRange;
116 updated = true;
117 }
118 return { updated, log };
119}
120/**
121 * Handle an individual package on the path - update the dependency.
122 */
123async function handlePackage(name, specifier, packagePath, dryRun = false, minimal = false) {
124 let fileUpdated = false;
125 const fileLog = [];
126 // Read in the package.json.
127 packagePath = path.join(packagePath, 'package.json');
128 let data;
129 try {
130 data = utils.readJSONFile(packagePath);
131 }
132 catch (e) {
133 console.debug('Skipping package ' + packagePath);
134 return;
135 }
136 // Update dependencies as appropriate.
137 for (const dtype of ['dependencies', 'devDependencies']) {
138 const deps = data[dtype] || {};
139 if (typeof name === 'string') {
140 const dep = name;
141 if (dep in deps) {
142 const { updated, log } = await handleDependency(deps, dep, specifier, minimal);
143 if (updated) {
144 fileUpdated = true;
145 }
146 fileLog.push(...log);
147 }
148 }
149 else {
150 const keys = Object.keys(deps);
151 keys.sort();
152 for (const dep of keys) {
153 if (dep.match(name)) {
154 const { updated, log } = await handleDependency(deps, dep, specifier, minimal);
155 if (updated) {
156 fileUpdated = true;
157 }
158 fileLog.push(...log);
159 }
160 }
161 }
162 }
163 if (fileLog.length > 0) {
164 console.debug(packagePath);
165 console.debug(fileLog.join('\n'));
166 console.debug();
167 }
168 // Write the file back to disk.
169 if (!dryRun && fileUpdated) {
170 utils.writePackageData(packagePath, data);
171 }
172}
173commander_1.default
174 .description('Update dependency versions')
175 .usage('[options] <package> [versionspec], versionspec defaults to ^latest')
176 .option('--dry-run', 'Do not perform actions, just print output')
177 .option('--regex', 'Package is a regular expression')
178 .option('--lerna', 'Update dependencies in all lerna packages')
179 .option('--path <path>', 'Path to package or monorepo to update')
180 .option('--minimal', 'only update if the change is substantial')
181 .arguments('<package> [versionspec]')
182 .action(async (name, version = '^latest', args) => {
183 const basePath = path.resolve(args.path || '.');
184 const pkg = args.regex ? new RegExp(name) : name;
185 if (args.lerna) {
186 const paths = utils.getLernaPaths(basePath).sort();
187 // We use a loop instead of Promise.all so that the output is in
188 // alphabetical order.
189 for (const pkgPath of paths) {
190 await handlePackage(pkg, version, pkgPath, args.dryRun, args.minimal);
191 }
192 }
193 await handlePackage(pkg, version, basePath, args.dryRun, args.minimal);
194});
195commander_1.default.on('--help', function () {
196 console.debug(`
197Examples
198--------
199
200 Update the package 'webpack' to a specific version range:
201
202 update-dependency webpack ^4.0.0
203
204 Update all packages to the latest version, with a caret.
205 Only update if the update is substantial:
206
207 update-dependency --minimal --regex '.*' ^latest
208
209 Update all packages, that does not start with '@jupyterlab',
210 to the latest version and use the same version specifier currently
211 being used
212
213 update:dependency --regex '^(?!@jupyterlab).*' latest --dry-run
214
215 Print the log of the above without actually making any changes.
216
217 update-dependency --dry-run --minimal --regex '.*' ^latest
218
219 Update all packages starting with '@jupyterlab/' to the version
220 the 'latest' tag currently points to, with a caret range:
221
222 update-dependency --regex '^@jupyterlab/' ^latest
223
224 Update all packages starting with '@jupyterlab/' in all lerna
225 workspaces and the root package.json to whatever version the 'next'
226 tag for each package currently points to (with a caret tag).
227 Update the version range only if the change is substantial.
228
229 update-dependency --lerna --regex --minimal '^@jupyterlab/' ^next
230`);
231});
232commander_1.default.parse(process.argv);
233// If no arguments supplied
234if (!process.argv.slice(2).length) {
235 commander_1.default.outputHelp();
236 process.exit(1);
237}
238//# sourceMappingURL=update-dependency.js.map
\No newline at end of file