mirror of
https://github.com/LukeHagar/redocly-cli.git
synced 2025-12-06 04:21:09 +00:00
feat: create update notifier for cli (#1090)
This commit is contained in:
2
.github/workflows/performance.yaml
vendored
2
.github/workflows/performance.yaml
vendored
@@ -29,6 +29,8 @@ jobs:
|
||||
- run: redocly --version
|
||||
- name: Run Benchmark
|
||||
run: hyperfine -i --warmup 3 'redocly lint packages/core/src/benchmark/benches/rebilly.yaml' 'redocly-next lint packages/core/src/benchmark/benches/rebilly.yaml' --export-markdown benchmark_check.md --export-json benchmark_check.json
|
||||
env:
|
||||
CI: true
|
||||
- name: Comment PR
|
||||
uses: thollander/actions-comment-pull-request@v2
|
||||
with:
|
||||
|
||||
63
package-lock.json
generated
63
package-lock.json
generated
@@ -1508,6 +1508,12 @@
|
||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
|
||||
"integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/sizzle": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
|
||||
@@ -5850,7 +5856,6 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
@@ -7742,10 +7747,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"dev": true,
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
|
||||
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
@@ -9510,8 +9514,7 @@
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
@@ -9589,6 +9592,7 @@
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"redoc": "~2.0.0",
|
||||
"semver": "^7.5.1",
|
||||
"simple-websocket": "^9.0.0",
|
||||
"styled-components": "5.3.3",
|
||||
"yargs": "17.0.1"
|
||||
@@ -9601,18 +9605,24 @@
|
||||
"@types/configstore": "^5.0.1",
|
||||
"@types/react": "^17.0.8",
|
||||
"@types/react-dom": "^17.0.5",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@types/styled-components": "^5.1.1",
|
||||
"@types/yargs": "16.0.2",
|
||||
"@types/yargs": "17.0.5",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"packages/cli/node_modules/@types/node": {
|
||||
"version": "14.18.47",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.47.tgz",
|
||||
"integrity": "sha512-OuJi8bIng4wYHHA3YpKauL58dZrPxro3d0tabPHyiNF8rKfGKuVfr83oFlPLmKri1cX+Z3cJP39GXmnqkP11Gw=="
|
||||
},
|
||||
"packages/cli/node_modules/@types/yargs": {
|
||||
"version": "16.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.2.tgz",
|
||||
"integrity": "sha512-EkZDJvHXblOE2/iFZb3Ty5c8RHPssmFvrXqj3V/ZLXtOt0f7xJh9Fd7p1pl2ITrNHM7Rl3Db3JJVBT7q/C0wUg==",
|
||||
"version": "17.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.5.tgz",
|
||||
"integrity": "sha512-4HNq144yhaVjJs+ON6A07NEoi9Hh0Rhl/jI9Nt/l/YRjt+T6St/QK3meFARWZ8IgkzoD1LC0PdTdJenlQQi2WQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/yargs-parser": "*"
|
||||
@@ -10587,8 +10597,9 @@
|
||||
"@types/configstore": "^5.0.1",
|
||||
"@types/react": "^17.0.8",
|
||||
"@types/react-dom": "^17.0.5",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@types/styled-components": "^5.1.1",
|
||||
"@types/yargs": "16.0.2",
|
||||
"@types/yargs": "17.0.5",
|
||||
"assert-node-version": "^1.0.3",
|
||||
"chokidar": "^3.5.1",
|
||||
"colorette": "^1.2.0",
|
||||
@@ -10600,16 +10611,21 @@
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"redoc": "~2.0.0",
|
||||
"semver": "^7.5.1",
|
||||
"simple-websocket": "^9.0.0",
|
||||
"styled-components": "5.3.3",
|
||||
"typescript": "^4.0.3",
|
||||
"yargs": "17.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "https://registry.npmjs.org/@types/node/-/node-14.18.47.tgz",
|
||||
"integrity": "sha512-OuJi8bIng4wYHHA3YpKauL58dZrPxro3d0tabPHyiNF8rKfGKuVfr83oFlPLmKri1cX+Z3cJP39GXmnqkP11Gw=="
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "16.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.2.tgz",
|
||||
"integrity": "sha512-EkZDJvHXblOE2/iFZb3Ty5c8RHPssmFvrXqj3V/ZLXtOt0f7xJh9Fd7p1pl2ITrNHM7Rl3Db3JJVBT7q/C0wUg==",
|
||||
"version": "17.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.5.tgz",
|
||||
"integrity": "sha512-4HNq144yhaVjJs+ON6A07NEoi9Hh0Rhl/jI9Nt/l/YRjt+T6St/QK3meFARWZ8IgkzoD1LC0PdTdJenlQQi2WQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
@@ -10937,6 +10953,12 @@
|
||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/semver": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
|
||||
"integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/sizzle": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
|
||||
@@ -14281,7 +14303,6 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
@@ -15696,10 +15717,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"dev": true,
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
|
||||
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
@@ -17028,8 +17048,7 @@
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"yaml": {
|
||||
"version": "1.10.2",
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"redoc": "~2.0.0",
|
||||
"semver": "^7.5.1",
|
||||
"simple-websocket": "^9.0.0",
|
||||
"styled-components": "5.3.3",
|
||||
"yargs": "17.0.1"
|
||||
@@ -54,7 +55,8 @@
|
||||
"@types/react": "^17.0.8",
|
||||
"@types/react-dom": "^17.0.5",
|
||||
"@types/styled-components": "^5.1.1",
|
||||
"@types/yargs": "16.0.2",
|
||||
"@types/yargs": "17.0.5",
|
||||
"@types/semver": "^7.5.0",
|
||||
"typescript": "^4.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
handleError,
|
||||
CircularJSONNotSupportedError,
|
||||
sortTopLevelKeysForOas,
|
||||
cleanColors,
|
||||
} from '../utils';
|
||||
import {
|
||||
ResolvedApi,
|
||||
@@ -439,3 +440,12 @@ describe('checkIfRulesetExist', () => {
|
||||
expect(process.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanColors', () => {
|
||||
it('should remove colors from string', () => {
|
||||
const stringWithColors = `String for ${red('test')}`;
|
||||
const result = cleanColors(stringWithColors);
|
||||
|
||||
expect(result).not.toMatch(/\x1b\[\d+m/g);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,12 @@ import { handleBundle } from './commands/bundle';
|
||||
import { handleLogin } from './commands/login';
|
||||
import { handlerBuildCommand } from './commands/build-docs';
|
||||
import type { BuildDocsArgv } from './commands/build-docs/types';
|
||||
import { cacheLatestVersion, notifyUpdateCliVersion } from './update-version-notifier';
|
||||
|
||||
const version = require('../package.json').version;
|
||||
|
||||
cacheLatestVersion();
|
||||
|
||||
yargs
|
||||
.version('version', 'Show version number.', version)
|
||||
.help('help', 'Show help.')
|
||||
@@ -429,4 +433,5 @@ yargs
|
||||
)
|
||||
.completion('completion', 'Generate completion script.')
|
||||
.demandCommand(1)
|
||||
.middleware([notifyUpdateCliVersion])
|
||||
.strict().argv;
|
||||
|
||||
103
packages/cli/src/update-version-notifier.ts
Normal file
103
packages/cli/src/update-version-notifier.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { existsSync, writeFileSync, readFileSync, statSync } from 'fs';
|
||||
import { compare } from 'semver';
|
||||
import fetch from 'node-fetch';
|
||||
import { cyan, green, yellow } from 'colorette';
|
||||
import { cleanColors } from './utils';
|
||||
|
||||
const { version, name } = require('../package.json');
|
||||
|
||||
const VERSION_CACHE_FILE = 'redocly-cli-version';
|
||||
const SPACE_TO_BORDER = 4;
|
||||
|
||||
const INTERVAL_TO_CHECK = 1000 * 60 * 60 * 12;
|
||||
const SHOULD_NOT_NOTIFY =
|
||||
process.env.NODE_ENV === 'test' || process.env.CI || !!process.env.LAMBDA_TASK_ROOT;
|
||||
|
||||
export const notifyUpdateCliVersion = () => {
|
||||
if (SHOULD_NOT_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const latestVersion = readFileSync(join(tmpdir(), VERSION_CACHE_FILE)).toString();
|
||||
|
||||
if (isNewVersionAvailable(version, latestVersion)) {
|
||||
renderUpdateBanner(version, latestVersion);
|
||||
}
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const isNewVersionAvailable = (current: string, latest: string) => compare(current, latest) < 0;
|
||||
|
||||
const getLatestVersion = async (packageName: string): Promise<string> => {
|
||||
const latestUrl = `http://registry.npmjs.org/${packageName}/latest`;
|
||||
const response = await fetch(latestUrl);
|
||||
const info = await response.json();
|
||||
return info.version;
|
||||
};
|
||||
|
||||
export const cacheLatestVersion = () => {
|
||||
if (!isNeedToBeCached() || SHOULD_NOT_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
|
||||
getLatestVersion(name)
|
||||
.then((version) => {
|
||||
const lastCheckFile = join(tmpdir(), VERSION_CACHE_FILE);
|
||||
writeFileSync(lastCheckFile, version);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const renderUpdateBanner = (current: string, latest: string) => {
|
||||
const messageLines = [
|
||||
`A new version of ${cyan('Redocly CLI')} (${green(latest)}) is available.`,
|
||||
`Update now: \`${cyan('npm i -g @redocly/cli@latest')}\`.`,
|
||||
`Changelog: https://redocly.com/docs/cli/changelog/`,
|
||||
];
|
||||
const maxLength = Math.max(...messageLines.map((line) => cleanColors(line).length));
|
||||
|
||||
const border = yellow('═'.repeat(maxLength + SPACE_TO_BORDER));
|
||||
|
||||
const banner = `
|
||||
${yellow('╔' + border + '╗')}
|
||||
${yellow('║' + ' '.repeat(maxLength + SPACE_TO_BORDER) + '║')}
|
||||
${messageLines
|
||||
.map((line, index) => {
|
||||
return getLineWithPadding(maxLength, line, index);
|
||||
})
|
||||
.join('\n')}
|
||||
${yellow('║' + ' '.repeat(maxLength + SPACE_TO_BORDER) + '║')}
|
||||
${yellow('╚' + border + '╝')}
|
||||
`;
|
||||
|
||||
process.stderr.write(banner);
|
||||
};
|
||||
|
||||
const getLineWithPadding = (maxLength: number, line: string, index: number): string => {
|
||||
const padding = ' '.repeat(maxLength - cleanColors(line).length);
|
||||
const extraSpaces = index !== 0 ? ' '.repeat(SPACE_TO_BORDER) : '';
|
||||
return `${extraSpaces}${yellow('║')} ${line}${padding} ${yellow('║')}`;
|
||||
};
|
||||
|
||||
const isNeedToBeCached = (): boolean => {
|
||||
try {
|
||||
// Last version from npm is stored in a file in the OS temp folder
|
||||
const versionFile = join(tmpdir(), VERSION_CACHE_FILE);
|
||||
|
||||
if (!existsSync(versionFile)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = new Date().getTime();
|
||||
const stats = statSync(versionFile);
|
||||
const lastCheck = stats.mtime.getTime();
|
||||
|
||||
return now - lastCheck >= INTERVAL_TO_CHECK;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -470,3 +470,8 @@ export function checkIfRulesetExist(rules: typeof StyleguideConfig.prototype.rul
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanColors(input: string): string {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
return input.replace(/\x1b\[\d+m/g, '');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user