diff --git a/.eslintrc.js b/.eslintrc.js index 08f6d170..2245c459 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,18 +1,19 @@ +const prettierConfig = require('./.prettierrc.js') module.exports = { - root: true, - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint", "prettier"], - extends: [ - "oclif", - "oclif-typescript", - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "prettier", - ], - rules: { - "prettier/prettier": "error", - "no-useless-constructor": "off", - "@typescript-eslint/no-useless-constructor": "error", - "@typescript-eslint/no-var-requires": "off", - }, -}; + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'prettier'], + extends: [ + 'oclif', + 'oclif-typescript', + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + ], + rules: { + 'prettier/prettier': ['error', prettierConfig], + 'no-useless-constructor': 'off', + '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/no-var-requires': 'off', + }, +} diff --git a/.prettierrc.js b/.prettierrc.js index 51cc4825..3f7548f7 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,6 +1,5 @@ module.exports = { - trailingComma: "es5", - tabWidth: 4, - semi: false, singleQuote: true, -}; + trailingComma: 'all', + arrowParens: 'always', +}; \ No newline at end of file diff --git a/src/commands/use.ts b/src/commands/use.ts index f03f0f65..b40343d1 100644 --- a/src/commands/use.ts +++ b/src/commands/use.ts @@ -1,104 +1,96 @@ -import cli from 'cli-ux' -import * as fs from 'fs-extra' -import * as semver from 'semver' +import cli from 'cli-ux'; +import * as fs from 'fs-extra'; +import * as semver from 'semver'; -import UpdateCommand from './update' +import UpdateCommand from './update'; const SEMVER_REGEX = - /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?/ + /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?/; export default class UseCommand extends UpdateCommand { - static args = [{ name: 'version', optional: false }] + static args = [{ name: 'version', optional: false }]; - static flags = {} + static flags = {}; - async run() { - const { args } = this.parse(UseCommand) + async run() { + const { args } = this.parse(UseCommand); - // Check if this command is trying to update the channel. TODO: make this dynamic - const prereleaseChannels = ['alpha', 'beta', 'next'] - const isExplicitVersion = SEMVER_REGEX.test(args.version || '') - const channelUpdateRequested = ['stable', ...prereleaseChannels].some( - (c) => args.version === c - ) + // Check if this command is trying to update the channel. TODO: make this dynamic + const prereleaseChannels = ['alpha', 'beta', 'next']; + const isExplicitVersion = SEMVER_REGEX.test(args.version || ''); + const channelUpdateRequested = ['stable', ...prereleaseChannels].some( + (c) => args.version === c, + ); - if (!isExplicitVersion && !channelUpdateRequested) { - throw new Error( - `Invalid argument provided: ${args.version}. Please specify either a valid channel (alpha, beta, next, stable) or an explicit version (ex. 2.68.13)` - ) - } - - this.channel = channelUpdateRequested - ? args.version - : await this.determineChannel() - - const targetVersion = semver.clean(args.version || '') || args.version + if (!isExplicitVersion && !channelUpdateRequested) { + throw new Error( + `Invalid argument provided: ${args.version}. Please specify either a valid channel (alpha, beta, next, stable) or an explicit version (ex. 2.68.13)`, + ); + } - // Determine if the version is from a different channel and update to account for it (ex. cli-example update 3.0.0-next.22 should update the channel to next as well.) - const versionParts = targetVersion?.split('-') || ['', ''] - if (versionParts && versionParts[1]) { - this.channel = versionParts[1].substr( - 0, - versionParts[1].indexOf('.') - ) - this.debug(`Flag overriden target channel: ${this.channel}`) - } + this.channel = channelUpdateRequested + ? args.version + : await this.determineChannel(); - await this.ensureClientDir() - this.debug( - `Looking for locally installed versions at ${this.clientRoot}` - ) + const targetVersion = semver.clean(args.version || '') || args.version; - // Do not show known non-local version folder names, bin and current. - const versions = fs - .readdirSync(this.clientRoot) - .filter( - (dirOrFile) => dirOrFile !== 'bin' && dirOrFile !== 'current' - ) - if (versions.length === 0) - throw new Error('No locally installed versions found.') - const matchingLocalVersions = versions - .filter((version) => { - // - If the version contains 'partial', ignore it - if (version.includes('partial')) { - return false - } - // - If we request stable, only provide standard versions... - if (this.channel === 'stable') { - return !prereleaseChannels.some((c) => version.includes(c)) - } - // - ... otherwise check if the version is contained - return version.includes(targetVersion) - }) - .sort((a, b) => semver.compare(b, a)) + // Determine if the version is from a different channel and update to account for it (ex. cli-example update 3.0.0-next.22 should update the channel to next as well.) + const versionParts = targetVersion?.split('-') || ['', '']; + if (versionParts && versionParts[1]) { + this.channel = versionParts[1].substr(0, versionParts[1].indexOf('.')); + this.debug(`Flag overriden target channel: ${this.channel}`); + } - if ( - args.version && - (versions.includes(targetVersion) || - matchingLocalVersions.length > 0) - ) { - const target = versions.includes(targetVersion) - ? targetVersion - : matchingLocalVersions[0] - await this.updateToExistingVersion(target) - this.currentVersion = await this.determineCurrentVersion() - this.updatedVersion = target - if (channelUpdateRequested) { - await this.setChannel() - } - this.log(`Success! You are now on ${target}!`) - } else { - const localVersionsMsg = `Locally installed versions available: \n${versions - .map((version) => `\t${version}`) - .join('\n')}\n` + await this.ensureClientDir(); + this.debug(`Looking for locally installed versions at ${this.clientRoot}`); - throw new Error( - `Requested version could not be found locally. ${localVersionsMsg}` - ) + // Do not show known non-local version folder names, bin and current. + const versions = fs + .readdirSync(this.clientRoot) + .filter((dirOrFile) => dirOrFile !== 'bin' && dirOrFile !== 'current'); + if (versions.length === 0) + throw new Error('No locally installed versions found.'); + const matchingLocalVersions = versions + .filter((version) => { + // - If the version contains 'partial', ignore it + if (version.includes('partial')) { + return false; + } + // - If we request stable, only provide standard versions... + if (this.channel === 'stable') { + return !prereleaseChannels.some((c) => version.includes(c)); } + // - ... otherwise check if the version is contained + return version.includes(targetVersion); + }) + .sort((a, b) => semver.compare(b, a)); - this.log() - this.debug('done') - cli.action.stop() + if ( + args.version && + (versions.includes(targetVersion) || matchingLocalVersions.length > 0) + ) { + const target = versions.includes(targetVersion) + ? targetVersion + : matchingLocalVersions[0]; + await this.updateToExistingVersion(target); + this.currentVersion = await this.determineCurrentVersion(); + this.updatedVersion = target; + if (channelUpdateRequested) { + await this.setChannel(); + } + this.log(`Success! You are now on ${target}!`); + } else { + const localVersionsMsg = `Locally installed versions available: \n${versions + .map((version) => `\t${version}`) + .join('\n')}\n`; + + throw new Error( + `Requested version could not be found locally. ${localVersionsMsg}`, + ); } + + this.log(); + this.debug('done'); + cli.action.stop(); + } } diff --git a/src/hooks/init.ts b/src/hooks/init.ts index f01f6e54..9c2b31fe 100644 --- a/src/hooks/init.ts +++ b/src/hooks/init.ts @@ -1,79 +1,79 @@ -import * as Config from '@oclif/config' -import cli from 'cli-ux' -import * as spawn from 'cross-spawn' -import * as fs from 'fs-extra' -import * as path from 'path' -import debugUtil from 'debug' -import { touch } from '../util' +import * as Config from '@oclif/config'; +import cli from 'cli-ux'; +import * as spawn from 'cross-spawn'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import debugUtil from 'debug'; +import { touch } from '../util'; -const debug = debugUtil('cli:updater') +const debug = debugUtil('cli:updater'); function timestamp(msg: string): string { - return `[${new Date().toISOString()}] ${msg}` + return `[${new Date().toISOString()}] ${msg}`; } async function mtime(f: string) { - const { mtime } = await fs.stat(f) - return mtime + const { mtime } = await fs.stat(f); + return mtime; } export const init: Config.Hook<'init'> = async function (opts) { - if (opts.id === 'update') return - if (opts.config.scopedEnvVarTrue('DISABLE_AUTOUPDATE')) return - const binPath = this.config.binPath || this.config.bin - const lastrunfile = path.join(this.config.cacheDir, 'lastrun') - const autoupdatefile = path.join(this.config.cacheDir, 'autoupdate') - const autoupdatelogfile = path.join(this.config.cacheDir, 'autoupdate.log') - const clientRoot = - this.config.scopedEnvVar('OCLIF_CLIENT_HOME') || - path.join(this.config.dataDir, 'client') + if (opts.id === 'update') return; + if (opts.config.scopedEnvVarTrue('DISABLE_AUTOUPDATE')) return; + const binPath = this.config.binPath || this.config.bin; + const lastrunfile = path.join(this.config.cacheDir, 'lastrun'); + const autoupdatefile = path.join(this.config.cacheDir, 'autoupdate'); + const autoupdatelogfile = path.join(this.config.cacheDir, 'autoupdate.log'); + const clientRoot = + this.config.scopedEnvVar('OCLIF_CLIENT_HOME') || + path.join(this.config.dataDir, 'client'); - const autoupdateEnv = { - ...process.env, - [this.config.scopedEnvVarKey('TIMESTAMPS')]: '1', - [this.config.scopedEnvVarKey('SKIP_ANALYTICS')]: '1', - } + const autoupdateEnv = { + ...process.env, + [this.config.scopedEnvVarKey('TIMESTAMPS')]: '1', + [this.config.scopedEnvVarKey('SKIP_ANALYTICS')]: '1', + }; - async function autoupdateNeeded(): Promise { - try { - const m = await mtime(autoupdatefile) - let days = 1 - if (opts.config.channel === 'stable') days = 14 - m.setHours(m.getHours() + days * 24) - return m < new Date() - } catch (error) { - if (error.code !== 'ENOENT') cli.error(error.stack) - if ((global as any).testing) return false - debug('autoupdate ENOENT') - return true - } + async function autoupdateNeeded(): Promise { + try { + const m = await mtime(autoupdatefile); + let days = 1; + if (opts.config.channel === 'stable') days = 14; + m.setHours(m.getHours() + days * 24); + return m < new Date(); + } catch (error) { + if (error.code !== 'ENOENT') cli.error(error.stack); + if ((global as any).testing) return false; + debug('autoupdate ENOENT'); + return true; } + } - await touch(lastrunfile) - const clientDir = path.join(clientRoot, this.config.version) - if (await fs.pathExists(clientDir)) await touch(clientDir) - if (!(await autoupdateNeeded())) return + await touch(lastrunfile); + const clientDir = path.join(clientRoot, this.config.version); + if (await fs.pathExists(clientDir)) await touch(clientDir); + if (!(await autoupdateNeeded())) return; - debug('autoupdate running') - await fs.outputFile(autoupdatefile, '') + debug('autoupdate running'); + await fs.outputFile(autoupdatefile, ''); - debug(`spawning autoupdate on ${binPath}`) + debug(`spawning autoupdate on ${binPath}`); - const fd = await fs.open(autoupdatelogfile, 'a') - fs.write( - fd, - timestamp( - `starting \`${binPath} update --autoupdate\` from ${process.argv - .slice(1, 3) - .join(' ')}\n` - ) - ) + const fd = await fs.open(autoupdatelogfile, 'a'); + fs.write( + fd, + timestamp( + `starting \`${binPath} update --autoupdate\` from ${process.argv + .slice(1, 3) + .join(' ')}\n`, + ), + ); - spawn(binPath, ['update', '--autoupdate'], { - detached: !this.config.windows, - stdio: ['ignore', fd, fd], - env: autoupdateEnv, - }) - .on('error', (e: Error) => process.emitWarning(e)) - .unref() -} + spawn(binPath, ['update', '--autoupdate'], { + detached: !this.config.windows, + stdio: ['ignore', fd, fd], + env: autoupdateEnv, + }) + .on('error', (e: Error) => process.emitWarning(e)) + .unref(); +}; diff --git a/src/tar.ts b/src/tar.ts index e4db2c4c..d429a58d 100644 --- a/src/tar.ts +++ b/src/tar.ts @@ -1,97 +1,89 @@ -import * as fs from 'fs-extra' -import * as path from 'path' -import debugUtil from 'debug' -import { touch } from './util' +import * as fs from 'fs-extra'; +import * as path from 'path'; +import debugUtil from 'debug'; +import { touch } from './util'; -const debug = debugUtil('oclif-update') +const debug = debugUtil('oclif-update'); export async function extract( - stream: NodeJS.ReadableStream, - basename: string, - output: string, - sha?: string + stream: NodeJS.ReadableStream, + basename: string, + output: string, + sha?: string, ) { - const getTmp = () => - `${output}.partial.${Math.random() - .toString() - .split('.')[1] - .slice(0, 5)}` - let tmp = getTmp() - if (fs.pathExistsSync(tmp)) tmp = getTmp() - debug(`extracting to ${tmp}`) - try { - await new Promise((resolve, reject) => { - const zlib = require('zlib') - const tar = require('tar-fs') - const crypto = require('crypto') + const getTmp = () => + `${output}.partial.${Math.random().toString().split('.')[1].slice(0, 5)}`; + let tmp = getTmp(); + if (fs.pathExistsSync(tmp)) tmp = getTmp(); + debug(`extracting to ${tmp}`); + try { + await new Promise((resolve, reject) => { + const zlib = require('zlib'); + const tar = require('tar-fs'); + const crypto = require('crypto'); - let shaValidated = false - let extracted = false - const check = () => shaValidated && extracted && resolve(true) + let shaValidated = false; + let extracted = false; + const check = () => shaValidated && extracted && resolve(true); - if (sha) { - const hasher = crypto.createHash('sha256') - stream.on('error', reject) - stream.on('data', (d) => hasher.update(d)) - stream.on('end', () => { - const shasum = hasher.digest('hex') - if (sha === shasum) { - shaValidated = true - check() - } else { - reject( - new Error( - `SHA mismatch: expected ${shasum} to be ${sha}` - ) - ) - } - }) - } else shaValidated = true + if (sha) { + const hasher = crypto.createHash('sha256'); + stream.on('error', reject); + stream.on('data', (d) => hasher.update(d)); + stream.on('end', () => { + const shasum = hasher.digest('hex'); + if (sha === shasum) { + shaValidated = true; + check(); + } else { + reject(new Error(`SHA mismatch: expected ${shasum} to be ${sha}`)); + } + }); + } else shaValidated = true; - const ignore = (_: any, header: any) => { - switch (header.type) { - case 'directory': - case 'file': - if (process.env.OCLIF_DEBUG_UPDATE_FILES) - debug(header.name) - return false - case 'symlink': - return true - default: - throw new Error(header.type) - } - } - const extract = tar.extract(tmp, { ignore }) - extract.on('error', reject) - extract.on('finish', () => { - extracted = true - check() - }) + const ignore = (_: any, header: any) => { + switch (header.type) { + case 'directory': + case 'file': + if (process.env.OCLIF_DEBUG_UPDATE_FILES) debug(header.name); + return false; + case 'symlink': + return true; + default: + throw new Error(header.type); + } + }; + const extract = tar.extract(tmp, { ignore }); + extract.on('error', reject); + extract.on('finish', () => { + extracted = true; + check(); + }); - const gunzip = zlib.createGunzip() - gunzip.on('error', reject) + const gunzip = zlib.createGunzip(); + gunzip.on('error', reject); - stream.pipe(gunzip).pipe(extract) - }) + stream.pipe(gunzip).pipe(extract); + }); - if (await fs.pathExists(output)) { - try { - const tmp = getTmp() - await fs.move(output, tmp) - await fs.remove(tmp).catch(debug) - } catch (error) { - debug(error) - await fs.remove(output) - } - } - const from = path.join(tmp, basename) - debug('moving %s to %s', from, output) - await fs.rename(from, output) - await fs.remove(tmp).catch(debug) - await touch(output) - debug('done extracting') - } catch (error) { - await fs.remove(tmp).catch(process.emitWarning) - throw error + if (await fs.pathExists(output)) { + try { + const tmp = getTmp(); + await fs.move(output, tmp); + await fs.remove(tmp).catch(debug); + } catch (error) { + debug(error); + await fs.remove(output); + } } + const from = path.join(tmp, basename); + debug('moving %s to %s', from, output); + await fs.rename(from, output); + await fs.remove(tmp).catch(debug); + await touch(output); + debug('done extracting'); + } catch (error) { + await fs.remove(tmp).catch(process.emitWarning); + throw error; + } } diff --git a/src/util.ts b/src/util.ts index 4c86be08..1292acdb 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,25 +1,25 @@ -import * as fs from 'fs-extra' -import * as path from 'path' +import * as fs from 'fs-extra'; +import * as path from 'path'; export async function touch(p: string) { - try { - await fs.utimes(p, new Date(), new Date()) - } catch { - await fs.outputFile(p, '') - } + try { + await fs.utimes(p, new Date(), new Date()); + } catch { + await fs.outputFile(p, ''); + } } export async function ls(dir: string) { - const files = await fs.readdir(dir) - const paths = files.map((f) => path.join(dir, f)) - return Promise.all( - paths.map((path) => fs.stat(path).then((stat) => ({ path, stat }))) - ) + const files = await fs.readdir(dir); + const paths = files.map((f) => path.join(dir, f)); + return Promise.all( + paths.map((path) => fs.stat(path).then((stat) => ({ path, stat }))), + ); } export function wait(ms: number, unref = false): Promise { - return new Promise((resolve) => { - const t: any = setTimeout(() => resolve(), ms) - if (unref) t.unref() - }) + return new Promise((resolve) => { + const t: any = setTimeout(() => resolve(), ms); + if (unref) t.unref(); + }); } diff --git a/test/commands/install.test.ts b/test/commands/install.test.ts index fe7c8420..851153e0 100644 --- a/test/commands/install.test.ts +++ b/test/commands/install.test.ts @@ -1,53 +1,53 @@ -import InstallCommand from '../../src/commands/use' -import * as fs from 'fs' -import { mocked } from 'ts-jest/utils' -import { IConfig } from '@oclif/config' +import InstallCommand from '../../src/commands/use'; +import * as fs from 'fs'; +import { mocked } from 'ts-jest/utils'; +import { IConfig } from '@oclif/config'; -const mockFs = mocked(fs, true) +const mockFs = mocked(fs, true); class MockedInstallCommand extends InstallCommand { - public fetchManifest = jest.fn() + public fetchManifest = jest.fn(); - public downloadAndExtract = jest.fn() + public downloadAndExtract = jest.fn(); } describe.skip('Install Command', () => { - let commandInstance: MockedInstallCommand - let config: IConfig - beforeEach(() => { - mockFs.existsSync.mockReturnValue(true) - - config = { - name: 'test', - version: '', - channel: '', - cacheDir: '', - commandIDs: [''], - topics: [], - valid: true, - arch: 'arm64', - platform: 'darwin', - plugins: [], - commands: [], - configDir: '', - pjson: {} as any, - root: '', - bin: '', - } as any - }) - - it.skip('will run an update', async () => { - commandInstance = new MockedInstallCommand([], config) - - await commandInstance.run() - }) - - it.todo( - 'when requesting a channel, will fetch manifest to install the latest version' - ) - it.todo( - 'when requesting a version, will return the explicit version with appropriate URL' - ) - it.todo('will handle an invalid version request') - it.todo('will handle an invalid channel request') -}) + let commandInstance: MockedInstallCommand; + let config: IConfig; + beforeEach(() => { + mockFs.existsSync.mockReturnValue(true); + + config = { + name: 'test', + version: '', + channel: '', + cacheDir: '', + commandIDs: [''], + topics: [], + valid: true, + arch: 'arm64', + platform: 'darwin', + plugins: [], + commands: [], + configDir: '', + pjson: {} as any, + root: '', + bin: '', + } as any; + }); + + it.skip('will run an update', async () => { + commandInstance = new MockedInstallCommand([], config); + + await commandInstance.run(); + }); + + it.todo( + 'when requesting a channel, will fetch manifest to install the latest version', + ); + it.todo( + 'when requesting a version, will return the explicit version with appropriate URL', + ); + it.todo('will handle an invalid version request'); + it.todo('will handle an invalid channel request'); +}); diff --git a/test/commands/update.skip.ts b/test/commands/update.skip.ts index df5468e2..7e81e2af 100644 --- a/test/commands/update.skip.ts +++ b/test/commands/update.skip.ts @@ -1,94 +1,87 @@ -import { expect } from 'chai' -import * as path from 'path' -import * as qq from 'qqjs' +import { expect } from 'chai'; +import * as path from 'path'; +import * as qq from 'qqjs'; -const skipIfWindows = process.platform === 'win32' ? it.skip : it +const skipIfWindows = process.platform === 'win32' ? it.skip : it; describe('update', () => { - skipIfWindows('tests the updater', async () => { - await qq.rm([ - process.env.HOME!, - '.local', - 'share', - 'oclif-example-s3-cli', - ]) - await qq.x( - 'aws s3 rm --recursive s3://oclif-staging/s3-update-example-cli' - ) - const sha = await qq.x.stdout('git', ['rev-parse', '--short', 'HEAD']) - const stdout = await qq.x.stdout('npm', ['pack', '--unsafe-perm']) - const tarball = path.resolve(stdout.split('\n').pop()!) + skipIfWindows('tests the updater', async () => { + await qq.rm([process.env.HOME!, '.local', 'share', 'oclif-example-s3-cli']); + await qq.x( + 'aws s3 rm --recursive s3://oclif-staging/s3-update-example-cli', + ); + const sha = await qq.x.stdout('git', ['rev-parse', '--short', 'HEAD']); + const stdout = await qq.x.stdout('npm', ['pack', '--unsafe-perm']); + const tarball = path.resolve(stdout.split('\n').pop()!); - qq.cd('examples/s3-update-example-cli') - /* eslint-disable require-atomic-updates */ - process.env.EXAMPLE_CLI_DISABLE_AUTOUPDATE = '1' - process.env.YARN_CACHE_FOLDER = path.resolve('tmp', 'yarn') - /* eslint-enable require-atomic-updates */ - await qq.rm(process.env.YARN_CACHE_FOLDER) - const pjson = await qq.readJSON('package.json') - pjson.name = `s3-update-example-cli-${Math.floor( - Math.random() * 100000 - )}` - pjson.oclif.bin = pjson.name - delete pjson.dependencies['@oclif/plugin-update'] - await qq.writeJSON('package.json', pjson) + qq.cd('examples/s3-update-example-cli'); + /* eslint-disable require-atomic-updates */ + process.env.EXAMPLE_CLI_DISABLE_AUTOUPDATE = '1'; + process.env.YARN_CACHE_FOLDER = path.resolve('tmp', 'yarn'); + /* eslint-enable require-atomic-updates */ + await qq.rm(process.env.YARN_CACHE_FOLDER); + const pjson = await qq.readJSON('package.json'); + pjson.name = `s3-update-example-cli-${Math.floor(Math.random() * 100000)}`; + pjson.oclif.bin = pjson.name; + delete pjson.dependencies['@oclif/plugin-update']; + await qq.writeJSON('package.json', pjson); - await qq.rm('yarn.lock') - await qq.x(`yarn add ${tarball}`) - // await qq.x('yarn') + await qq.rm('yarn.lock'); + await qq.x(`yarn add ${tarball}`); + // await qq.x('yarn') - const release = async (version: string) => { - const pjson = await qq.readJSON('package.json') - pjson.version = version - await qq.writeJSON('package.json', pjson) - await qq.x('./node_modules/.bin/oclif-dev pack') - await qq.x('./node_modules/.bin/oclif-dev publish') - } - const checkVersion = async ( - version: string, - nodeVersion = pjson.oclif.update.node.version - ) => { - const stdout = await qq.x.stdout( - `./tmp/${pjson.oclif.bin}/bin/${pjson.oclif.bin}`, - ['version'] - ) - expect(stdout).to.equal( - `${pjson.oclif.bin}/${version} ${process.platform}-${process.arch} node-v${nodeVersion}` - ) - } - const update = async (channel?: string) => { - const f = `tmp/${pjson.oclif.bin}/package.json` - const pj = await qq.readJSON(f) - pj.version = '0.0.0' - await qq.writeJSON(f, pj) - const args = ['update'] - if (channel) args.push(channel) - await qq.x(`./tmp/${pjson.oclif.bin}/bin/${pjson.oclif.bin}`, args) - } - await release('1.0.0') - await checkVersion('1.0.0', process.versions.node) - await release('2.0.0-beta') - await checkVersion(`2.0.0-beta.${sha}`, process.versions.node) - await update() - await checkVersion('1.0.0') - await release('1.0.1') - await checkVersion('1.0.0') - await update() - await checkVersion('1.0.1') - await update() - await checkVersion('1.0.1') - await update('beta') - await checkVersion(`2.0.0-beta.${sha}`) - await release('2.0.1-beta') - await checkVersion(`2.0.0-beta.${sha}`) - await update() - await checkVersion(`2.0.1-beta.${sha}`) - await update() - await checkVersion(`2.0.1-beta.${sha}`) - await release('1.0.3') - await update() - await checkVersion(`2.0.1-beta.${sha}`) - await update('stable') - await checkVersion('1.0.3') - }) -}) + const release = async (version: string) => { + const pjson = await qq.readJSON('package.json'); + pjson.version = version; + await qq.writeJSON('package.json', pjson); + await qq.x('./node_modules/.bin/oclif-dev pack'); + await qq.x('./node_modules/.bin/oclif-dev publish'); + }; + const checkVersion = async ( + version: string, + nodeVersion = pjson.oclif.update.node.version, + ) => { + const stdout = await qq.x.stdout( + `./tmp/${pjson.oclif.bin}/bin/${pjson.oclif.bin}`, + ['version'], + ); + expect(stdout).to.equal( + `${pjson.oclif.bin}/${version} ${process.platform}-${process.arch} node-v${nodeVersion}`, + ); + }; + const update = async (channel?: string) => { + const f = `tmp/${pjson.oclif.bin}/package.json`; + const pj = await qq.readJSON(f); + pj.version = '0.0.0'; + await qq.writeJSON(f, pj); + const args = ['update']; + if (channel) args.push(channel); + await qq.x(`./tmp/${pjson.oclif.bin}/bin/${pjson.oclif.bin}`, args); + }; + await release('1.0.0'); + await checkVersion('1.0.0', process.versions.node); + await release('2.0.0-beta'); + await checkVersion(`2.0.0-beta.${sha}`, process.versions.node); + await update(); + await checkVersion('1.0.0'); + await release('1.0.1'); + await checkVersion('1.0.0'); + await update(); + await checkVersion('1.0.1'); + await update(); + await checkVersion('1.0.1'); + await update('beta'); + await checkVersion(`2.0.0-beta.${sha}`); + await release('2.0.1-beta'); + await checkVersion(`2.0.0-beta.${sha}`); + await update(); + await checkVersion(`2.0.1-beta.${sha}`); + await update(); + await checkVersion(`2.0.1-beta.${sha}`); + await release('1.0.3'); + await update(); + await checkVersion(`2.0.1-beta.${sha}`); + await update('stable'); + await checkVersion('1.0.3'); + }); +}); diff --git a/test/commands/update.test.ts b/test/commands/update.test.ts index b2b1496e..56f51161 100644 --- a/test/commands/update.test.ts +++ b/test/commands/update.test.ts @@ -1,49 +1,49 @@ -import UpdateCommand from '../../src/commands/update' -import * as fs from 'fs' -import { mocked } from 'ts-jest/utils' -import { IConfig } from '@oclif/config' +import UpdateCommand from '../../src/commands/update'; +import * as fs from 'fs'; +import { mocked } from 'ts-jest/utils'; +import { IConfig } from '@oclif/config'; -const mockFs = mocked(fs, true) +const mockFs = mocked(fs, true); class MockedUpdateCommand extends UpdateCommand { - constructor(a: string[], v: IConfig) { - super(a, v) - this.fetchManifest = jest.fn() - this.downloadAndExtract = jest.fn() - } + constructor(a: string[], v: IConfig) { + super(a, v); + this.fetchManifest = jest.fn(); + this.downloadAndExtract = jest.fn(); + } } describe('Update Command', () => { - let commandInstance: MockedUpdateCommand - let config: IConfig - beforeEach(() => { - mockFs.existsSync.mockReturnValue(true) + let commandInstance: MockedUpdateCommand; + let config: IConfig; + beforeEach(() => { + mockFs.existsSync.mockReturnValue(true); - config = { - name: 'test', - version: '', - channel: '', - cacheDir: '', - commandIDs: [''], - topics: [], - valid: true, - arch: 'arm64', - platform: 'darwin', - plugins: [], - commands: [], - configDir: '', - pjson: {} as any, - root: '', - bin: '', - } as any - }) + config = { + name: 'test', + version: '', + channel: '', + cacheDir: '', + commandIDs: [''], + topics: [], + valid: true, + arch: 'arm64', + platform: 'darwin', + plugins: [], + commands: [], + configDir: '', + pjson: {} as any, + root: '', + bin: '', + } as any; + }); - it.skip('will run an update', async () => { - commandInstance = new MockedUpdateCommand([], config) + it.skip('will run an update', async () => { + commandInstance = new MockedUpdateCommand([], config); - await commandInstance.run() - }) + await commandInstance.run(); + }); - it.todo('Will update to the current channel when no options are provided') - it.todo('Will update to a new channel when provided in args') -}) + it.todo('Will update to the current channel when no options are provided'); + it.todo('Will update to a new channel when provided in args'); +}); diff --git a/test/commands/use.test.ts b/test/commands/use.test.ts index 711cfd4d..9545eb0b 100644 --- a/test/commands/use.test.ts +++ b/test/commands/use.test.ts @@ -1,260 +1,260 @@ -import UseCommand from '../../src/commands/use' -import * as fs from 'fs-extra' -import { mocked } from 'ts-jest/utils' -import { IConfig } from '@oclif/config' -import { IManifest } from '@oclif/dev-cli' - -jest.mock('fs-extra') -const mockFs = mocked(fs, true) +import UseCommand from '../../src/commands/use'; +import * as fs from 'fs-extra'; +import { mocked } from 'ts-jest/utils'; +import { IConfig } from '@oclif/config'; +import { IManifest } from '@oclif/dev-cli'; + +jest.mock('fs-extra'); +const mockFs = mocked(fs, true); class MockedUseCommand extends UseCommand { - public channel!: string + public channel!: string; - public clientRoot!: string + public clientRoot!: string; - public currentVersion!: string + public currentVersion!: string; - public updatedVersion!: string + public updatedVersion!: string; - public fetchManifest = jest.fn() + public fetchManifest = jest.fn(); - public downloadAndExtract = jest.fn() + public downloadAndExtract = jest.fn(); } describe('Use Command', () => { - let commandInstance: MockedUseCommand - let config: IConfig - beforeEach(() => { - mockFs.existsSync.mockReturnValue(true) - - config = { - name: 'test', - version: '1.0.0', - channel: 'stable', - cacheDir: '', - commandIDs: [''], - runHook: jest.fn(), - topics: [], - valid: true, - arch: 'arm64', - platform: 'darwin', - plugins: [], - commands: [], - configDir: '', - dataDir: '', - pjson: {} as any, - root: '', - bin: 'cli', - scopedEnvVarKey: jest.fn(), - scopedEnvVar: jest.fn(), - } as any - }) - - it('when provided a channel, uses the latest version available locally', async () => { - mockFs.readdirSync.mockReturnValue([ - '1.0.0-next.2', - '1.0.0-next.3', - '1.0.1', - '1.0.0-alpha.0', - ] as any) - - // oclif-example use next - commandInstance = new MockedUseCommand(['next'], config) - - commandInstance.fetchManifest.mockResolvedValue({}) - - await commandInstance.run() - - expect(commandInstance.downloadAndExtract).not.toBeCalled() - expect(commandInstance.updatedVersion).toBe('1.0.0-next.3') - expect(commandInstance.channel).toBe('next') - }) - - it('when provided stable channel, uses only release versions', async () => { - mockFs.readdirSync.mockReturnValue([ - '1.0.0-next.2', - '1.0.3', - '1.0.0-next.3', - '1.0.1', - '1.0.0-alpha.0', - ] as any) - - // oclif-example use next - commandInstance = new MockedUseCommand(['stable'], config) - - commandInstance.fetchManifest.mockResolvedValue({}) - - await commandInstance.run() - - expect(commandInstance.downloadAndExtract).not.toBeCalled() - expect(commandInstance.updatedVersion).toBe('1.0.3') - expect(commandInstance.channel).toBe('stable') - }) - - it('when provided a version, will directly switch to it locally', async () => { - mockFs.readdirSync.mockReturnValue([ - '1.0.0-next.2', - '1.0.0-next.3', - '1.0.1', - '1.0.0-alpha.0', - ] as any) - - // oclif-example use '1.0.0-alpha.0' - commandInstance = new MockedUseCommand(['1.0.0-alpha.0'], config) - - commandInstance.fetchManifest.mockResolvedValue({ - channel: 'alpha', - } as IManifest) - - await commandInstance.run() - - expect(commandInstance.downloadAndExtract).not.toBeCalled() - expect(commandInstance.updatedVersion).toBe('1.0.0-alpha.0') - }) - - it('will print a warning when the requested static version is not available locally', async () => { - mockFs.readdirSync.mockReturnValue([ - '1.0.0-next.2', - '1.0.0-next.3', - '1.0.1', - '1.0.0-alpha.0', - ] as any) - - // oclif-example use '1.0.0-alpha.3' - commandInstance = new MockedUseCommand(['1.0.0-alpha.3'], config) - - commandInstance.fetchManifest.mockResolvedValue({}) - - let err - - try { - await commandInstance.run() - } catch (error) { - err = error - } - - const localVersionsMsg = `Locally installed versions available: \n${[ - '1.0.0-next.2', - '1.0.0-next.3', - '1.0.1', - '1.0.0-alpha.0', - ] - .map((version) => `\t${version}`) - .join('\n')}\n` - - expect(commandInstance.downloadAndExtract).not.toBeCalled() - expect(err.message).toBe( - `Requested version could not be found locally. ${localVersionsMsg}` - ) - }) - - it('will ignore partials when trying to update to stable', async () => { - mockFs.readdirSync.mockReturnValue([ - '1.0.0-next.2', - '1.0.0-next.3', - '1.0.1', - '1.0.2.partial.0000', - '1.0.1.partial.0000', - '1.0.0-alpha.0', - ] as any) - - // oclif-example use '1.0.0-alpha.0' - commandInstance = new MockedUseCommand(['stable'], config) - - commandInstance.fetchManifest.mockResolvedValue({ - channel: 'stable', - } as IManifest) - - await commandInstance.run() - - expect(commandInstance.downloadAndExtract).not.toBeCalled() - expect(commandInstance.updatedVersion).toBe('1.0.1') - }) - - it('will ignore partials when trying to update to a prerelease', async () => { - mockFs.readdirSync.mockReturnValue([ - '1.0.0-next.2', - '1.0.0-next.3', - '1.0.1', - '1.0.2.partial.0000', - '1.0.1.partial.0000', - '1.0.0-alpha.0', - '1.0.0-alpha.1.partial.0', - '1.0.0-alpha.2.partial.12', - ] as any) - - // oclif-example use '1.0.0-alpha.0' - commandInstance = new MockedUseCommand(['alpha'], config) - - commandInstance.fetchManifest.mockResolvedValue({ - channel: 'stable', - } as IManifest) - - await commandInstance.run() - - expect(commandInstance.downloadAndExtract).not.toBeCalled() - expect(commandInstance.updatedVersion).toBe('1.0.0-alpha.0') - }) - - it('will print a warning when the requested channel is not available locally', async () => { - mockFs.readdirSync.mockReturnValue([ - '1.0.0-next.2', - '1.0.0-next.3', - '1.0.1', - '1.0.0-alpha.0', - ] as any) - - // oclif-example use test - commandInstance = new MockedUseCommand(['beta'], config) - - commandInstance.fetchManifest.mockResolvedValue({}) - - let err - - try { - await commandInstance.run() - } catch (error) { - err = error - } - - const localVersionsMsg = `Locally installed versions available: \n${[ - '1.0.0-next.2', - '1.0.0-next.3', - '1.0.1', - '1.0.0-alpha.0', - ] - .map((version) => `\t${version}`) - .join('\n')}\n` - - expect(commandInstance.downloadAndExtract).not.toBeCalled() - expect(err.message).toBe( - `Requested version could not be found locally. ${localVersionsMsg}` - ) - }) - - it('will throw an error when invalid channel is provided', async () => { - mockFs.readdirSync.mockReturnValue([ - '1.0.0-next.2', - '1.0.0-next.3', - '1.0.1', - '1.0.0-alpha.0', - ] as any) - - // oclif-example use test - commandInstance = new MockedUseCommand(['test'], config) - - commandInstance.fetchManifest.mockResolvedValue({}) - - let err - - try { - await commandInstance.run() - } catch (error) { - err = error - } - - expect(commandInstance.downloadAndExtract).not.toBeCalled() - expect(err.message).toBe( - 'Invalid argument provided: test. Please specify either a valid channel (alpha, beta, next, stable) or an explicit version (ex. 2.68.13)' - ) - }) -}) + let commandInstance: MockedUseCommand; + let config: IConfig; + beforeEach(() => { + mockFs.existsSync.mockReturnValue(true); + + config = { + name: 'test', + version: '1.0.0', + channel: 'stable', + cacheDir: '', + commandIDs: [''], + runHook: jest.fn(), + topics: [], + valid: true, + arch: 'arm64', + platform: 'darwin', + plugins: [], + commands: [], + configDir: '', + dataDir: '', + pjson: {} as any, + root: '', + bin: 'cli', + scopedEnvVarKey: jest.fn(), + scopedEnvVar: jest.fn(), + } as any; + }); + + it('when provided a channel, uses the latest version available locally', async () => { + mockFs.readdirSync.mockReturnValue([ + '1.0.0-next.2', + '1.0.0-next.3', + '1.0.1', + '1.0.0-alpha.0', + ] as any); + + // oclif-example use next + commandInstance = new MockedUseCommand(['next'], config); + + commandInstance.fetchManifest.mockResolvedValue({}); + + await commandInstance.run(); + + expect(commandInstance.downloadAndExtract).not.toBeCalled(); + expect(commandInstance.updatedVersion).toBe('1.0.0-next.3'); + expect(commandInstance.channel).toBe('next'); + }); + + it('when provided stable channel, uses only release versions', async () => { + mockFs.readdirSync.mockReturnValue([ + '1.0.0-next.2', + '1.0.3', + '1.0.0-next.3', + '1.0.1', + '1.0.0-alpha.0', + ] as any); + + // oclif-example use next + commandInstance = new MockedUseCommand(['stable'], config); + + commandInstance.fetchManifest.mockResolvedValue({}); + + await commandInstance.run(); + + expect(commandInstance.downloadAndExtract).not.toBeCalled(); + expect(commandInstance.updatedVersion).toBe('1.0.3'); + expect(commandInstance.channel).toBe('stable'); + }); + + it('when provided a version, will directly switch to it locally', async () => { + mockFs.readdirSync.mockReturnValue([ + '1.0.0-next.2', + '1.0.0-next.3', + '1.0.1', + '1.0.0-alpha.0', + ] as any); + + // oclif-example use '1.0.0-alpha.0' + commandInstance = new MockedUseCommand(['1.0.0-alpha.0'], config); + + commandInstance.fetchManifest.mockResolvedValue({ + channel: 'alpha', + } as IManifest); + + await commandInstance.run(); + + expect(commandInstance.downloadAndExtract).not.toBeCalled(); + expect(commandInstance.updatedVersion).toBe('1.0.0-alpha.0'); + }); + + it('will print a warning when the requested static version is not available locally', async () => { + mockFs.readdirSync.mockReturnValue([ + '1.0.0-next.2', + '1.0.0-next.3', + '1.0.1', + '1.0.0-alpha.0', + ] as any); + + // oclif-example use '1.0.0-alpha.3' + commandInstance = new MockedUseCommand(['1.0.0-alpha.3'], config); + + commandInstance.fetchManifest.mockResolvedValue({}); + + let err; + + try { + await commandInstance.run(); + } catch (error) { + err = error; + } + + const localVersionsMsg = `Locally installed versions available: \n${[ + '1.0.0-next.2', + '1.0.0-next.3', + '1.0.1', + '1.0.0-alpha.0', + ] + .map((version) => `\t${version}`) + .join('\n')}\n`; + + expect(commandInstance.downloadAndExtract).not.toBeCalled(); + expect(err.message).toBe( + `Requested version could not be found locally. ${localVersionsMsg}`, + ); + }); + + it('will ignore partials when trying to update to stable', async () => { + mockFs.readdirSync.mockReturnValue([ + '1.0.0-next.2', + '1.0.0-next.3', + '1.0.1', + '1.0.2.partial.0000', + '1.0.1.partial.0000', + '1.0.0-alpha.0', + ] as any); + + // oclif-example use '1.0.0-alpha.0' + commandInstance = new MockedUseCommand(['stable'], config); + + commandInstance.fetchManifest.mockResolvedValue({ + channel: 'stable', + } as IManifest); + + await commandInstance.run(); + + expect(commandInstance.downloadAndExtract).not.toBeCalled(); + expect(commandInstance.updatedVersion).toBe('1.0.1'); + }); + + it('will ignore partials when trying to update to a prerelease', async () => { + mockFs.readdirSync.mockReturnValue([ + '1.0.0-next.2', + '1.0.0-next.3', + '1.0.1', + '1.0.2.partial.0000', + '1.0.1.partial.0000', + '1.0.0-alpha.0', + '1.0.0-alpha.1.partial.0', + '1.0.0-alpha.2.partial.12', + ] as any); + + // oclif-example use '1.0.0-alpha.0' + commandInstance = new MockedUseCommand(['alpha'], config); + + commandInstance.fetchManifest.mockResolvedValue({ + channel: 'stable', + } as IManifest); + + await commandInstance.run(); + + expect(commandInstance.downloadAndExtract).not.toBeCalled(); + expect(commandInstance.updatedVersion).toBe('1.0.0-alpha.0'); + }); + + it('will print a warning when the requested channel is not available locally', async () => { + mockFs.readdirSync.mockReturnValue([ + '1.0.0-next.2', + '1.0.0-next.3', + '1.0.1', + '1.0.0-alpha.0', + ] as any); + + // oclif-example use test + commandInstance = new MockedUseCommand(['beta'], config); + + commandInstance.fetchManifest.mockResolvedValue({}); + + let err; + + try { + await commandInstance.run(); + } catch (error) { + err = error; + } + + const localVersionsMsg = `Locally installed versions available: \n${[ + '1.0.0-next.2', + '1.0.0-next.3', + '1.0.1', + '1.0.0-alpha.0', + ] + .map((version) => `\t${version}`) + .join('\n')}\n`; + + expect(commandInstance.downloadAndExtract).not.toBeCalled(); + expect(err.message).toBe( + `Requested version could not be found locally. ${localVersionsMsg}`, + ); + }); + + it('will throw an error when invalid channel is provided', async () => { + mockFs.readdirSync.mockReturnValue([ + '1.0.0-next.2', + '1.0.0-next.3', + '1.0.1', + '1.0.0-alpha.0', + ] as any); + + // oclif-example use test + commandInstance = new MockedUseCommand(['test'], config); + + commandInstance.fetchManifest.mockResolvedValue({}); + + let err; + + try { + await commandInstance.run(); + } catch (error) { + err = error; + } + + expect(commandInstance.downloadAndExtract).not.toBeCalled(); + expect(err.message).toBe( + 'Invalid argument provided: test. Please specify either a valid channel (alpha, beta, next, stable) or an explicit version (ex. 2.68.13)', + ); + }); +});