Skip to content

Commit c05bf6f

Browse files
committed
feat(@angular/cli): version check the CLI version against npm
Fixes angular#6592
1 parent 220e59d commit c05bf6f

File tree

11 files changed

+344
-3
lines changed

11 files changed

+344
-3
lines changed

packages/@angular/cli/commands/generate.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,17 @@ export default Command.extend({
125125
};
126126

127127
return blueprint.install(blueprintOptions)
128+
.then(() => {
129+
const VersionCheckTask = require('../tasks/version-check').default;
130+
const versionCheckTask = new VersionCheckTask({
131+
ui: this.ui,
132+
project: this.project
133+
});
134+
return versionCheckTask.run({
135+
forceCheck: false,
136+
includeUnstable: CliConfig.getValue('update.includePrerelease')
137+
});
138+
})
128139
.then(() => {
129140
const lintFix = commandOptions.lintFix !== undefined ?
130141
commandOptions.lintFix : CliConfig.getValue('defaults.lintFix');
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const Command = require('../ember-cli/lib/models/command');
2+
import { default as VersionCheckTask, VersionCheckTaskOptions } from '../tasks/version-check';
3+
4+
export interface UpdateOptions {
5+
includeUnstable: boolean;
6+
}
7+
8+
const VersionCheckCommand = Command.extend({
9+
name: 'version-check',
10+
description: 'Checks for an updated version.',
11+
works: 'insideProject',
12+
availableOptions: [
13+
{
14+
name: 'include-unstable',
15+
type: Boolean,
16+
default: false,
17+
description: 'Include unstable releases when checking.'
18+
}
19+
],
20+
21+
run: function(commandOptions: UpdateOptions) {
22+
const versionCheckTask = new VersionCheckTask({
23+
ui: this.ui,
24+
project: this.project
25+
});
26+
const options: VersionCheckTaskOptions = {...commandOptions, ...{ forceCheck: true } };
27+
28+
return versionCheckTask.run(options)
29+
.then((newVersionAvailable: boolean) => {
30+
if (!newVersionAvailable) {
31+
console.log('@angular/cli is up to date.');
32+
}
33+
});
34+
}
35+
});
36+
37+
export default VersionCheckCommand;

packages/@angular/cli/lib/cli/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function loadCommands() {
2323
'help': require('../../commands/help').default,
2424
'lint': require('../../commands/lint').default,
2525
'version': require('../../commands/version').default,
26+
'version-check': require('../../commands/version-check').default,
2627
'completion': require('../../commands/completion').default,
2728
'doc': require('../../commands/doc').default,
2829
'xi18n': require('../../commands/xi18n').default,

packages/@angular/cli/lib/config/schema.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,28 @@
514514
"default": true
515515
}
516516
}
517+
},
518+
"update": {
519+
"type": "object",
520+
"description": "Configuration for CLI update checks.",
521+
"default": {},
522+
"properties": {
523+
"lastChecked": {
524+
"type": "object",
525+
"default": {},
526+
"additionalProperties": true
527+
},
528+
"checkFrequency": {
529+
"type": "string",
530+
"description": "Defines how often to check for an updated version.",
531+
"default": "1d"
532+
},
533+
"includePrerelease": {
534+
"type": "boolean",
535+
"description": "Include pre-release versions when checking for updates.",
536+
"default": false
537+
}
538+
}
517539
}
518540
},
519541
"additionalProperties": false
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const Task = require('../ember-cli/lib/models/task');
2+
import { CliConfig } from '../models/config';
3+
import { CliConfig as ConfigInterface } from '../lib/config/schema';
4+
import { timeToCheck, newVersionAvailable, CheckResult } from '../update/check';
5+
import { oneLine } from 'common-tags';
6+
import * as chalk from 'chalk';
7+
8+
export interface VersionCheckTaskOptions {
9+
forceCheck: boolean;
10+
includeUnstable: boolean;
11+
}
12+
13+
export default Task.extend({
14+
run: function(options: VersionCheckTaskOptions) {
15+
const globalConfigManager = CliConfig.fromGlobal();
16+
const globalConfig = globalConfigManager.config;
17+
const lastChecked = getLastChecked(globalConfig, this.project.root);
18+
const frequency = getFrequency(globalConfig);
19+
20+
const isTimeToCheck = timeToCheck(lastChecked, frequency);
21+
if (isTimeToCheck) {
22+
updateLastChecked(globalConfigManager, this.project.root);
23+
}
24+
25+
if (options.forceCheck || isTimeToCheck) {
26+
const rootDir = '';
27+
const packageName = '@angular/cli';
28+
return newVersionAvailable(rootDir, packageName, options)
29+
.then((result: CheckResult) => {
30+
const newVersionAvailable = !!result.newVersion;
31+
if (newVersionAvailable) {
32+
this.ui.writeLine(chalk.yellow(oneLine`Current version (${result.currentVersion})
33+
is out of date consider updating to the new version (${result.newVersion})`));
34+
}
35+
return newVersionAvailable;
36+
})
37+
.catch(() => {}); // Ignore version check failures.
38+
}
39+
}
40+
});
41+
42+
function getLastChecked(config: ConfigInterface, rootDir: string): Date {
43+
let lastChecked;
44+
if (config && config.update && config.update.lastChecked) {
45+
lastChecked = config.update.lastChecked[encodePath(rootDir)];
46+
if (lastChecked) {
47+
lastChecked = new Date(lastChecked);
48+
}
49+
}
50+
return lastChecked || new Date(0);
51+
}
52+
53+
function updateLastChecked(config: CliConfig, rootDir: string): void {
54+
const now = (new Date()).toISOString();
55+
if (!config.get('update')) {
56+
config.set('update', {});
57+
}
58+
if (!config.get('update.lastChecked')) {
59+
config.set('update.lastChecked', {});
60+
}
61+
config.set(`update.lastChecked.${encodePath(rootDir)}`, now);
62+
config.save();
63+
}
64+
65+
function encodePath(path: string): string {
66+
return path.replace(/\./g, '|');
67+
}
68+
69+
function getFrequency(config: ConfigInterface): string {
70+
let frequency;
71+
if (config && config.update) {
72+
frequency = config.update.checkFrequency;
73+
}
74+
return frequency || '1d';
75+
}

