UNPKG

29.9 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.UpdateCommand = void 0;
4/**
5 * @license
6 * Copyright Google Inc. All Rights Reserved.
7 *
8 * Use of this source code is governed by an MIT-style license that can be
9 * found in the LICENSE file at https://angular.io/license
10 */
11const core_1 = require("@angular-devkit/core");
12const node_1 = require("@angular-devkit/core/node");
13const schematics_1 = require("@angular-devkit/schematics");
14const tools_1 = require("@angular-devkit/schematics/tools");
15const child_process_1 = require("child_process");
16const fs = require("fs");
17const path = require("path");
18const semver = require("semver");
19const schema_1 = require("../lib/config/schema");
20const command_1 = require("../models/command");
21const install_package_1 = require("../tasks/install-package");
22const color_1 = require("../utilities/color");
23const log_file_1 = require("../utilities/log-file");
24const package_manager_1 = require("../utilities/package-manager");
25const package_metadata_1 = require("../utilities/package-metadata");
26const package_tree_1 = require("../utilities/package-tree");
27const npa = require('npm-package-arg');
28const pickManifest = require('npm-pick-manifest');
29const oldConfigFileNames = ['.angular-cli.json', 'angular-cli.json'];
30const NG_VERSION_9_POST_MSG = color_1.colors.cyan('\nYour project has been updated to Angular version 9!\n' +
31 'For more info, please see: https://v9.angular.io/guide/updating-to-version-9');
32/**
33 * Disable CLI version mismatch checks and forces usage of the invoked CLI
34 * instead of invoking the local installed version.
35 */
36const disableVersionCheckEnv = process.env['NG_DISABLE_VERSION_CHECK'];
37const disableVersionCheck = disableVersionCheckEnv !== undefined &&
38 disableVersionCheckEnv !== '0' &&
39 disableVersionCheckEnv.toLowerCase() !== 'false';
40class UpdateCommand extends command_1.Command {
41 constructor() {
42 super(...arguments);
43 this.allowMissingWorkspace = true;
44 this.packageManager = schema_1.PackageManager.Npm;
45 }
46 async initialize() {
47 this.packageManager = await package_manager_1.getPackageManager(this.workspace.root);
48 this.workflow = new tools_1.NodeWorkflow(new core_1.virtualFs.ScopedHost(new node_1.NodeJsSyncHost(), core_1.normalize(this.workspace.root)), {
49 packageManager: this.packageManager,
50 root: core_1.normalize(this.workspace.root),
51 // __dirname -> favor @schematics/update from this package
52 // Otherwise, use packages from the active workspace (migrations)
53 resolvePaths: [__dirname, this.workspace.root],
54 });
55 this.workflow.engineHost.registerOptionsTransform(tools_1.validateOptionsWithSchema(this.workflow.registry));
56 }
57 async executeSchematic(collection, schematic, options = {}) {
58 let error = false;
59 let logs = [];
60 const files = new Set();
61 const reporterSubscription = this.workflow.reporter.subscribe(event => {
62 // Strip leading slash to prevent confusion.
63 const eventPath = event.path.startsWith('/') ? event.path.substr(1) : event.path;
64 switch (event.kind) {
65 case 'error':
66 error = true;
67 const desc = event.description == 'alreadyExist' ? 'already exists' : 'does not exist.';
68 this.logger.error(`ERROR! ${eventPath} ${desc}.`);
69 break;
70 case 'update':
71 logs.push(`${color_1.colors.whiteBright('UPDATE')} ${eventPath} (${event.content.length} bytes)`);
72 files.add(eventPath);
73 break;
74 case 'create':
75 logs.push(`${color_1.colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)`);
76 files.add(eventPath);
77 break;
78 case 'delete':
79 logs.push(`${color_1.colors.yellow('DELETE')} ${eventPath}`);
80 files.add(eventPath);
81 break;
82 case 'rename':
83 const eventToPath = event.to.startsWith('/') ? event.to.substr(1) : event.to;
84 logs.push(`${color_1.colors.blue('RENAME')} ${eventPath} => ${eventToPath}`);
85 files.add(eventPath);
86 break;
87 }
88 });
89 const lifecycleSubscription = this.workflow.lifeCycle.subscribe(event => {
90 if (event.kind == 'end' || event.kind == 'post-tasks-start') {
91 if (!error) {
92 // Output the logging queue, no error happened.
93 logs.forEach(log => this.logger.info(log));
94 logs = [];
95 }
96 }
97 });
98 // TODO: Allow passing a schematic instance directly
99 try {
100 await this.workflow
101 .execute({
102 collection,
103 schematic,
104 options,
105 logger: this.logger,
106 })
107 .toPromise();
108 reporterSubscription.unsubscribe();
109 lifecycleSubscription.unsubscribe();
110 return { success: !error, files };
111 }
112 catch (e) {
113 if (e instanceof schematics_1.UnsuccessfulWorkflowExecution) {
114 this.logger.error(`${color_1.colors.symbols.cross} Migration failed. See above for further details.\n`);
115 }
116 else {
117 const logPath = log_file_1.writeErrorToLogFile(e);
118 this.logger.fatal(`${color_1.colors.symbols.cross} Migration failed: ${e.message}\n` +
119 ` See "${logPath}" for further details.\n`);
120 }
121 return { success: false, files };
122 }
123 }
124 /**
125 * @return Whether or not the migration was performed successfully.
126 */
127 async executeMigration(packageName, collectionPath, migrationName, commit) {
128 const collection = this.workflow.engine.createCollection(collectionPath);
129 const name = collection.listSchematicNames().find(name => name === migrationName);
130 if (!name) {
131 this.logger.error(`Cannot find migration '${migrationName}' in '${packageName}'.`);
132 return false;
133 }
134 const schematic = this.workflow.engine.createSchematic(name, collection);
135 this.logger.info(color_1.colors.cyan(`** Executing '${migrationName}' of package '${packageName}' **\n`));
136 return this.executePackageMigrations([schematic.description], packageName, commit);
137 }
138 /**
139 * @return Whether or not the migrations were performed successfully.
140 */
141 async executeMigrations(packageName, collectionPath, range, commit) {
142 const collection = this.workflow.engine.createCollection(collectionPath);
143 const migrations = [];
144 for (const name of collection.listSchematicNames()) {
145 const schematic = this.workflow.engine.createSchematic(name, collection);
146 const description = schematic.description;
147 description.version = coerceVersionNumber(description.version) || undefined;
148 if (!description.version) {
149 continue;
150 }
151 if (semver.satisfies(description.version, range, { includePrerelease: true })) {
152 migrations.push(description);
153 }
154 }
155 migrations.sort((a, b) => semver.compare(a.version, b.version) || a.name.localeCompare(b.name));
156 if (migrations.length === 0) {
157 return true;
158 }
159 this.logger.info(color_1.colors.cyan(`** Executing migrations of package '${packageName}' **\n`));
160 return this.executePackageMigrations(migrations, packageName, commit);
161 }
162 async executePackageMigrations(migrations, packageName, commit = false) {
163 for (const migration of migrations) {
164 this.logger.info(`${color_1.colors.symbols.pointer} ${migration.description.replace(/\. /g, '.\n ')}`);
165 const result = await this.executeSchematic(migration.collection.name, migration.name);
166 if (!result.success) {
167 return false;
168 }
169 this.logger.info(' Migration completed.');
170 // Commit migration
171 if (commit) {
172 const commitPrefix = `${packageName} migration - ${migration.name}`;
173 const commitMessage = migration.description
174 ? `${commitPrefix}\n${migration.description}`
175 : commitPrefix;
176 const committed = this.commit(commitMessage);
177 if (!committed) {
178 // Failed to commit, something went wrong. Abort the update.
179 return false;
180 }
181 }
182 this.logger.info(''); // Extra trailing newline.
183 }
184 return true;
185 }
186 // tslint:disable-next-line:no-big-function
187 async run(options) {
188 // Check if the @angular-devkit/schematics package can be resolved from the workspace root
189 // This works around issues with packages containing migrations that cannot directly depend on the package
190 // This check can be removed once the schematic runtime handles this situation
191 try {
192 require.resolve('@angular-devkit/schematics', { paths: [this.workspace.root] });
193 }
194 catch (e) {
195 if (e.code === 'MODULE_NOT_FOUND') {
196 this.logger.fatal('The "@angular-devkit/schematics" package cannot be resolved from the workspace root directory. ' +
197 'This may be due to an unsupported node modules structure.\n' +
198 'Please remove both the "node_modules" directory and the package lock file; and then reinstall.\n' +
199 'If this does not correct the problem, ' +
200 'please temporarily install the "@angular-devkit/schematics" package within the workspace. ' +
201 'It can be removed once the update is complete.');
202 return 1;
203 }
204 throw e;
205 }
206 // Check if the current installed CLI version is older than the latest version.
207 if (!disableVersionCheck && await this.checkCLILatestVersion(options.verbose, options.next)) {
208 this.logger.warn(`The installed local Angular CLI version is older than the latest ${options.next ? 'pre-release' : 'stable'} version.\n` +
209 'Installing a temporary version to perform the update.');
210 return install_package_1.runTempPackageBin(`@angular/cli@${options.next ? 'next' : 'latest'}`, this.logger, this.packageManager, process.argv.slice(2));
211 }
212 const packages = [];
213 for (const request of options['--'] || []) {
214 try {
215 const packageIdentifier = npa(request);
216 // only registry identifiers are supported
217 if (!packageIdentifier.registry) {
218 this.logger.error(`Package '${request}' is not a registry package identifer.`);
219 return 1;
220 }
221 if (packages.some(v => v.name === packageIdentifier.name)) {
222 this.logger.error(`Duplicate package '${packageIdentifier.name}' specified.`);
223 return 1;
224 }
225 if (options.migrateOnly && packageIdentifier.rawSpec) {
226 this.logger.warn('Package specifier has no effect when using "migrate-only" option.');
227 }
228 // If next option is used and no specifier supplied, use next tag
229 if (options.next && !packageIdentifier.rawSpec) {
230 packageIdentifier.fetchSpec = 'next';
231 }
232 packages.push(packageIdentifier);
233 }
234 catch (e) {
235 this.logger.error(e.message);
236 return 1;
237 }
238 }
239 if (options.all && packages.length > 0) {
240 this.logger.error('Cannot specify packages when using the "all" option.');
241 return 1;
242 }
243 else if (options.all && options.migrateOnly) {
244 this.logger.error('Cannot use "all" option with "migrate-only" option.');
245 return 1;
246 }
247 else if (!options.migrateOnly && (options.from || options.to)) {
248 this.logger.error('Can only use "from" or "to" options with "migrate-only" option.');
249 return 1;
250 }
251 // If not asking for status then check for a clean git repository.
252 // This allows the user to easily reset any changes from the update.
253 const statusCheck = packages.length === 0 && !options.all;
254 if (!statusCheck && !this.checkCleanGit()) {
255 if (options.allowDirty) {
256 this.logger.warn('Repository is not clean. Update changes will be mixed with pre-existing changes.');
257 }
258 else {
259 this.logger.error('Repository is not clean. Please commit or stash any changes before updating.');
260 return 2;
261 }
262 }
263 this.logger.info(`Using package manager: '${this.packageManager}'`);
264 // Special handling for Angular CLI 1.x migrations
265 if (options.migrateOnly === undefined &&
266 options.from === undefined &&
267 !options.all &&
268 packages.length === 1 &&
269 packages[0].name === '@angular/cli' &&
270 this.workspace.configFile &&
271 oldConfigFileNames.includes(this.workspace.configFile)) {
272 options.migrateOnly = true;
273 options.from = '1.0.0';
274 }
275 this.logger.info('Collecting installed dependencies...');
276 const packageTree = await package_tree_1.readPackageTree(this.workspace.root);
277 const rootDependencies = package_tree_1.findNodeDependencies(packageTree);
278 this.logger.info(`Found ${Object.keys(rootDependencies).length} dependencies.`);
279 if (options.all) {
280 // 'all' option and a zero length packages have already been checked.
281 // Add all direct dependencies to be updated
282 for (const dep of Object.keys(rootDependencies)) {
283 const packageIdentifier = npa(dep);
284 if (options.next) {
285 packageIdentifier.fetchSpec = 'next';
286 }
287 packages.push(packageIdentifier);
288 }
289 }
290 else if (packages.length === 0) {
291 // Show status
292 const { success } = await this.executeSchematic('@schematics/update', 'update', {
293 force: options.force || false,
294 next: options.next || false,
295 verbose: options.verbose || false,
296 packageManager: this.packageManager,
297 packages: options.all ? Object.keys(rootDependencies) : [],
298 });
299 return success ? 0 : 1;
300 }
301 if (options.migrateOnly) {
302 if (!options.from && typeof options.migrateOnly !== 'string') {
303 this.logger.error('"from" option is required when using the "migrate-only" option without a migration name.');
304 return 1;
305 }
306 else if (packages.length !== 1) {
307 this.logger.error('A single package must be specified when using the "migrate-only" option.');
308 return 1;
309 }
310 if (options.next) {
311 this.logger.warn('"next" option has no effect when using "migrate-only" option.');
312 }
313 const packageName = packages[0].name;
314 const packageDependency = rootDependencies[packageName];
315 let packageNode = packageDependency && packageDependency.node;
316 if (packageDependency && !packageNode) {
317 this.logger.error('Package found in package.json but is not installed.');
318 return 1;
319 }
320 else if (!packageDependency) {
321 // Allow running migrations on transitively installed dependencies
322 // There can technically be nested multiple versions
323 // TODO: If multiple, this should find all versions and ask which one to use
324 const child = packageTree.children.find(c => c.name === packageName);
325 if (child) {
326 packageNode = child;
327 }
328 }
329 if (!packageNode) {
330 this.logger.error('Package is not installed.');
331 return 1;
332 }
333 const updateMetadata = packageNode.package['ng-update'];
334 let migrations = updateMetadata && updateMetadata.migrations;
335 if (migrations === undefined) {
336 this.logger.error('Package does not provide migrations.');
337 return 1;
338 }
339 else if (typeof migrations !== 'string') {
340 this.logger.error('Package contains a malformed migrations field.');
341 return 1;
342 }
343 else if (path.posix.isAbsolute(migrations) || path.win32.isAbsolute(migrations)) {
344 this.logger.error('Package contains an invalid migrations field. Absolute paths are not permitted.');
345 return 1;
346 }
347 // Normalize slashes
348 migrations = migrations.replace(/\\/g, '/');
349 if (migrations.startsWith('../')) {
350 this.logger.error('Package contains an invalid migrations field. ' +
351 'Paths outside the package root are not permitted.');
352 return 1;
353 }
354 // Check if it is a package-local location
355 const localMigrations = path.join(packageNode.path, migrations);
356 if (fs.existsSync(localMigrations)) {
357 migrations = localMigrations;
358 }
359 else {
360 // Try to resolve from package location.
361 // This avoids issues with package hoisting.
362 try {
363 migrations = require.resolve(migrations, { paths: [packageNode.path] });
364 }
365 catch (e) {
366 if (e.code === 'MODULE_NOT_FOUND') {
367 this.logger.error('Migrations for package were not found.');
368 }
369 else {
370 this.logger.error(`Unable to resolve migrations for package. [${e.message}]`);
371 }
372 return 1;
373 }
374 }
375 let success = false;
376 if (typeof options.migrateOnly == 'string') {
377 success = await this.executeMigration(packageName, migrations, options.migrateOnly, options.createCommits);
378 }
379 else {
380 const from = coerceVersionNumber(options.from);
381 if (!from) {
382 this.logger.error(`"from" value [${options.from}] is not a valid version.`);
383 return 1;
384 }
385 const migrationRange = new semver.Range('>' + from + ' <=' + (options.to || packageNode.package.version));
386 success = await this.executeMigrations(packageName, migrations, migrationRange, options.createCommits);
387 }
388 if (success) {
389 if (packageName === '@angular/core'
390 && options.from
391 && +options.from.split('.')[0] < 9
392 && (options.to || packageNode.package.version).split('.')[0] === '9') {
393 this.logger.info(NG_VERSION_9_POST_MSG);
394 }
395 return 0;
396 }
397 return 1;
398 }
399 const requests = [];
400 // Validate packages actually are part of the workspace
401 for (const pkg of packages) {
402 const node = rootDependencies[pkg.name] && rootDependencies[pkg.name].node;
403 if (!node) {
404 this.logger.error(`Package '${pkg.name}' is not a dependency.`);
405 return 1;
406 }
407 // If a specific version is requested and matches the installed version, skip.
408 if (pkg.type === 'version' && node.package.version === pkg.fetchSpec) {
409 this.logger.info(`Package '${pkg.name}' is already at '${pkg.fetchSpec}'.`);
410 continue;
411 }
412 requests.push({ identifier: pkg, node });
413 }
414 if (requests.length === 0) {
415 return 0;
416 }
417 const packagesToUpdate = [];
418 this.logger.info('Fetching dependency metadata from registry...');
419 for (const { identifier: requestIdentifier, node } of requests) {
420 const packageName = requestIdentifier.name;
421 let metadata;
422 try {
423 // Metadata requests are internally cached; multiple requests for same name
424 // does not result in additional network traffic
425 metadata = await package_metadata_1.fetchPackageMetadata(packageName, this.logger, {
426 verbose: options.verbose,
427 });
428 }
429 catch (e) {
430 this.logger.error(`Error fetching metadata for '${packageName}': ` + e.message);
431 return 1;
432 }
433 // Try to find a package version based on the user requested package specifier
434 // registry specifier types are either version, range, or tag
435 let manifest;
436 if (requestIdentifier.type === 'version' ||
437 requestIdentifier.type === 'range' ||
438 requestIdentifier.type === 'tag') {
439 try {
440 manifest = pickManifest(metadata, requestIdentifier.fetchSpec);
441 }
442 catch (e) {
443 if (e.code === 'ETARGET') {
444 // If not found and next was used and user did not provide a specifier, try latest.
445 // Package may not have a next tag.
446 if (requestIdentifier.type === 'tag' &&
447 requestIdentifier.fetchSpec === 'next' &&
448 !requestIdentifier.rawSpec) {
449 try {
450 manifest = pickManifest(metadata, 'latest');
451 }
452 catch (e) {
453 if (e.code !== 'ETARGET' && e.code !== 'ENOVERSIONS') {
454 throw e;
455 }
456 }
457 }
458 }
459 else if (e.code !== 'ENOVERSIONS') {
460 throw e;
461 }
462 }
463 }
464 if (!manifest) {
465 this.logger.error(`Package specified by '${requestIdentifier.raw}' does not exist within the registry.`);
466 return 1;
467 }
468 if (manifest.version === node.package.version) {
469 this.logger.info(`Package '${packageName}' is already up to date.`);
470 continue;
471 }
472 packagesToUpdate.push(requestIdentifier.toString());
473 }
474 if (packagesToUpdate.length === 0) {
475 return 0;
476 }
477 const { success } = await this.executeSchematic('@schematics/update', 'update', {
478 verbose: options.verbose || false,
479 force: options.force || false,
480 next: !!options.next,
481 packageManager: this.packageManager,
482 packages: packagesToUpdate,
483 migrateExternal: true,
484 });
485 if (success && options.createCommits) {
486 const committed = this.commit(`Angular CLI update for packages - ${packagesToUpdate.join(', ')}`);
487 if (!committed) {
488 return 1;
489 }
490 }
491 // This is a temporary workaround to allow data to be passed back from the update schematic
492 // tslint:disable-next-line: no-any
493 const migrations = global.externalMigrations;
494 if (success && migrations) {
495 for (const migration of migrations) {
496 const result = await this.executeMigrations(migration.package,
497 // Resolve the collection from the workspace root, as otherwise it will be resolved from the temp
498 // installed CLI version.
499 require.resolve(migration.collection, { paths: [this.workspace.root] }), new semver.Range('>' + migration.from + ' <=' + migration.to), options.createCommits);
500 if (!result) {
501 return 0;
502 }
503 }
504 if (migrations.some(m => m.package === '@angular/core' && m.to.split('.')[0] === '9' && +m.from.split('.')[0] < 9)) {
505 this.logger.info(NG_VERSION_9_POST_MSG);
506 }
507 }
508 return success ? 0 : 1;
509 }
510 /**
511 * @return Whether or not the commit was successful.
512 */
513 commit(message) {
514 // Check if a commit is needed.
515 let commitNeeded;
516 try {
517 commitNeeded = hasChangesToCommit();
518 }
519 catch (err) {
520 this.logger.error(` Failed to read Git tree:\n${err.stderr}`);
521 return false;
522 }
523 if (!commitNeeded) {
524 this.logger.info(' No changes to commit after migration.');
525 return true;
526 }
527 // Commit changes and abort on error.
528 try {
529 createCommit(message);
530 }
531 catch (err) {
532 this.logger.error(`Failed to commit update (${message}):\n${err.stderr}`);
533 return false;
534 }
535 // Notify user of the commit.
536 const hash = findCurrentGitSha();
537 const shortMessage = message.split('\n')[0];
538 if (hash) {
539 this.logger.info(` Committed migration step (${getShortHash(hash)}): ${shortMessage}.`);
540 }
541 else {
542 // Commit was successful, but reading the hash was not. Something weird happened,
543 // but nothing that would stop the update. Just log the weirdness and continue.
544 this.logger.info(` Committed migration step: ${shortMessage}.`);
545 this.logger.warn(' Failed to look up hash of most recent commit, continuing anyways.');
546 }
547 return true;
548 }
549 checkCleanGit() {
550 try {
551 const topLevel = child_process_1.execSync('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: 'pipe' });
552 const result = child_process_1.execSync('git status --porcelain', { encoding: 'utf8', stdio: 'pipe' });
553 if (result.trim().length === 0) {
554 return true;
555 }
556 // Only files inside the workspace root are relevant
557 for (const entry of result.split('\n')) {
558 const relativeEntry = path.relative(path.resolve(this.workspace.root), path.resolve(topLevel.trim(), entry.slice(3).trim()));
559 if (!relativeEntry.startsWith('..') && !path.isAbsolute(relativeEntry)) {
560 return false;
561 }
562 }
563 }
564 catch (_a) { }
565 return true;
566 }
567 /**
568 * Checks if the current installed CLI version is older than the latest version.
569 * @returns `true` when the installed version is older.
570 */
571 async checkCLILatestVersion(verbose = false, next = false) {
572 const { version: installedCLIVersion } = require('../package.json');
573 const LatestCLIManifest = await package_metadata_1.fetchPackageManifest(`@angular/cli@${next ? 'next' : 'latest'}`, this.logger, {
574 verbose,
575 usingYarn: this.packageManager === schema_1.PackageManager.Yarn,
576 });
577 return semver.lt(installedCLIVersion, LatestCLIManifest.version);
578 }
579}
580exports.UpdateCommand = UpdateCommand;
581/**
582 * @return Whether or not the working directory has Git changes to commit.
583 */
584function hasChangesToCommit() {
585 // List all modified files not covered by .gitignore.
586 const files = child_process_1.execSync('git ls-files -m -d -o --exclude-standard').toString();
587 // If any files are returned, then there must be something to commit.
588 return files !== '';
589}
590/**
591 * Precondition: Must have pending changes to commit, they do not need to be staged.
592 * Postcondition: The Git working tree is committed and the repo is clean.
593 * @param message The commit message to use.
594 */
595function createCommit(message) {
596 // Stage entire working tree for commit.
597 child_process_1.execSync('git add -A', { encoding: 'utf8', stdio: 'pipe' });
598 // Commit with the message passed via stdin to avoid bash escaping issues.
599 child_process_1.execSync('git commit --no-verify -F -', { encoding: 'utf8', stdio: 'pipe', input: message });
600}
601/**
602 * @return The Git SHA hash of the HEAD commit. Returns null if unable to retrieve the hash.
603 */
604function findCurrentGitSha() {
605 try {
606 const hash = child_process_1.execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: 'pipe' });
607 return hash.trim();
608 }
609 catch (_a) {
610 return null;
611 }
612}
613function getShortHash(commitHash) {
614 return commitHash.slice(0, 9);
615}
616function coerceVersionNumber(version) {
617 if (!version) {
618 return null;
619 }
620 if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}/)) {
621 const match = version.match(/^\d{1,30}(\.\d{1,30})*/);
622 if (!match) {
623 return null;
624 }
625 if (!match[1]) {
626 version = version.substr(0, match[0].length) + '.0.0' + version.substr(match[0].length);
627 }
628 else if (!match[2]) {
629 version = version.substr(0, match[0].length) + '.0' + version.substr(match[0].length);
630 }
631 else {
632 return null;
633 }
634 }
635 return semver.valid(version);
636}