diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d5263d1c1..68f90185d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,6 +52,29 @@ jobs: - run: yarn install --network-timeout 600000 - run: yarn build - run: yarn test:e2e + esm-cjs-interop: + needs: linux-unit-tests + strategy: + matrix: + os: ["ubuntu-latest", "windows-latest"] + node_version: [lts/-1, lts/*, latest] + exclude: + - os: windows-latest + node_version: lts/* + - os: windows-latest + node_version: lts/-1 + fail-fast: false + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node_version }} + cache: yarn + - run: yarn install --network-timeout 600000 + - run: yarn build + - run: yarn test:esm-cjs nuts: needs: linux-unit-tests uses: salesforcecli/github-workflows/.github/workflows/externalNut.yml@main diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5e5ece4..13f803a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## [2.11.10](https://github.com/oclif/core/compare/2.11.9...2.11.10) (2023-08-23) + + +### Bug Fixes + +* add getPluginsList to Config interface ([cba7a75](https://github.com/oclif/core/commit/cba7a755cf5d0ff4886edd0db33d911441979984)) + + + +## [2.11.9](https://github.com/oclif/core/compare/2.11.8...2.11.9) (2023-08-23) + + +### Bug Fixes + +* add getPluginsList ([b01083f](https://github.com/oclif/core/commit/b01083fb7132f3b3b35b98a6d43996b1713ca5ef)) + + + ## [2.11.8](https://github.com/oclif/core/compare/2.11.7...2.11.8) (2023-08-08) diff --git a/package.json b/package.json index 340159049..e1fb9395b 100644 --- a/package.json +++ b/package.json @@ -66,13 +66,14 @@ "chai": "^4.3.7", "chai-as-promised": "^7.1.1", "commitlint": "^12.1.4", + "cross-env": "^7.0.3", "eslint": "^7.32.0", "eslint-config-oclif": "^4.0.0", "eslint-config-oclif-typescript": "^1.0.3", "fancy-test": "^2.0.16", "globby": "^11.1.0", "husky": "6", - "mocha": "^8.4.0", + "mocha": "^10.2.0", "nock": "^13.3.0", "proxyquire": "^2.1.3", "shelljs": "^0.8.5", @@ -110,14 +111,15 @@ "scripts": { "build": "shx rm -rf lib && tsc", "commitlint": "commitlint", + "compile": "tsc", "lint": "eslint . --ext .ts --config .eslintrc", "posttest": "yarn lint", - "compile": "tsc", "prepack": "yarn run build", - "test": "mocha --forbid-only \"test/**/*.test.ts\"", - "test:e2e": "mocha --forbid-only \"test/**/*.e2e.ts\" --timeout 1200000", "pretest": "yarn build --noEmit && tsc -p test --noEmit --skipLibCheck", - "test:perf": "ts-node test/perf/parser.perf.ts" + "test:e2e": "mocha --forbid-only \"test/**/*.e2e.ts\" --parallel --timeout 1200000", + "test:esm-cjs": "cross-env DEBUG=e2e:* ts-node test/integration/esm-cjs.ts", + "test:perf": "ts-node test/perf/parser.perf.ts", + "test": "mocha --forbid-only \"test/**/*.test.ts\"" }, "types": "lib/index.d.ts" } diff --git a/src/config/config.ts b/src/config/config.ts index 078069583..0e3382754 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -111,6 +111,8 @@ export class Config implements IConfig { private _commandIDs!: string[] + private static _rootPlugin: Plugin.Plugin + constructor(public options: Options) {} static async load(opts: LoadOptions = module.filename || __dirname): Promise { @@ -127,11 +129,16 @@ export class Config implements IConfig { return config } + static get rootPlugin(): Plugin.Plugin | undefined { + return Config._rootPlugin + } + // eslint-disable-next-line complexity public async load(): Promise { settings.performanceEnabled = (settings.performanceEnabled === undefined ? this.options.enablePerf : settings.performanceEnabled) ?? false const plugin = new Plugin.Plugin({root: this.options.root}) await plugin.load() + Config._rootPlugin = plugin this.plugins.push(plugin) this.root = plugin.root this.pjson = plugin.pjson @@ -544,6 +551,10 @@ export class Config implements IConfig { return url.toString() } + public getPluginsList(): IPlugin[] { + return this.plugins + } + protected dir(category: 'cache' | 'data' | 'config'): string { const base = process.env[`XDG_${category.toUpperCase()}_HOME`] || (this.windows && process.env.LOCALAPPDATA) || diff --git a/src/config/plugin.ts b/src/config/plugin.ts index 0320c5092..1a6d253cf 100644 --- a/src/config/plugin.ts +++ b/src/config/plugin.ts @@ -105,6 +105,8 @@ export class Plugin implements IPlugin { type!: string + moduleType!: 'module' | 'commonjs' + root!: string alias!: string @@ -150,6 +152,7 @@ export class Plugin implements IPlugin { this.root = root this._debug('reading %s plugin %s', this.type, root) this.pjson = await loadJSON(path.join(root, 'package.json')) + this.moduleType = this.pjson.type === 'module' ? 'module' : 'commonjs' this.name = this.pjson.name this.alias = this.options.name ?? this.pjson.name const pjsonPath = path.join(root, 'package.json') @@ -185,7 +188,7 @@ export class Plugin implements IPlugin { public get commandsDir(): string | undefined { if (this._commandsDir) return this._commandsDir - this._commandsDir = tsPath(this.root, this.pjson.oclif.commands, this.type) + this._commandsDir = tsPath(this.root, this.pjson.oclif.commands, this) return this._commandsDir } diff --git a/src/config/ts-node.ts b/src/config/ts-node.ts index eb35f6840..478f5e2dc 100644 --- a/src/config/ts-node.ts +++ b/src/config/ts-node.ts @@ -2,24 +2,27 @@ import * as fs from 'fs' import * as path from 'path' import * as TSNode from 'ts-node' -import {TSConfig} from '../interfaces/ts-config' +import {TSConfig, Plugin} from '../interfaces' import {settings} from '../settings' import {isProd} from '../util' import {Debug} from './util' +import {Config} from './config' +import {memoizedWarn} from '../errors' // eslint-disable-next-line new-cap const debug = Debug('ts-node') -const TYPE_ROOTS = [`${__dirname}/../node_modules/@types`] -const ROOT_DIRS: string[] = [] +const TS_CONFIGS: Record = {} +const REGISTERED = new Set() function loadTSConfig(root: string): TSConfig | undefined { + if (TS_CONFIGS[root]) return TS_CONFIGS[root] const tsconfigPath = path.join(root, 'tsconfig.json') let typescript: typeof import('typescript') | undefined try { typescript = require('typescript') } catch { try { - typescript = require(root + '/node_modules/typescript') + typescript = require(path.join(root, 'node_modules', 'typescript')) } catch {} } @@ -34,47 +37,70 @@ function loadTSConfig(root: string): TSConfig | undefined { 'did not contain a "compilerOptions" section.') } + TS_CONFIGS[root] = tsconfig return tsconfig } } -function registerTSNode(root: string) { +function registerTSNode(root: string): TSConfig | undefined { const tsconfig = loadTSConfig(root) if (!tsconfig) return + if (REGISTERED.has(root)) return tsconfig debug('registering ts-node at', root) const tsNodePath = require.resolve('ts-node', {paths: [root, __dirname]}) + debug('ts-node path:', tsNodePath) const tsNode: typeof TSNode = require(tsNodePath) - TYPE_ROOTS.push(`${root}/node_modules/@types`) + const typeRoots = [ + path.join(root, 'node_modules', '@types'), + ] + + const rootDirs: string[] = [] if (tsconfig.compilerOptions.rootDirs) { - ROOT_DIRS.push(...tsconfig.compilerOptions.rootDirs.map(r => path.join(root, r))) + for (const r of tsconfig.compilerOptions.rootDirs) { + rootDirs.push(path.join(root, r)) + } + } else if (tsconfig.compilerOptions.rootDir) { + rootDirs.push(path.join(root, tsconfig.compilerOptions.rootDir)) } else { - ROOT_DIRS.push(`${root}/src`) + rootDirs.push(path.join(root, 'src')) } - const cwd = process.cwd() - try { - process.chdir(root) - tsNode.register({ - skipProject: true, - transpileOnly: true, - compilerOptions: { - esModuleInterop: tsconfig.compilerOptions.esModuleInterop, - target: tsconfig.compilerOptions.target || 'es2017', - experimentalDecorators: tsconfig.compilerOptions.experimentalDecorators || false, - emitDecoratorMetadata: tsconfig.compilerOptions.emitDecoratorMetadata || false, - module: 'commonjs', - sourceMap: true, - rootDirs: ROOT_DIRS, - typeRoots: TYPE_ROOTS, - jsx: 'react', - }, - }) - return tsconfig - } finally { - process.chdir(cwd) + const conf: TSNode.RegisterOptions = { + compilerOptions: { + esModuleInterop: tsconfig.compilerOptions.esModuleInterop, + target: tsconfig.compilerOptions.target ?? 'es2019', + experimentalDecorators: tsconfig.compilerOptions.experimentalDecorators ?? false, + emitDecoratorMetadata: tsconfig.compilerOptions.emitDecoratorMetadata ?? false, + module: tsconfig.compilerOptions.module ?? 'commonjs', + sourceMap: tsconfig.compilerOptions.sourceMap ?? true, + rootDirs, + typeRoots, + }, + skipProject: true, + transpileOnly: true, + esm: tsconfig['ts-node']?.esm ?? true, + scope: true, + scopeDir: root, + cwd: root, + experimentalSpecifierResolution: tsconfig['ts-node']?.experimentalSpecifierResolution ?? 'explicit', + } + + if (tsconfig.compilerOptions.moduleResolution) { + // @ts-expect-error TSNode.RegisterOptions.compilerOptions is typed as a plain object + conf.compilerOptions.moduleResolution = tsconfig.compilerOptions.moduleResolution } + + if (tsconfig.compilerOptions.jsx) { + // @ts-expect-error TSNode.RegisterOptions.compilerOptions is typed as a plain object + conf.compilerOptions.jsx = tsconfig.compilerOptions.jsx + } + + tsNode.register(conf) + REGISTERED.add(root) + + return tsconfig } /** @@ -82,23 +108,53 @@ function registerTSNode(root: string) { * this is for developing typescript plugins/CLIs * if there is a tsconfig and the original sources exist, it attempts to require ts-node */ -export function tsPath(root: string, orig: string, type?: string): string -export function tsPath(root: string, orig: string | undefined, type?: string): string | undefined -export function tsPath(root: string, orig: string | undefined, type?: string): string | undefined { +export function tsPath(root: string, orig: string, plugin: Plugin): string +export function tsPath(root: string, orig: string | undefined, plugin?: Plugin): string | undefined +// eslint-disable-next-line complexity +export function tsPath(root: string, orig: string | undefined, plugin?: Plugin): string | undefined { if (!orig) return orig orig = orig.startsWith(root) ? orig : path.join(root, orig) - const skipTSNode = - // the CLI specifically turned it off - (settings.tsnodeEnabled === false) || - // the CLI didn't specify ts-node and it is production - (settings.tsnodeEnabled === undefined && isProd()) + // NOTE: The order of these checks matter! - // We always want to load the tsconfig for linked plugins. - if (skipTSNode && type !== 'link') return orig + if (settings.tsnodeEnabled === false) { + debug(`Skipping ts-node registration for ${root} because tsNodeEnabled is explicitly set to false`) + return orig + } + + // Skip ts-node registration if plugin is an ESM plugin executing from a CJS plugin + if (plugin?.moduleType === 'module' && Config.rootPlugin?.moduleType === 'commonjs') { + debug(`Skipping ts-node registration for ${root} because it's an ESM module but the root plugin is CommonJS`) + if (plugin.type === 'link') + memoizedWarn(`${plugin.name} is a linked ESM module and cannot be auto-compiled from a CommonJS root plugin. Existing compiled source will be used instead.`) + + return orig + } + + // If plugin is an ESM plugin being executed from an ESM root plugin, check to see if ts-node/esm loader has been set + // either in the NODE_OPTIONS env var or from the exec args. If the ts-node/esm loader has NOT been loaded then we want + // to skip ts-node registration so that it falls back on the compiled source. + if (plugin?.moduleType === 'module') { + const tsNodeEsmLoaderInExecArgv = process.execArgv.includes('--loader') && process.execArgv.includes('ts-node/esm') + const tsNodeEsmLoaderInNodeOptions = process.env.NODE_OPTIONS?.includes('--loader=ts-node/esm') ?? false + if (!tsNodeEsmLoaderInExecArgv && !tsNodeEsmLoaderInNodeOptions) { + debug(`Skipping ts-node registration for ${root} because it's an ESM module but the ts-node/esm loader hasn't been run`) + debug('try setting NODE_OPTIONS="--loader ts-node/esm" in your environment.') + if (plugin.type === 'link') { + memoizedWarn(`${plugin.name} is a linked ESM module and cannot be auto-compiled without setting NODE_OPTIONS="--loader=ts-node/esm" in the environment. Existing compiled source will be used instead.`) + } + + return orig + } + } + + if (settings.tsnodeEnabled === undefined && isProd() && plugin?.type !== 'link') { + debug(`Skipping ts-node registration for ${root} because NODE_ENV is NOT "test" or "development"`) + return orig + } try { - const tsconfig = type === 'link' ? registerTSNode(root) : loadTSConfig(root) + const tsconfig = registerTSNode(root) if (!tsconfig) return orig const {rootDir, rootDirs, outDir} = tsconfig.compilerOptions const rootDirPath = rootDir || (rootDirs || [])[0] diff --git a/src/errors/errors/cli.ts b/src/errors/errors/cli.ts index 67df36c92..3fb241c7f 100644 --- a/src/errors/errors/cli.ts +++ b/src/errors/errors/cli.ts @@ -24,11 +24,13 @@ export class CLIError extends Error implements OclifError { oclif: OclifError['oclif'] = {} code?: string + suggestions?: string[] constructor(error: string | Error, options: { exit?: number | false } & PrettyPrintableError = {}) { super(error instanceof Error ? error.message : error) addOclifExitCode(this, options) this.code = options.code + this.suggestions = options.suggestions } get stack(): string { diff --git a/src/errors/index.ts b/src/errors/index.ts index e3810907d..4fbbbfe0e 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -54,3 +54,10 @@ export function warn(input: string | Error): void { console.error(message) if (config.errorLogger) config.errorLogger.log(err?.stack ?? '') } + +const WARNINGS = new Set() +export function memoizedWarn(input: string | Error): void { + if (!WARNINGS.has(input)) warn(input) + + WARNINGS.add(input) +} diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 28ec10f1e..cc75d0afa 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -132,6 +132,7 @@ export interface Config { s3Url(key: string): string; s3Key(type: 'versioned' | 'unversioned', ext: '.tar.gz' | '.tar.xz', options?: Config.s3Key.Options): string; s3Key(type: keyof PJSON.S3.Templates, options?: Config.s3Key.Options): string; + getPluginsList(): Plugin[]; } export namespace Config { diff --git a/src/interfaces/pjson.ts b/src/interfaces/pjson.ts index 13c5818f8..2834167ce 100644 --- a/src/interfaces/pjson.ts +++ b/src/interfaces/pjson.ts @@ -9,6 +9,7 @@ export interface PJSON { bin?: string; dirname?: string; hooks?: Record; + plugins?: string[]; }; } diff --git a/src/interfaces/plugin.ts b/src/interfaces/plugin.ts index 303f20846..617dc74ad 100644 --- a/src/interfaces/plugin.ts +++ b/src/interfaces/plugin.ts @@ -52,6 +52,10 @@ export interface Plugin { * examples: core, link, user, dev */ type: string; + /** + * Plugin is written in ESM or CommonJS + */ + moduleType: 'module' | 'commonjs'; /** * base path of plugin */ diff --git a/src/interfaces/ts-config.ts b/src/interfaces/ts-config.ts index b65654178..d871120e2 100644 --- a/src/interfaces/ts-config.ts +++ b/src/interfaces/ts-config.ts @@ -7,5 +7,14 @@ export interface TSConfig { esModuleInterop?: boolean; experimentalDecorators?: boolean; emitDecoratorMetadata?: boolean; + module?: string; + moduleResolution?: string; + sourceMap?: boolean; + jsx?: boolean; }; + 'ts-node'?: { + esm?: boolean; + experimentalSpecifierResolution?: 'node' | 'explicit'; + scope?: boolean; + } } diff --git a/src/main.ts b/src/main.ts index f5e0792d5..46e4b95f5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,10 +8,11 @@ import {Config} from './config' import {getHelpFlagAdditions, loadHelpClass, normalizeArgv} from './help' import {settings} from './settings' import {Errors, flush} from '.' -import {join, dirname} from 'path' import {stdout} from './cli-ux/stream' import {Performance} from './performance' +const debug = require('debug')('oclif:main') + const log = (message = '', ...args: any[]) => { message = typeof message === 'string' ? message : inspect(message) stdout.write(format(message, ...args) + '\n') @@ -47,6 +48,10 @@ export async function run(argv?: string[], options?: Interfaces.LoadOptions): Pr Performance.debug() } + debug(`process.execPath: ${process.execPath}`) + debug(`process.execArgv: ${process.execArgv}`) + debug('process.argv: %O', process.argv) + argv = argv ?? process.argv.slice(2) // Handle the case when a file URL string or URL is passed in such as 'import.meta.url'; covert to file path. if (options && ((typeof options === 'string' && options.startsWith('file://')) || options instanceof URL)) { @@ -100,10 +105,6 @@ export async function run(argv?: string[], options?: Interfaces.LoadOptions): Pr } } -function getTsConfigPath(dir: string, type: 'esm' | 'cjs'): string { - return type === 'cjs' ? join(dir, '..', 'tsconfig.json') : join(dirname(fileURLToPath(dir)), '..', 'tsconfig.json') -} - /** * Load and run oclif CLI * @@ -112,47 +113,42 @@ function getTsConfigPath(dir: string, type: 'esm' | 'cjs'): string { * * @example For ESM dev.js * ``` - * #!/usr/bin/env ts-node - * // eslint-disable-next-line node/shebang + * #!/usr/bin/env node * (async () => { * const oclif = await import('@oclif/core') - * await oclif.execute({type: 'esm', development: true, dir: import.meta.url}) + * await oclif.execute({development: true, dir: import.meta.url}) * })() * ``` * * @example For ESM run.js * ``` * #!/usr/bin/env node - * // eslint-disable-next-line node/shebang * (async () => { * const oclif = await import('@oclif/core') - * await oclif.execute({type: 'esm', dir: import.meta.url}) + * await oclif.execute({dir: import.meta.url}) * })() * ``` * * @example For CJS dev.js * ``` * #!/usr/bin/env node - * // eslint-disable-next-line node/shebang * (async () => { * const oclif = await import('@oclif/core') - * await oclif.execute({type: 'cjs', development: true, dir: __dirname}) + * await oclif.execute({development: true, dir: __dirname}) * })() * ``` * * @example For CJS run.js * ``` * #!/usr/bin/env node - * // eslint-disable-next-line node/shebang * (async () => { * const oclif = await import('@oclif/core') - * await oclif.execute({type: 'cjs', dir: import.meta.url}) + * await oclif.execute({dir: __dirname}) * })() * ``` */ export async function execute( options: { - type: 'cjs' | 'esm'; dir: string; args?: string[]; loadOptions?: Interfaces.LoadOptions; @@ -162,9 +158,6 @@ export async function execute( if (options.development) { // In dev mode -> use ts-node and dev plugins process.env.NODE_ENV = 'development' - require('ts-node').register({ - project: getTsConfigPath(options.dir, options.type), - }) settings.debug = true } diff --git a/src/module-loader.ts b/src/module-loader.ts index fcf7843fa..dcacd81f4 100644 --- a/src/module-loader.ts +++ b/src/module-loader.ts @@ -5,7 +5,7 @@ import * as fs from 'fs-extra' import {ModuleLoadError} from './errors' import {Config as IConfig} from './interfaces' import {Plugin as IPlugin} from './interfaces' -import * as Config from './config' +import {tsPath} from './config' const getPackageType = require('get-package-type') @@ -15,6 +15,10 @@ const getPackageType = require('get-package-type') // eslint-disable-next-line camelcase const s_EXTENSIONS: string[] = ['.ts', '.js', '.mjs', '.cjs'] +const isPlugin = (config: IConfig|IPlugin): config is IPlugin => { + return (config).type !== undefined +} + /** * Provides a static class with several utility methods to work with Oclif config / plugin to load ESM or CJS Node * modules and source files. @@ -81,7 +85,9 @@ export default class ModuleLoader { return {isESM, module, filePath} } catch (error: any) { if (error.code === 'MODULE_NOT_FOUND' || error.code === 'ERR_MODULE_NOT_FOUND') { - throw new ModuleLoadError(`${isESM ? 'import()' : 'require'} failed to load ${filePath || modulePath}: ${error.message}`) + throw new ModuleLoadError( + `${isESM ? 'import()' : 'require'} failed to load ${filePath || modulePath}: ${error.message}`, + ) } throw error @@ -129,17 +135,13 @@ export default class ModuleLoader { */ static resolvePath(config: IConfig|IPlugin, modulePath: string): {isESM: boolean; filePath: string} { let isESM: boolean - let filePath: string - - const isPlugin = (config: IConfig|IPlugin): config is IPlugin => { - return (config).type !== undefined - } + let filePath: string | undefined try { filePath = require.resolve(modulePath) isESM = ModuleLoader.isPathModule(filePath) } catch { - filePath = isPlugin(config) ? Config.tsPath(config.root, modulePath, config.type) : Config.tsPath(config.root, modulePath) + filePath = (isPlugin(config) ? tsPath(config.root, modulePath, config) : tsPath(config.root, modulePath)) ?? modulePath let fileExists = false let isDirectory = false diff --git a/test/config/config.flexible.test.ts b/test/config/config.flexible.test.ts index 217fc0dc5..20c1957b5 100644 --- a/test/config/config.flexible.test.ts +++ b/test/config/config.flexible.test.ts @@ -99,6 +99,7 @@ describe('Config with flexible taxonomy', () => { topics: [], valid: true, tag: 'tag', + moduleType: 'commonjs', } const pluginB: IPlugin = { @@ -117,6 +118,7 @@ describe('Config with flexible taxonomy', () => { topics: [], valid: true, tag: 'tag', + moduleType: 'commonjs', } const plugins: IPlugin[] = [pluginA, pluginB] diff --git a/test/config/config.test.ts b/test/config/config.test.ts index 7de416f80..9b4b062d7 100644 --- a/test/config/config.test.ts +++ b/test/config/config.test.ts @@ -282,6 +282,7 @@ describe('Config', () => { topics: [], valid: true, tag: 'tag', + moduleType: 'commonjs', } const pluginB: IPlugin = { @@ -300,6 +301,7 @@ describe('Config', () => { topics: [], valid: true, tag: 'tag', + moduleType: 'commonjs', } const plugins: IPlugin[] = [pluginA, pluginB] let test = fancy diff --git a/test/integration/esm-cjs.ts b/test/integration/esm-cjs.ts new file mode 100644 index 000000000..827cd3a7a --- /dev/null +++ b/test/integration/esm-cjs.ts @@ -0,0 +1,421 @@ +/** + * These integration tests do not use mocha because we encountered an issue with + * spawning child processes for testing root ESM plugins with linked ESM plugins. + * This scenario works as expected when running outside of mocha. + * + * Instead of spending more time diagnosing the root cause, we are just going to + * run these integration tests using ts-node and a lightweight homemade test runner. + */ +import * as fs from 'fs/promises' +import * as path from 'path' +import {Executor, setup} from './util' +import {expect} from 'chai' +import {bold, green, red} from 'chalk' + +const FAILED: string[] = [] +const PASSED: string[] = [] + +async function test(name: string, fn: () => Promise) { + try { + await fn() + PASSED.push(name) + console.log(green('✓'), name) + } catch (error) { + FAILED.push(name) + console.log(red('𐄂'), name) + console.log(error) + } +} + +function exit(): never { + console.log() + console.log(bold('#### Summary ####')) + + for (const name of PASSED) { + console.log(green('✓'), name) + } + + for (const name of FAILED) { + console.log(red('𐄂'), name) + } + + console.log(`${green('Passed:')} ${PASSED.length}`) + console.log(`${red('Failed:')} ${FAILED.length}`) + + // eslint-disable-next-line no-process-exit, unicorn/no-process-exit + process.exit(FAILED.length) +} + +type Plugin = { + name: string; + command: string; + package: string; + repo: string; +} + +type Script = 'run' | 'dev' + +type InstallPluginOptions = { + executor: Executor; + plugin: Plugin; + script: Script; +} + +type LinkPluginOptions = { + executor: Executor; + plugin: Plugin; + script: Script; +} + +type RunCommandOptions = { + executor: Executor; + plugin: Plugin; + script: Script; + expectStrings?: string[]; + env?: Record; +} + +type ModifyCommandOptions = { + executor: Executor; + plugin: Plugin; + from: string; + to: string; +} + +type CleanUpOptions = { + executor: Executor; + script: Script; + plugin: Plugin; +} + +(async () => { + const PLUGINS = { + esm1: { + name: 'plugin-test-esm-1', + command: 'esm1', + package: '@oclif/plugin-test-esm-1', + repo: 'https://github.com/oclif/plugin-test-esm-1', + commandText: 'hello I am an ESM plugin', + hookText: 'Greetings! from plugin-test-esm-1 init hook', + }, + esm2: { + name: 'plugin-test-esm-2', + command: 'esm2', + package: '@oclif/plugin-test-esm-2', + repo: 'https://github.com/oclif/plugin-test-esm-2', + commandText: 'hello I am an ESM plugin', + hookText: 'Greetings! from plugin-test-esm-2 init hook', + }, + cjs1: { + name: 'plugin-test-cjs-1', + command: 'cjs1', + package: '@oclif/plugin-test-cjs-1', + repo: 'https://github.com/oclif/plugin-test-cjs-1', + commandText: 'hello I am a CJS plugin', + hookText: 'Greetings! from plugin-test-cjs-1 init hook', + }, + cjs2: { + name: 'plugin-test-cjs-2', + command: 'cjs2', + package: '@oclif/plugin-test-cjs-2', + repo: 'https://github.com/oclif/plugin-test-cjs-2', + commandText: 'hello I am a CJS plugin', + hookText: 'Greetings! from plugin-test-cjs-2 init hook', + }, + } + + async function installPlugin(options: InstallPluginOptions): Promise { + const result = await options.executor.executeCommand(`plugins:install ${options.plugin.package}`, options.script) + expect(result.code).to.equal(0) + + const pluginsResult = await options.executor.executeCommand('plugins', options.script) + expect(pluginsResult.stdout).to.include(options.plugin.name) + } + + async function linkPlugin(options: LinkPluginOptions): Promise { + const pluginExecutor = await setup(__filename, { + repo: options.plugin.repo, + subDir: options.executor.parentDir, + }) + + const result = await options.executor.executeCommand(`plugins:link ${pluginExecutor.pluginDir}`, options.script) + expect(result.code).to.equal(0) + + const pluginsResult = await options.executor.executeCommand('plugins', options.script) + expect(pluginsResult.stdout).to.include(options.plugin.name) + + return pluginExecutor + } + + async function modifyCommand(options: ModifyCommandOptions): Promise { + const filePath = path.join(options.executor.pluginDir, 'src', 'commands', `${options.plugin.command}.ts`) + const content = await fs.readFile(filePath, 'utf8') + const modifiedContent = content.replace(options.from, options.to) + await fs.writeFile(filePath, modifiedContent) + } + + async function runCommand(options: RunCommandOptions): Promise { + const env = {...process.env, ...options.env} + const result = await options.executor.executeCommand(options.plugin.command, options.script, {env}) + expect(result.code).to.equal(0) + + if (options.expectStrings) { + for (const expectString of options.expectStrings) { + expect(result.stdout).to.include(expectString) + } + } + } + + async function cleanUp(options: CleanUpOptions): Promise { + await options.executor.executeCommand(`plugins:uninstall @oclif/${options.plugin.name}`) + expect((await options.executor.executeCommand('plugins')).stdout).to.not.include(options.plugin.name) + } + + const args = process.argv.slice(process.argv.indexOf(__filename) + 1) + const runInParallel = args.includes('--parallel') + const skip = args.find(arg => arg.startsWith('--skip=')) + + const skips = skip ? skip.split('=')[1].split(',') : [] + const runEsmTests = !skips.includes('esm') + const runCjsTests = !skips.includes('cjs') + + console.log('Node version:', process.version) + console.log(runInParallel ? '🐇 Running tests in parallel' : '🐢 Running tests sequentially') + if (skips.length > 0) console.log(`🚨 Skipping ${skips.join(', ')} tests 🚨`) + + let cjsExecutor: Executor + let esmExecutor: Executor + + const cjsBefore = async () => { + cjsExecutor = await setup(__filename, {repo: PLUGINS.cjs1.repo, subDir: 'cjs'}) + } + + const esmBefore = async () => { + esmExecutor = await setup(__filename, {repo: PLUGINS.esm1.repo, subDir: 'esm'}) + } + + const cjsTests = async () => { + await test('Install CJS plugin to CJS root plugin', async () => { + const plugin = PLUGINS.cjs2 + + await installPlugin({executor: cjsExecutor, plugin, script: 'run'}) + await runCommand({ + executor: cjsExecutor, + plugin, + script: 'run', + expectStrings: [plugin.commandText, plugin.hookText], + }) + await runCommand({ + executor: cjsExecutor, + plugin, + script: 'dev', + expectStrings: [plugin.commandText, plugin.hookText], + }) + await cleanUp({executor: cjsExecutor, plugin, script: 'run'}) + }) + + await test('Install ESM plugin to CJS root plugin', async () => { + const plugin = PLUGINS.esm1 + + await installPlugin({executor: cjsExecutor, plugin, script: 'run'}) + await runCommand({ + executor: cjsExecutor, + plugin, + script: 'run', + expectStrings: [plugin.commandText, plugin.hookText], + }) + await runCommand({ + executor: cjsExecutor, + plugin, + script: 'dev', + expectStrings: [plugin.commandText, plugin.hookText], + }) + await cleanUp({executor: cjsExecutor, plugin, script: 'run'}) + }) + + await test('Link CJS plugin to CJS root plugin', async () => { + const plugin = PLUGINS.cjs2 + + const linkedPlugin = await linkPlugin({executor: cjsExecutor, plugin, script: 'run'}) + + // test bin/run + await runCommand({ + executor: cjsExecutor, + plugin, + script: 'run', + expectStrings: [plugin.commandText, plugin.hookText], + }) + // test un-compiled changes with bin/run + await modifyCommand({executor: linkedPlugin, plugin, from: 'hello', to: 'howdy'}) + await runCommand({ + executor: cjsExecutor, + plugin, + script: 'run', + expectStrings: ['howdy', plugin.hookText], + }) + + // test un-compiled changes with bin/dev + await modifyCommand({executor: linkedPlugin, plugin, from: 'howdy', to: 'cheers'}) + await runCommand({ + executor: cjsExecutor, + plugin, + script: 'dev', + expectStrings: ['cheers', plugin.hookText], + }) + + await cleanUp({executor: cjsExecutor, plugin, script: 'run'}) + }) + + await test('Link ESM plugin to CJS root plugin', async () => { + const plugin = PLUGINS.esm2 + + await linkPlugin({executor: cjsExecutor, plugin, script: 'run'}) + + // test bin/run + await runCommand({ + executor: cjsExecutor, + plugin, + script: 'run', + expectStrings: [plugin.commandText, plugin.hookText], + }) + + // test bin/dev + await runCommand({ + executor: cjsExecutor, + plugin, + script: 'dev', + expectStrings: [plugin.commandText, plugin.hookText], + }) + + await cleanUp({executor: cjsExecutor, plugin, script: 'run'}) + }) + } + + const esmTests = async () => { + await test('Install CJS plugin to ESM root plugin', async () => { + const plugin = PLUGINS.cjs1 + + await installPlugin({executor: esmExecutor, plugin, script: 'run'}) + await runCommand({ + executor: esmExecutor, + plugin, + script: 'run', + expectStrings: [plugin.commandText, plugin.hookText], + }) + await runCommand({ + executor: esmExecutor, + plugin, + script: 'dev', + expectStrings: [plugin.commandText, plugin.hookText], + }) + await cleanUp({executor: esmExecutor, plugin, script: 'run'}) + }) + + await test('Install ESM plugin to ESM root plugin', async () => { + const plugin = PLUGINS.esm2 + + await installPlugin({executor: esmExecutor, plugin, script: 'run'}) + await runCommand({ + executor: esmExecutor, + plugin, + script: 'run', + expectStrings: [plugin.commandText, plugin.hookText], + }) + await runCommand({ + executor: esmExecutor, + plugin, + script: 'dev', + expectStrings: [plugin.commandText, plugin.hookText], + }) + await cleanUp({executor: esmExecutor, plugin, script: 'run'}) + }) + + await test('Link CJS plugin to ESM root plugin', async () => { + const plugin = PLUGINS.cjs1 + + const linkedPlugin = await linkPlugin({executor: esmExecutor, plugin, script: 'run'}) + // test bin/run + await runCommand({ + executor: esmExecutor, + plugin, + script: 'run', + expectStrings: [plugin.commandText, plugin.hookText], + }) + // test un-compiled changes with bin/run + await modifyCommand({executor: linkedPlugin, plugin, from: 'hello', to: 'howdy'}) + await runCommand({ + executor: esmExecutor, + plugin, + script: 'run', + expectStrings: ['howdy', plugin.hookText], + }) + + // test un-compiled changes with bin/dev + await modifyCommand({executor: linkedPlugin, plugin, from: 'howdy', to: 'cheers'}) + await runCommand({ + executor: esmExecutor, + plugin, + script: 'dev', + expectStrings: ['cheers', plugin.hookText], + }) + + await cleanUp({executor: esmExecutor, plugin, script: 'run'}) + }) + + await test('Link ESM plugin to ESM root plugin', async () => { + const plugin = PLUGINS.esm2 + + const linkedPlugin = await linkPlugin({executor: esmExecutor, plugin, script: 'run'}) + // test bin/run + // NOTE: this also tests that the compiled source is used when ts-node/esm loader is not specified + await runCommand({ + executor: esmExecutor, + plugin, + script: 'run', + expectStrings: [plugin.commandText, plugin.hookText], + }) + // test un-compiled changes with bin/run + await modifyCommand({executor: linkedPlugin, plugin, from: 'hello', to: 'howdy'}) + await runCommand({ + executor: esmExecutor, + plugin, + script: 'run', + expectStrings: ['howdy', plugin.hookText], + env: {NODE_OPTIONS: '--loader=ts-node/esm'}, + }) + + // test un-compiled changes with bin/dev + await modifyCommand({executor: linkedPlugin, plugin, from: 'howdy', to: 'cheers'}) + await runCommand({ + executor: esmExecutor, + plugin, + script: 'dev', + expectStrings: ['cheers', plugin.hookText], + env: {NODE_OPTIONS: '--loader=ts-node/esm'}, + }) + + await cleanUp({executor: esmExecutor, plugin, script: 'run'}) + }) + } + + if (runInParallel) { + await Promise.all([ + runCjsTests ? cjsBefore() : Promise.resolve(), + runEsmTests ? esmBefore() : Promise.resolve(), + ]) + + await Promise.all([ + runCjsTests ? cjsTests() : Promise.resolve(), + runEsmTests ? esmTests() : Promise.resolve(), + ]) + } else { + if (runCjsTests) await cjsBefore() + if (runEsmTests) await esmBefore() + + if (runCjsTests) await cjsTests() + if (runEsmTests) await esmTests() + } + + exit() +})() + diff --git a/test/integration/plugins.e2e.ts b/test/integration/plugins.e2e.ts index 8bdb42373..b8206320e 100644 --- a/test/integration/plugins.e2e.ts +++ b/test/integration/plugins.e2e.ts @@ -30,20 +30,20 @@ describe('oclif plugins', () => { }) it('should show description', () => { - expect(help.output).to.include('oclif example Hello World CLI') + expect(help.stdout).to.include('oclif example Hello World CLI') }) it('should show version', () => { - expect(help.output).to.include('VERSION\n oclif-hello-world/0.0.0') + expect(help.stdout).to.include('VERSION\n oclif-hello-world/0.0.0') }) it('should show usage', () => { - expect(help.output).to.include('USAGE\n $ oclif-hello-world [COMMAND]') + expect(help.stdout).to.include('USAGE\n $ oclif-hello-world [COMMAND]') }) it('should show topics', () => { - expect(help.output).to.include('TOPICS\n plugins') + expect(help.stdout).to.include('TOPICS\n plugins') }) it('should show commands', () => { const regex = /COMMANDS\n\s\sautocomplete|\s\scommands|\s\shelp|\s\splugins|\s\sversion|\s\supdate|\s\swhich/ - expect(regex.test(help.output!)).to.be.true + expect(regex.test(help.stdout!)).to.be.true }) }) @@ -54,20 +54,20 @@ describe('oclif plugins', () => { }) it('should show summary', () => { - expect(help.output).to.include('List installed plugins.') + expect(help.stdout).to.include('List installed plugins.') }) it('should show usage', () => { - expect(help.output).to.include('USAGE\n $ oclif-hello-world plugins [--json] [--core]') + expect(help.stdout).to.include('USAGE\n $ oclif-hello-world plugins [--json] [--core]') }) it('should show description', () => { - expect(help.output).to.include('DESCRIPTION\n List installed plugins.') + expect(help.stdout).to.include('DESCRIPTION\n List installed plugins.') }) it('should show examples', () => { - expect(help.output).to.include('EXAMPLES\n $ oclif-hello-world plugins') + expect(help.stdout).to.include('EXAMPLES\n $ oclif-hello-world plugins') }) it('should show commands', () => { const regex = /COMMANDS\n\s\splugins:inspect|\s\splugins:install|\s\splugins:link|\s\splugins:uninstall|\s\splugins:update/ - expect(regex.test(help.output!)).to.be.true + expect(regex.test(help.stdout!)).to.be.true }) }) @@ -78,31 +78,31 @@ describe('oclif plugins', () => { }) it('should show summary', () => { - expect(help.output).to.include('Installs a plugin into the CLI.') + expect(help.stdout).to.include('Installs a plugin into the CLI.') }) it('should show usage', () => { - expect(help.output).to.include('USAGE\n $ oclif-hello-world plugins:install PLUGIN...') + expect(help.stdout).to.include('USAGE\n $ oclif-hello-world plugins:install PLUGIN...') }) it('should show arguments', () => { - expect(help.output).to.include('ARGUMENTS\n PLUGIN Plugin to install.') + expect(help.stdout).to.include('ARGUMENTS\n PLUGIN Plugin to install.') }) it('should show flags', () => { - expect(help.output).to.include('FLAGS\n') - expect(help.output).to.include('-f, --force Run yarn install with force flag.') - expect(help.output).to.include('-h, --help Show CLI help.') - expect(help.output).to.include('-v, --verbose') + expect(help.stdout).to.include('FLAGS\n') + expect(help.stdout).to.include('-f, --force Run yarn install with force flag.') + expect(help.stdout).to.include('-h, --help Show CLI help.') + expect(help.stdout).to.include('-v, --verbose') }) it('should show description', () => { - expect(help.output).to.include('DESCRIPTION\n Installs a plugin into the CLI.') + expect(help.stdout).to.include('DESCRIPTION\n Installs a plugin into the CLI.') }) it('should show aliases', () => { - expect(help.output).to.include('ALIASES\n $ oclif-hello-world plugins:add') + expect(help.stdout).to.include('ALIASES\n $ oclif-hello-world plugins:add') }) it('should show examples', () => { - expect(help.output).to.include('EXAMPLES\n') - expect(help.output).to.include('$ oclif-hello-world plugins:install myplugin') - expect(help.output).to.include('$ oclif-hello-world plugins:install https://github.com/someuser/someplugin') - expect(help.output).to.include('$ oclif-hello-world plugins:install someuser/someplugin') + expect(help.stdout).to.include('EXAMPLES\n') + expect(help.stdout).to.include('$ oclif-hello-world plugins:install myplugin') + expect(help.stdout).to.include('$ oclif-hello-world plugins:install https://github.com/someuser/someplugin') + expect(help.stdout).to.include('$ oclif-hello-world plugins:install someuser/someplugin') }) }) }) @@ -112,68 +112,68 @@ describe('oclif plugins', () => { it('should show commands', async () => { commands = await executor.executeCommand('commands') - expect(commands.output).to.include('commands') - expect(commands.output).to.include('help') - expect(commands.output).to.include('plugins') - expect(commands.output).to.include('plugins:inspect') - expect(commands.output).to.include('plugins:install') - expect(commands.output).to.include('plugins:link') - expect(commands.output).to.include('plugins:uninstall') - expect(commands.output).to.include('plugins:update') - expect(commands.output).to.include('version') - expect(commands.output).to.include('which') + expect(commands.stdout).to.include('commands') + expect(commands.stdout).to.include('help') + expect(commands.stdout).to.include('plugins') + expect(commands.stdout).to.include('plugins:inspect') + expect(commands.stdout).to.include('plugins:install') + expect(commands.stdout).to.include('plugins:link') + expect(commands.stdout).to.include('plugins:uninstall') + expect(commands.stdout).to.include('plugins:update') + expect(commands.stdout).to.include('version') + expect(commands.stdout).to.include('which') }) - it('should fliter commands', async () => { + it('should filter commands', async () => { commands = await executor.executeCommand('commands --filter "command=plugins"') - expect(commands.output).to.include('plugins') - expect(commands.output).to.include('plugins:inspect') - expect(commands.output).to.include('plugins:install') - expect(commands.output).to.include('plugins:link') - expect(commands.output).to.include('plugins:uninstall') - expect(commands.output).to.include('plugins:update') + expect(commands.stdout).to.include('plugins') + expect(commands.stdout).to.include('plugins:inspect') + expect(commands.stdout).to.include('plugins:install') + expect(commands.stdout).to.include('plugins:link') + expect(commands.stdout).to.include('plugins:uninstall') + expect(commands.stdout).to.include('plugins:update') - expect(commands.output).to.not.include('commands') - expect(commands.output).to.not.include('help') - expect(commands.output).to.not.include('version') - expect(commands.output).to.not.include('which') + expect(commands.stdout).to.not.include('commands') + expect(commands.stdout).to.not.include('help') + expect(commands.stdout).to.not.include('version') + expect(commands.stdout).to.not.include('which') }) - it('should extedned columns', async () => { + it('should extend columns', async () => { commands = await executor.executeCommand('commands --extended') - expect(commands.output).to.include('Command') - expect(commands.output).to.include('Summary') - expect(commands.output).to.include('Description') - expect(commands.output).to.include('Usage') - expect(commands.output).to.include('Plugin') - expect(commands.output).to.include('Type') - expect(commands.output).to.include('Hidden') + expect(commands.stdout).to.include('Command') + expect(commands.stdout).to.include('Summary') + expect(commands.stdout).to.include('Description') + expect(commands.stdout).to.include('Usage') + expect(commands.stdout).to.include('Plugin') + expect(commands.stdout).to.include('Type') + expect(commands.stdout).to.include('Hidden') }) it('should filter columns', async () => { commands = await executor.executeCommand('commands --columns Command') - expect(commands.output).to.include('Command') - expect(commands.output).to.not.include('Summary') + expect(commands.stdout).to.include('Command') + expect(commands.stdout).to.not.include('Summary') }) it('should show commands in csv', async () => { commands = await executor.executeCommand('commands --csv') - expect(commands.output).to.include('Command,Summary\n') - expect(commands.output).to.include('commands') - expect(commands.output).to.include('help') - expect(commands.output).to.include('plugins') - expect(commands.output).to.include('plugins:inspect') - expect(commands.output).to.include('plugins:install') - expect(commands.output).to.include('plugins:link') - expect(commands.output).to.include('plugins:uninstall') - expect(commands.output).to.include('plugins:update') - expect(commands.output).to.include('version') - expect(commands.output).to.include('which') + expect(commands.stdout).to.include('Command,Summary\n') + expect(commands.stdout).to.include('commands') + expect(commands.stdout).to.include('help') + expect(commands.stdout).to.include('plugins') + expect(commands.stdout).to.include('plugins:inspect') + expect(commands.stdout).to.include('plugins:install') + expect(commands.stdout).to.include('plugins:link') + expect(commands.stdout).to.include('plugins:uninstall') + expect(commands.stdout).to.include('plugins:update') + expect(commands.stdout).to.include('version') + expect(commands.stdout).to.include('which') }) it('should show commands in json', async () => { commands = await executor.executeCommand('commands --json') - const json = JSON.parse(commands.output!) as Array<{ id: string }> + const json = JSON.parse(commands.stdout!) as Array<{ id: string }> const commandIds = json.map(j => j.id) expect(commandIds).to.include('commands') expect(commandIds).to.include('help') @@ -197,11 +197,11 @@ describe('oclif plugins', () => { it('should install the plugin', async () => { const result = await executor.executeCommand('plugins:install @oclif/plugin-warn-if-update-available 2>&1') expect(result.code).to.equal(0) - expect(result.output).to.include('@oclif/plugin-warn-if-update-available... installed v') + expect(result.stdout).to.include('@oclif/plugin-warn-if-update-available... installed v') const pluginsResult = await executor.executeCommand('plugins') expect(pluginsResult.code).to.equal(0) - expect(pluginsResult.output).to.include('@oclif/plugin-warn-if-update-available') + expect(pluginsResult.stdout).to.include('@oclif/plugin-warn-if-update-available') }) }) @@ -214,9 +214,9 @@ describe('oclif plugins', () => { const result = await executor.executeCommand('plugins:install https://github.com/oclif/plugin-warn-if-update-available') expect(result.code).to.equal(0) - const pluginsResult = await executor.executeCommand('plugins') + const pluginsResult = await executor.executeCommand('plugins --core') expect(pluginsResult.code).to.equal(0) - expect(pluginsResult.output).to.include('@oclif/plugin-warn-if-update-available') + expect(pluginsResult.stdout).to.include('@oclif/plugin-warn-if-update-available') }) }) @@ -224,11 +224,11 @@ describe('oclif plugins', () => { it('should install the plugin', async () => { const result = await executor.executeCommand('plugins:install @oclif/plugin-warn-if-update-available --force 2>&1') expect(result.code).to.equal(0) - expect(result.output).to.include('@oclif/plugin-warn-if-update-available... installed v') + expect(result.stdout).to.include('@oclif/plugin-warn-if-update-available... installed v') const pluginsResult = await executor.executeCommand('plugins') expect(pluginsResult.code).to.equal(0) - expect(pluginsResult.output).to.include('@oclif/plugin-warn-if-update-available') + expect(pluginsResult.stdout).to.include('@oclif/plugin-warn-if-update-available') }) }) @@ -240,11 +240,11 @@ describe('oclif plugins', () => { it('should uninstall the plugin', async () => { const result = await executor.executeCommand('plugins:uninstall @oclif/plugin-warn-if-update-available 2>&1') expect(result.code).to.equal(0) - expect(result.output).to.include('Uninstalling @oclif/plugin-warn-if-update-available... done\n') + expect(result.stdout).to.include('Uninstalling @oclif/plugin-warn-if-update-available... done\n') const pluginsResult = await executor.executeCommand('plugins') expect(pluginsResult.code).to.equal(0) - expect(pluginsResult.output).to.not.include('@oclif/plugin-warn-if-update-available') + expect(pluginsResult.stdout).to.not.include('@oclif/plugin-warn-if-update-available') }) }) }) @@ -255,16 +255,16 @@ describe('oclif plugins', () => { version = await executor.executeCommand('version') }) - it('should show version', () => expect(version.output).to.include('oclif-hello-world/0.0.0')) - it('should show platform', () => expect(version.output).to.include(process.platform)) - it('should show arch', () => expect(version.output).to.include(os.arch())) - it('should show node version', () => expect(version.output).to.include(process.version)) + it('should show version', () => expect(version.stdout).to.include('oclif-hello-world/0.0.0')) + it('should show platform', () => expect(version.stdout).to.include(process.platform)) + it('should show arch', () => expect(version.stdout).to.include(os.arch())) + it('should show node version', () => expect(version.stdout).to.include(process.version)) }) describe('plugin-which', () => { it('should show the plugin that a command belongs to', async () => { const result = await executor.executeCommand('which plugins:install') - expect(result.output).to.include('@oclif/plugin-plugins') + expect(result.stdout).to.include('@oclif/plugin-plugins') }) }) }) diff --git a/test/integration/sf.e2e.ts b/test/integration/sf.e2e.ts index 48a4615d8..3b2bc4949 100644 --- a/test/integration/sf.e2e.ts +++ b/test/integration/sf.e2e.ts @@ -51,7 +51,7 @@ describe('Salesforce CLI (sf)', () => { * */ const regex = /^.*?USAGE.*?FLAGS.*?GLOBAL FLAGS.*?DESCRIPTION.*?EXAMPLES.*?FLAG DESCRIPTIONS.*?CONFIGURATION VARIABLES.*?ENVIRONMENT VARIABLES.*$/gs - expect(regex.test(help.output!)).to.be.true + expect(regex.test(help.stdout!)).to.be.true }) it('should show custom short help', async () => { @@ -72,20 +72,20 @@ describe('Salesforce CLI (sf)', () => { * */ const regex = /^.*?USAGE.*?FLAGS.*?GLOBAL FLAGS.*?(?!DESCRIPTION).*?(?!EXAMPLES).*?(?!FLAG DESCRIPTIONS).*?(?!CONFIGURATION VARIABLES).*?(?!ENVIRONMENT VARIABLES).*$/gs - expect(regex.test(help.output!)).to.be.true + expect(regex.test(help.stdout!)).to.be.true }) it('should show version using -v', async () => { const version = await executor.executeCommand('-v') - expect(version.output).to.include('@salesforce/cli') - expect(version.output).to.include(process.platform) - expect(version.output).to.include(os.arch()) - expect(version.output).to.include(process.version) + expect(version.stdout).to.include('@salesforce/cli') + expect(version.stdout).to.include(process.platform) + expect(version.stdout).to.include(os.arch()) + expect(version.stdout).to.include(process.version) }) it('should have formatted json success output', async () => { const config = await executor.executeCommand('config list --json') - const result = parseJson(config.output!) + const result = parseJson(config.stdout!) expect(result).to.have.property('status') expect(result).to.have.property('result') expect(result).to.have.property('warnings') @@ -93,7 +93,7 @@ describe('Salesforce CLI (sf)', () => { it('should have formatted json error output', async () => { const config = await executor.executeCommand('config set DOES_NOT_EXIST --json') - const result = parseJson(config.output!) + const result = parseJson(config.stdout!) expect(result).to.have.property('status') expect(result).to.have.property('stack') expect(result).to.have.property('name') @@ -103,7 +103,7 @@ describe('Salesforce CLI (sf)', () => { it('should handle varargs', async () => { const config = await executor.executeCommand('config set disable-telemetry=true org-api-version=54.0 --global --json') - const parsed = parseJson(config.output!) + const parsed = parseJson(config.stdout!) expect(parsed.status).to.equal(0) const results = parsed.result as {successes: Array<{success: boolean}>, failures: Array<{failed: boolean}>} for (const result of results.successes) { diff --git a/test/integration/util.ts b/test/integration/util.ts index 024b43f94..0e724e1d1 100644 --- a/test/integration/util.ts +++ b/test/integration/util.ts @@ -6,65 +6,117 @@ import * as chalk from 'chalk' import * as fs from 'fs' import * as os from 'os' import * as path from 'path' +import {Interfaces} from '../../src' + +const debug = require('debug')('e2e') export type ExecError = cp.ExecException & { stderr: string; stdout: string }; -export interface Result { +export type Result = { code: number; - output?: string; + stdout?: string; + stderr?: string; error?: ExecError } -export interface Options { +export type SetupOptions = { repo: string; plugins?: string[]; + subDir?: string; +} + +export type ExecutorOptions = { + pluginDir: string; + testFileName: string; } -function updatePkgJson(testDir: string, obj: Record): void { +export type ExecOptions = cp.ExecSyncOptionsWithBufferEncoding & {silent?: boolean} + +function updatePkgJson(testDir: string, obj: Record): Interfaces.PJSON { const pkgJsonFile = path.join(testDir, 'package.json') const pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8')) obj.dependencies = Object.assign(pkgJson.dependencies || {}, obj.dependencies || {}) obj.resolutions = Object.assign(pkgJson.resolutions || {}, obj.resolutions || {}) const updated = Object.assign(pkgJson, obj) fs.writeFileSync(pkgJsonFile, JSON.stringify(updated, null, 2)) + + return updated } export class Executor { - public constructor(private testDir: string) {} + public usesJsScript = false + public pluginDir: string + public testFileName: string + public parentDir: string + public pluginName: string + + public constructor(options: ExecutorOptions) { + this.pluginDir = options.pluginDir + this.testFileName = options.testFileName + this.parentDir = path.basename(path.dirname(this.pluginDir)) + this.pluginName = path.basename(this.pluginDir) + + this.debug = debug.extend(`${this.testFileName}:${this.parentDir}:${this.pluginName}`) + } + + public clone(repo: string): Promise { + const result = this.exec(`git clone ${repo} ${this.pluginDir} --depth 1`) + this.usesJsScript = fs.existsSync(path.join(this.pluginDir, 'bin', 'run.js')) + return result + } - public executeInTestDir(cmd: string, silent = true): Promise { - return this.exec(cmd, this.testDir, silent) + public executeInTestDir(cmd: string, options?: ExecOptions): Promise { + return this.exec(cmd, {...options, cwd: this.pluginDir} as ExecOptions) } - public executeCommand(cmd: string): Promise { - const executable = process.platform === 'win32' ? path.join('bin', 'run.cmd') : path.join('bin', 'run') - return this.executeInTestDir(`${executable} ${cmd}`) + public executeCommand(cmd: string, script: 'run' | 'dev' = 'run', options: ExecOptions = {}): Promise { + const executable = process.platform === 'win32' ? + path.join('bin', `${script}.cmd`) : + path.join('bin', `${script}${this.usesJsScript ? '.js' : ''}`) + return this.executeInTestDir(`${executable} ${cmd}`, options) } - public exec(cmd: string, cwd = process.cwd(), silent = true): Promise { + public exec(cmd: string, options?: ExecOptions): Promise { + const cwd = options?.cwd ?? process.cwd() + const silent = options?.silent ?? true return new Promise(resolve => { + this.debug(cmd, chalk.dim(`(cwd: ${cwd})`)) if (silent) { try { - const r = cp.execSync(cmd, {stdio: 'pipe', cwd}) - resolve({code: 0, output: r.toString()}) + const r = cp.execSync(cmd, { + stdio: 'pipe', + ...options, + cwd, + }) + const stdout = r.toString() + this.debug(stdout) + resolve({code: 0, stdout}) } catch (error) { const err = error as ExecError - resolve({code: 1, error: err, output: err.stdout.toString()}) + this.debug('stdout', err.stdout.toString()) + this.debug('stderr', err.stderr.toString()) + resolve({ + code: 1, + error: err, + stdout: err.stdout.toString(), + stderr: err.stderr.toString(), + }) } } else { - console.log(chalk.cyan(cmd)) cp.execSync(cmd, {stdio: 'inherit', cwd}) resolve({code: 0}) } }) } + + public debug: (...args: any[]) => void } // eslint-disable-next-line valid-jsdoc /** * Setup for integration tests. * - * Clones the hello-world repo from github + * Clones the requested repo from github * Adds the local version of @oclif/core to the package.json * Adds relevant oclif plugins * Builds the package @@ -73,56 +125,74 @@ export class Executor { * - OCLIF_CORE_E2E_TEST_DIR: the directory that you want the setup to happen in * - OCLIF_CORE_E2E_SKIP_SETUP: skip all the setup steps (useful if iterating on tests) */ -export async function setup(testFile: string, options: Options): Promise { +export async function setup(testFile: string, options: SetupOptions): Promise { const testFileName = path.basename(testFile) - const location = path.join(process.env.OCLIF_CORE_E2E_TEST_DIR || os.tmpdir(), testFileName) - const [name] = options.repo.match(/(?<=\/).+?(?=\.)/) ?? ['hello-world'] - const testDir = path.join(location, name) - const executor = new Executor(testDir) + const dir = process.env.OCLIF_CORE_E2E_TEST_DIR || os.tmpdir() + const testDir = options.subDir ? path.join(dir, testFileName, options.subDir) : path.join(dir, testFileName) + + const name = options.repo.slice(options.repo.lastIndexOf('/') + 1) + const pluginDir = path.join(testDir, name) + const executor = new Executor({pluginDir, testFileName}) - console.log(chalk.cyan(`${testFileName}:`), testDir) + executor.debug('plugin directory:', pluginDir) if (process.env.OCLIF_CORE_E2E_SKIP_SETUP === 'true') { console.log(chalk.yellow.bold('OCLIF_CORE_E2E_SKIP_SETUP is true. Skipping test setup...')) return executor } - await mkdirp(location) - rm('-rf', testDir) + await mkdirp(testDir) + rm('-rf', pluginDir) - const clone = `git clone ${options.repo} ${testDir}` - console.log(chalk.cyan(`${testFileName}:`), clone) - await executor.exec(clone) + await executor.clone(options.repo) - console.log(chalk.cyan(`${testFileName}:`), 'Updating package.json') + executor.debug('Updating package.json') const dependencies = {'@oclif/core': `file:${path.resolve('.')}`} + let pjson: Interfaces.PJSON if (options.plugins) { // eslint-disable-next-line unicorn/prefer-object-from-entries const pluginDeps = options.plugins.reduce((x, y) => ({...x, [y]: 'latest'}), {}) - updatePkgJson(testDir, { + pjson = updatePkgJson(pluginDir, { resolutions: {'@oclif/core': path.resolve('.')}, dependencies: Object.assign(dependencies, pluginDeps), oclif: {plugins: options.plugins}, }) } else { - updatePkgJson(testDir, { + pjson = updatePkgJson(pluginDir, { resolutions: {'@oclif/core': path.resolve('.')}, dependencies, }) } - const install = 'yarn install --force' - console.log(chalk.cyan(`${testFileName}:`), install) - const yarnInstallRes = await executor.executeInTestDir(install, false) + executor.debug('updated dependencies:', JSON.stringify(pjson.dependencies, null, 2)) + executor.debug('updated resolutions:', JSON.stringify(pjson.resolutions, null, 2)) + executor.debug('updated plugins:', JSON.stringify(pjson.oclif.plugins, null, 2)) + + const bin = (pjson.oclif.bin ?? pjson.name.replace(/-/g, '_')).toUpperCase() + const dataDir = path.join(testDir, 'data', pjson.oclif.bin ?? pjson.name) + const cacheDir = path.join(testDir, 'cache', pjson.oclif.bin ?? pjson.name) + const configDir = path.join(testDir, 'config', pjson.oclif.bin ?? pjson.name) + + await mkdirp(dataDir) + await mkdirp(configDir) + await mkdirp(cacheDir) + + process.env[`${bin}_DATA_DIR`] = dataDir + process.env[`${bin}_CONFIG_DIR`] = configDir + process.env[`${bin}_CACHE_DIR`] = cacheDir + + executor.debug(`${bin}_DATA_DIR:`, process.env[`${bin}_DATA_DIR`]) + executor.debug(`${bin}_CONFIG_DIR:`, process.env[`${bin}_CONFIG_DIR`]) + executor.debug(`${bin}_CACHE_DIR:`, process.env[`${bin}_CACHE_DIR`]) + + const yarnInstallRes = await executor.executeInTestDir('yarn install --force') if (yarnInstallRes.code !== 0) { console.error(yarnInstallRes?.error) throw new Error('Failed to run `yarn install`') } - const build = 'yarn build' - console.log(chalk.cyan(`${testFileName}:`), build) - const yarnBuildRes = await executor.executeInTestDir(build, false) + const yarnBuildRes = await executor.executeInTestDir('yarn build') if (yarnBuildRes.code !== 0) { console.error(yarnBuildRes?.error) throw new Error('Failed to run `yarn build`') diff --git a/yarn.lock b/yarn.lock index 42d0def4d..0117b62ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -823,11 +823,6 @@ "@typescript-eslint/types" "4.31.2" eslint-visitor-keys "^2.0.0" -"@ungap/promise-all-settled@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" - integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== - JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -893,11 +888,6 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.2: dependencies: type-fest "^0.21.3" -ansi-regex@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" - integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -927,10 +917,10 @@ ansicolors@~0.3.2: resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= -anymatch@~3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -1013,6 +1003,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -1122,20 +1119,20 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -chokidar@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: - anymatch "~3.1.1" + anymatch "~3.1.2" braces "~3.0.2" - glob-parent "~5.1.0" + glob-parent "~5.1.2" is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.5.0" + readdirp "~3.6.0" optionalDependencies: - fsevents "~2.3.1" + fsevents "~2.3.2" ci-info@^3.2.0: version "3.2.0" @@ -1287,6 +1284,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -1298,7 +1302,7 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.2: +cross-spawn@^7.0.1, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -1312,14 +1316,7 @@ dargs@^7.0.0: resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== -debug@4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4: +debug@4.3.4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -1809,7 +1806,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@~2.3.1: +fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -1855,17 +1852,17 @@ git-raw-commits@^2.0.0: split2 "^3.0.0" through2 "^4.0.0" -glob-parent@^5.1.2, glob-parent@~5.1.0: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob@7.1.6: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -1922,11 +1919,6 @@ graceful-fs@^4.1.15, graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - hard-rejection@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" @@ -2085,11 +2077,6 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -2181,10 +2168,10 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" - integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" @@ -2323,14 +2310,7 @@ lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" - integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== - dependencies: - chalk "^4.0.0" - -log-symbols@^4.0.0: +log-symbols@4.1.0, log-symbols@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -2425,7 +2405,14 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@3.0.4, minimatch@^3.0.4: +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -2446,33 +2433,29 @@ minimist@^1.2.3: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -mocha@^8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.4.0.tgz#677be88bf15980a3cae03a73e10a0fc3997f0cff" - integrity sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ== +mocha@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" + integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== dependencies: - "@ungap/promise-all-settled" "1.1.2" ansi-colors "4.1.1" browser-stdout "1.3.1" - chokidar "3.5.1" - debug "4.3.1" + chokidar "3.5.3" + debug "4.3.4" diff "5.0.0" escape-string-regexp "4.0.0" find-up "5.0.0" - glob "7.1.6" - growl "1.10.5" + glob "7.2.0" he "1.2.0" - js-yaml "4.0.0" - log-symbols "4.0.0" - minimatch "3.0.4" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" ms "2.1.3" - nanoid "3.1.20" - serialize-javascript "5.0.1" + nanoid "3.3.3" + serialize-javascript "6.0.0" strip-json-comments "3.1.1" supports-color "8.1.1" - which "2.0.2" - wide-align "1.1.3" - workerpool "6.1.0" + workerpool "6.2.1" yargs "16.2.0" yargs-parser "20.2.4" yargs-unparser "2.0.0" @@ -2507,10 +2490,10 @@ nanocolors@^0.1.0, nanocolors@^0.1.5: resolved "https://registry.yarnpkg.com/nanocolors/-/nanocolors-0.1.12.tgz#8577482c58cbd7b5bb1681db4cf48f11a87fd5f6" integrity sha512-2nMHqg1x5PU+unxX7PGY7AuYxl2qDx7PSrTRjizr8sxdd3l/3hBuWWaki62qmtYm2U5i4Z5E7GbjlyDFhs9/EQ== -nanoid@3.1.20: - version "3.1.20" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" - integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== natural-compare@^1.4.0: version "1.4.0" @@ -2828,10 +2811,10 @@ readable-stream@3, readable-stream@^3.0.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" @@ -2962,10 +2945,10 @@ semver@^7.2.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.0, semve dependencies: lru-cache "^6.0.0" -serialize-javascript@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" - integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== dependencies: randombytes "^2.1.0" @@ -3087,14 +3070,6 @@ stdout-stderr@^0.1.9: debug "^4.1.1" strip-ansi "^6.0.0" -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -3118,13 +3093,6 @@ strip-ansi@*: dependencies: ansi-regex "^6.0.0" -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -3371,13 +3339,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -which@2.0.2, which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -3385,12 +3346,12 @@ which@^1.2.9: dependencies: isexe "^2.0.0" -wide-align@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: - string-width "^1.0.2 || 2" + isexe "^2.0.0" widest-line@^3.1.0: version "3.1.0" @@ -3409,10 +3370,10 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -workerpool@6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b" - integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg== +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== wrap-ansi@^7.0.0: version "7.0.0"