packages/@angular/cli/update/check.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import * as path from 'path';
2+
import * as https from 'https';
3+
import {lt} from 'semver';
4+
5+
6+
const ONE_HOUR = 3600000;
7+
const ONE_DAY = ONE_HOUR * 24;
8+
const ONE_WEEK = ONE_DAY * 7;
9+
10+
export interface CheckOptions {
11+
includeUnstable: boolean;
12+
}
13+
14+
export interface CheckResult {
15+
currentVersion: string;
16+
newVersion?: string;
17+
}
18+
19+
export function timeToCheck(lastChecked: Date, frequency: string): boolean {
20+
const freq = frequency || '1d';
21+
if (freq.toLowerCase() === 'never') {
22+
return false;
23+
} else if (freq.toLowerCase() === 'always') {
24+
return true;
25+
}
26+
if (!lastChecked) {
27+
lastChecked = new Date(0);
28+
}
29+
const diff = Math.abs(lastChecked.valueOf() - new Date().valueOf());
30+
const threshold = convertFrequency(freq);
31+
return diff > threshold;
32+
}
33+
34+
export function newVersionAvailable(
35+
rootDir: string,
36+
name: string,
37+
options?: CheckOptions): Promise<CheckResult> {
38+
return Promise.all([
39+
getLocalVersion(rootDir, name),
40+
getRemoteVersion(name, options)
41+
])
42+
.then(([localVersion, remoteVersion]) => {
43+
const newer = lt(localVersion, remoteVersion);
44+
const result: CheckResult = {
45+
currentVersion: localVersion
46+
};
47+
if (newer) {
48+
result.newVersion = remoteVersion;
49+
}
50+
return result;
51+
});
52+
}
53+
54+
function encodePackageName(name: string): string {
55+
let encoded = encodeURIComponent(name);
56+
if (name.startsWith('@')) {
57+
encoded = `@${encoded.substr(3)}`;
58+
}
59+
return encoded;
60+
}
61+
62+
function getRemoteVersion(name: string, options?: CheckOptions): Promise<string> {
63+
const url = `https://registry.npmjs.org/${encodePackageName(name)}`;
64+
return httpsGet(url)
65+
.then(resp => {
66+
const version = options.includeUnstable ?
67+
resp['dist-tags'].next :
68+
resp['dist-tags'].latest;
69+
return version;
70+
});
71+
}
72+
73+
function getLocalVersion(rootDir: string, name: string): Promise<string> {
74+
const packagePath = path.resolve(rootDir, 'node_modules', name, 'package.json');
75+
return new Promise((resolve, reject) => {
76+
try {
77+
const packageInfo = require(packagePath);
78+
resolve(packageInfo.version);
79+
} catch (err) {
80+
reject();
81+
}
82+
83+
});
84+
}
85+
86+
function httpsGet(url: string): Promise<any> {
87+
return new Promise((resolve, reject) => {
88+
https.get(url, (res) => {
89+
const { statusCode } = res;
90+
const contentType = res.headers['content-type'];
91+
92+
let error;
93+
if (statusCode !== 200) {
94+
error = new Error(`Request Failed. Status Code: ${statusCode}`);
95+
} else if (!/^application\/json/.test(contentType)) {
96+
error = new Error(
97+
`Invalid content-type. Expected application/json but received ${contentType}`);
98+
}
99+
if (error) {
100+
reject(error.message);
101+
// consume response data to free up memory
102+
res.resume();
103+
return;
104+
}
105+
106+
res.setEncoding('utf8');
107+
let rawData = '';
108+
res.on('data', (chunk) => {
109+
rawData += chunk;
110+
});
111+
112+
res.on('end', () => {
113+
try {
114+
const parsedData = JSON.parse(rawData);
115+
resolve(parsedData);
116+
} catch (e) {
117+
reject(e.message);
118+
}
119+
});
120+
}).on('error', (e) => {
121+
reject(e.message);
122+
});
123+
});
124+
}
125+
126+
function convertFrequency(frequency: string): number {
127+
const value = parseFloat(frequency.substring(0, frequency.length - 1));
128+
const unit = frequency[frequency.length - 1];
129+
let factor = 1;
130+
switch (unit.toLowerCase()) {
131+
case 'h':
132+
factor = ONE_HOUR;
133+
break;
134+
case 'w':
135+
factor = ONE_WEEK;
136+
break;
137+
default:
138+
factor = ONE_DAY;
139+
break;
140+
}
141+
142+
return value * factor;
143+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// import * as checker from './check';
2+
3+
// const lastChecked = getLastChecked();
4+
// const frequency = '1d';
5+
6+
// if (checker.timeToCheck(lastChecked, frequency)) {
7+
// const rootDir = '';
8+
// const options = {
9+
// includeUnstable: false
10+
// };
11+
// checker.newVersionAvailable(rootDir, '@angular/cli', options)
12+
// .then(newVersion => {
13+
// if (!!newVersion) {
14+
// console.log('NEW VERSION AVAILABLE');
15+
// }
16+
// });
17+
// }
18+
19+
// function getLastChecked(): Date {
20+
// return new Date();
21+
// }

packages/@ngtools/json-schema/src/schema-class-factory.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ class SchemaClassBase<T> implements SchemaClass<T> {
130130
/** Set a value from a JSON path. */
131131
$$set(path: string, value: any): void {
132132
const node = _getSchemaNodeForPath(this.$$schema(), path);
133-
134133
if (node) {
135134
node.set(value);
136135
} else {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ng } from '../../utils/process';
2+
import { updateJsonFile } from '../../utils/project';
3+
import { oneLine } from 'common-tags';
4+
5+
export default function () {
6+
return updateJsonFile('node_modules/@angular/cli/package.json', (json) => {
7+
json.version = '1.0.0';
8+
})
9+
.then(() => ng('generate', 'component', 'basic'))
10+
.then(({ stdout }) => {
11+
if (!stdout.match(/is out of date/)) {
12+
throw new Error(oneLine`
13+
Expected to match "is out of date"
14+
in ${stdout}.
15+
`);
16+
}
17+
});
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ng } from '../../utils/process';
2+
import { oneLine } from 'common-tags';
3+
4+
export default function () {
5+
return ng('generate', 'component', 'basic')
6+
.then(({ stdout }) => {
7+
if (stdout.match(/is out of date/)) {
8+
throw new Error(oneLine`
9+
Expected to not match "is out of date"
10+
in ${stdout}.
11+
`);
12+
}
13+
});
14+
}

tests/e2e/utils/process.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ let _processes: child_process.ChildProcess[] = [];
1515

1616
type ProcessOutput = {
1717
stdout: string;
18-
stdout: string;
18+
stderr: string;
1919
};
2020

2121

@@ -77,7 +77,7 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise<Proce
7777
_processes = _processes.filter(p => p !== childProcess);
7878

7979
if (!error) {
80-
resolve({ stdout });
80+
resolve({ stdout, stderr });
8181
} else {
8282
err.message += `${error}...\n\nSTDOUT:\n${stdout}\n`;
8383
reject(err);

0 commit comments

Comments
 (0)