diff --git a/.eslintignore b/.eslintignore index 088756bc013e..3de1d944b361 100644 --- a/.eslintignore +++ b/.eslintignore @@ -42,3 +42,7 @@ npm/webpack-preprocessor/examples/use-babelrc/cypress/integration/spec.js **/.git /npm/react/bin/* +/npm/react/**/coverage +**/.next/** +/npm/create-cypress-tests/initial-template +/npm/create-cypress-tests/**/*.template.* diff --git a/.gitignore b/.gitignore index c6889fcd6245..45bd54d4eed0 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ npm/**/cypress/screenshots # from example packages/example/app packages/example/build -packages/example/cypress +packages/example/cypress/integration # from server packages/server/.cy @@ -39,6 +39,10 @@ packages/server/test/support/fixtures/server/libs /npm/react/bin/* /npm/react/cypress/videos +# from npm/create-cypress-tests +/npm/create-cypress-tests/initial-template +/npm/create-cypress-tests/src/test-output + # Building app binary scripts/support package-lock.json diff --git a/circle.yml b/circle.yml index 01d4c576e28d..dc10b6867535 100644 --- a/circle.yml +++ b/circle.yml @@ -992,7 +992,18 @@ jobs: name: Run e2e example tests command: yarn test working_directory: npm/react/<> - + + + npm-create-cypress-tests: + <<: *defaults + steps: + - attach_workspace: + at: ~/ + - run: yarn workspace create-cypress-tests build + - run: + name: Run unit test + command: yarn workspace create-cypress-tests test + npm-eslint-plugin-dev: <<: *defaults steps: @@ -1822,6 +1833,10 @@ linux-workflow: &linux-workflow path: examples/webpack-options requires: - npm-react + + - npm-create-cypress-tests: + requires: + - build - npm-eslint-plugin-dev: requires: diff --git a/npm/create-cypress-tests/.eslintrc b/npm/create-cypress-tests/.eslintrc new file mode 100644 index 000000000000..08df16eaf0d4 --- /dev/null +++ b/npm/create-cypress-tests/.eslintrc @@ -0,0 +1,38 @@ +{ + "plugins": [ + "cypress", + "@cypress/dev" + ], + "extends": [ + "plugin:@cypress/dev/general", + "plugin:@cypress/dev/tests" + ], + "parser": "@typescript-eslint/parser", + "env": { + "cypress/globals": true + }, + "rules": { + "no-console": "off", + "mocha/no-global-tests": "off", + "@typescript-eslint/no-unused-vars": "off" + }, + "overrides": [ + { + "files": [ + "lib/*" + ], + "rules": { + "no-console": 1 + } + }, + { + "files": [ + "**/*.json" + ], + "rules": { + "quotes": "off", + "comma-dangle": "off" + } + } + ] +} diff --git a/npm/create-cypress-tests/.mocharc.json b/npm/create-cypress-tests/.mocharc.json new file mode 100644 index 000000000000..05d782fced5a --- /dev/null +++ b/npm/create-cypress-tests/.mocharc.json @@ -0,0 +1,7 @@ +{ + "watch-ignore": [ + "node_modules" + ], + "require": "ts-node/register", + "exit": true +} diff --git a/npm/create-cypress-tests/.npmignore b/npm/create-cypress-tests/.npmignore new file mode 100644 index 000000000000..c9cfa44194a1 --- /dev/null +++ b/npm/create-cypress-tests/.npmignore @@ -0,0 +1,4 @@ +./src/ +./initial-template/ +scripts/ +__snapshots__/ \ No newline at end of file diff --git a/npm/create-cypress-tests/README.md b/npm/create-cypress-tests/README.md new file mode 100644 index 000000000000..a845dd0b82a1 --- /dev/null +++ b/npm/create-cypress-tests/README.md @@ -0,0 +1,52 @@ +# Create Cypress Tests + +Installs and injects all the required configuration to run cypress tests. + +## Quick overview + +``` +cd my-app +npx create-cypress-test +npx cypress open +``` + +![demo](./demo.gif) + +## Package manager + +This wizard will automatically determine which package do you use. If `yarn` available as global dependency it will use yarn to install dependencies and create lock file. + +If you need to use `npm` over `yarn` you can do the following + +``` +npx create-cypress-tests --use-npm +``` + +By the way you can use yarn to run the installation wizard ๐Ÿ˜‰ + +``` +yarn create cypress tests +``` + +## Typescript + +This package will also automatically determine if typescript if available in this project and inject the required typescript configuration for cypress. If you are starting a new project and want to create typescript configuration, please do the following: + +``` +npm init +npm install typescript +npx create-cypress-tests +``` + +## Configuration + +Here is a list of available configuration options: + +`--use-npm` โ€“ย use npm if yarn available +`--ignore-typescript` โ€“ will not create typescript configuration if available +`--ignore-examples` โ€“ย will create a 1 empty spec file (`cypress/integration/spec.js`) to start with +`--component-tests`ย โ€“ will not ask should setup component testing or not + +## License + +The project is licensed under the terms of [MIT license](../../LICENSE) \ No newline at end of file diff --git a/npm/create-cypress-tests/__snapshots__/babel.test.ts.js b/npm/create-cypress-tests/__snapshots__/babel.test.ts.js new file mode 100644 index 000000000000..5e5dc1658279 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/babel.test.ts.js @@ -0,0 +1,10 @@ +exports['babel installation template correctly generates plugins config 1'] = ` +const preprocessor = require('@cypress/react/plugins/babel'); + +const something = require("something"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js b/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js new file mode 100644 index 000000000000..bc623f51e593 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js @@ -0,0 +1,50 @@ +exports['Injects guessed next.js template cypress.json'] = ` +const preprocessor = require("@cypress/react/plugins/next"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; + +` + +exports['Injects guessed next.js template plugins/index.js'] = ` +const preprocessor = require("@cypress/react/plugins/next"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; + +` + +exports['Injects guessed next.js template support/index.js'] = ` +import "@cypress/react/support"; + +` + +exports['Injected overridden webpack template cypress.json'] = ` +const preprocessor = require("@cypress/react/plugins/react-scripts"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; + +` + +exports['Injected overridden webpack template plugins/index.js'] = ` +const preprocessor = require("@cypress/react/plugins/react-scripts"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; + +` + +exports['Injected overridden webpack template support/index.js'] = ` +import "./commands.js"; +import "@cypress/react/support"; + +` diff --git a/npm/create-cypress-tests/__snapshots__/next.test.ts.js b/npm/create-cypress-tests/__snapshots__/next.test.ts.js new file mode 100644 index 000000000000..5fffc9059960 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/next.test.ts.js @@ -0,0 +1,10 @@ +exports['next.js install template correctly generates plugins config 1'] = ` +const preprocessor = require('@cypress/react/plugins/next'); + +const something = require("something"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js b/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js new file mode 100644 index 000000000000..53f5b90ea4ae --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js @@ -0,0 +1,10 @@ +exports['create-react-app install template correctly generates plugins config 1'] = ` +const preprocessor = require('@cypress/react/plugins/react-scripts'); + +const something = require("something"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js b/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js new file mode 100644 index 000000000000..314d0ac6f470 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js @@ -0,0 +1,24 @@ +exports['webpack-file install template correctly generates plugins config when webpack config path is missing 1'] = ` +const preprocessor = require("@cypress/react/plugins/load-webpack"); + +const something = require("something"); + +module.exports = (on, config) => { + // TODO replace with valid webpack config path + config.env.webpackFilename = './webpack.config.js'; + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; +` + +exports['webpack-file install template correctly generates plugins config when webpack config path is provided 1'] = ` +const preprocessor = require("@cypress/react/plugins/load-webpack"); + +const something = require("something"); + +module.exports = (on, config) => { + config.env.webpackFilename = 'config/webpack.config.js'; + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/rollup.test.ts.js b/npm/create-cypress-tests/__snapshots__/rollup.test.ts.js new file mode 100644 index 000000000000..3e28e760ff9b --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/rollup.test.ts.js @@ -0,0 +1,32 @@ +exports['rollup-file install template correctly generates plugins config when webpack config path is missing 1'] = ` +const rollupPreprocessor = require("@bahmutov/cy-rollup"); + +const something = require("something"); + +module.exports = (on, config) => { + on('file:preprocessor', rollupPreprocessor({ + // TODO replace with valid rollup config path + configFile: 'rollup.config.js' + })); + + require('@cypress/code-coverage/task')(on, config); + + return config; // IMPORTANT to return the config object +}; +` + +exports['rollup-file install template correctly generates plugins config when webpack config path is provided 1'] = ` +const rollupPreprocessor = require("@bahmutov/cy-rollup"); + +const something = require("something"); + +module.exports = (on, config) => { + on('file:preprocessor', rollupPreprocessor({ + configFile: 'config/rollup.config.js' + })); + + require('@cypress/code-coverage/task')(on, config); + + return config; // IMPORTANT to return the config object +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/vueCli.test.ts.js b/npm/create-cypress-tests/__snapshots__/vueCli.test.ts.js new file mode 100644 index 000000000000..ec7c70acef0f --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/vueCli.test.ts.js @@ -0,0 +1,11 @@ +exports['vue webpack-file install template correctly generates plugins for vue-cli-service 1'] = ` +const preprocessor = require("@cypress/vue/dist/plugins/webpack"); + +const something = require("something"); + +module.exports = (on, config) => { + preprocessor(on, config); // IMPORTANT return the config object + + return config; +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js b/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js new file mode 100644 index 000000000000..373b8e8975f6 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js @@ -0,0 +1,24 @@ +exports['vue webpack-file install template correctly generates plugins config when webpack config path is missing 1'] = ` +const { + onFilePreprocessor +} = require('@cypress/vue/dist/preprocessor/webpack'); + +const something = require("something"); + +module.exports = (on, config) => { + // TODO replace with valid webpack config path + on('file:preprocessor', onFilePreprocessor('./webpack.config.js')); +}; +` + +exports['vue webpack-file install template correctly generates plugins config when webpack config path is provided 1'] = ` +const { + onFilePreprocessor +} = require('@cypress/vue/dist/preprocessor/webpack'); + +const something = require("something"); + +module.exports = (on, config) => { + on('file:preprocessor', onFilePreprocessor('build/webpack.config.js')); +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js b/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js new file mode 100644 index 000000000000..352d39aff258 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js @@ -0,0 +1,29 @@ +exports['webpack-options template correctly generates plugins config 1'] = ` +const webpackPreprocessor = require("@cypress/webpack-preprocessor"); + +const something = require("something"); + +module.exports = (on, config) => { + const opts = webpackPreprocessor.defaultOptions; + const babelLoader = opts.webpackOptions.module.rules[0].use[0]; // add React preset to be able to transpile JSX + + babelLoader.options.presets.push(require.resolve('@babel/preset-react')); // We can also push Babel istanbul plugin to instrument the code on the fly + // and get code coverage reports from component tests (optional) + + if (!babelLoader.options.plugins) { + babelLoader.options.plugins = []; + } + + babelLoader.options.plugins.push(require.resolve('babel-plugin-istanbul')); // in order to mock named imports, need to include a plugin + + babelLoader.options.plugins.push([require.resolve('@babel/plugin-transform-modules-commonjs'), { + loose: true + }]); // add code coverage plugin + + require('@cypress/code-coverage/task')(on, config); + + on('file:preprocessor', webpackPreprocessor(opts)); // if adding code coverage, important to return updated config + + return config; +}; +` diff --git a/npm/create-cypress-tests/demo.gif b/npm/create-cypress-tests/demo.gif new file mode 100644 index 000000000000..344fc47d3faa Binary files /dev/null and b/npm/create-cypress-tests/demo.gif differ diff --git a/npm/create-cypress-tests/package.json b/npm/create-cypress-tests/package.json new file mode 100644 index 000000000000..e2cd65c174d6 --- /dev/null +++ b/npm/create-cypress-tests/package.json @@ -0,0 +1,44 @@ +{ + "name": "create-cypress-tests", + "version": "0.0.0-development", + "description": "Cypress smart installation wizard", + "private": false, + "main": "index.js", + "scripts": { + "build": "yarn prepare-example && tsc -p ./tsconfig.json && chmod +x dist/src/index.js && node scripts/example copy-to ./dist/initial-template", + "prepare-example": "node scripts/example copy-to ./initial-template", + "test": "cross-env TS_NODE_PROJECT=./tsconfig.test.json mocha --config .mocharc.json './src/**/*.test.ts'", + "test:watch": "yarn test -w" + }, + "dependencies": { + "@babel/core": "^7.5.4", + "@babel/plugin-transform-typescript": "^7.2.0", + "@babel/template": "^7.5.4", + "@babel/types": "^7.5.0", + "bluebird": "^3.7.2", + "chalk": "4.1.0", + "cli-highlight": "2.1.4", + "commander": "6.1.0", + "find-up": "5.0.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "inquirer": "7.3.3", + "ora": "^5.1.0" + }, + "devDependencies": { + "@types/babel__core": "^7.1.2", + "@types/inquirer": "7.3.1", + "@types/mock-fs": "4.10.0", + "@types/node": "9.6.49", + "@types/ora": "^3.2.0", + "mocha": "7.1.1", + "mock-fs": "4.13.0", + "typescript": "4.0.3" + }, + "bin": { + "create-cypress-tests": "dist/src/index.js" + }, + "license": "MIT", + "repository": "https://github.com/cypress-io/cypress.git", + "author": "Cypress.io team" +} diff --git a/npm/create-cypress-tests/scripts/example.js b/npm/create-cypress-tests/scripts/example.js new file mode 100644 index 000000000000..23fd021fea45 --- /dev/null +++ b/npm/create-cypress-tests/scripts/example.js @@ -0,0 +1,19 @@ +const fs = require('fs-extra') +const chalk = require('chalk') +const path = require('path') +const program = require('commander') + +program +.command('copy-to [destination]') +.description('copy cypress/packages/example into destination') +.action(async (destination) => { + const exampleFolder = path.resolve(__dirname, '..', '..', '..', 'packages', 'example') + const destinationPath = path.resolve(process.cwd(), destination) + + await fs.remove(destinationPath) + await fs.copy(exampleFolder, destinationPath, { recursive: true }) + + console.log(`โœ… Example was successfully created at ${chalk.cyan(destination)}`) +}) + +program.parse(process.argv) diff --git a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts new file mode 100644 index 000000000000..78c6e5c93c2a --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts @@ -0,0 +1,85 @@ +import * as babel from '@babel/core' +import { expect } from 'chai' +import { createSupportBabelPlugin, createTransformPluginsFileBabelPlugin } from './babelTransform' + +describe('babel transform utils', () => { + context('support babel template', () => { + it('injects import after the last import in the file', () => { + const plugin = createSupportBabelPlugin('import "@cypress/react"') + + const output = babel.transformSync([ + 'import "./commands.js"', + ].join('\n'), { + plugins: [plugin], + })?.code + + expect(output).to.equal([ + 'import "./commands.js";', + 'import "@cypress/react";', + ].join('\n')) + }) + + it('injects import after the last import if a lot of imports and code inside', () => { + const plugin = createSupportBabelPlugin('import "@cypress/react"') + + const output = babel.transformSync([ + 'import "./commands.js";', + 'import "./commands4.js";', + 'import "./commands3.js";', + 'import "./commands2.js";', + '', + 'function hello() {', + ' console.log("world");', + '}', + ].join('\n'), { + plugins: [plugin], + })?.code + + expect(output).to.equal([ + 'import "./commands.js";', + 'import "./commands4.js";', + 'import "./commands3.js";', + 'import "./commands2.js";', + 'import "@cypress/react";', + '', + 'function hello() {', + ' console.log("world");', + '}', + ].join('\n')) + }) + + it('adds import as 1st line if no imports or require found', () => { + const plugin = createSupportBabelPlugin('import "@cypress/react"') + + const output = babel.transformSync('', { plugins: [plugin] })?.code + + expect(output).to.equal('import "@cypress/react";') + }) + }) + + context('Plugins config babel plugin', () => { + it('injects code into the plugins file based on ast', () => { + const plugin = createTransformPluginsFileBabelPlugin({ + Require: babel.template.ast('require("something")'), + ModuleExportsBody: babel.template.ast('yey()'), + }) + + const output = babel.transformSync([ + 'module.exports = (on, config) => {', + 'on("do")', + '}', + ].join('\n'), { + plugins: [plugin], + })?.code + + expect(output).to.equal([ + 'require("something");', + '', + 'module.exports = (on, config) => {', + ' on("do");', + ' yey();', + '};', + ].join(`\n`)) + }) + }) +}) diff --git a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts new file mode 100644 index 000000000000..56be68ce83ea --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts @@ -0,0 +1,147 @@ +import path from 'path' +import * as fs from 'fs-extra' +import * as babel from '@babel/core' +import * as babelTypes from '@babel/types' + +export type PluginsConfigAst = Record<'Require' | 'ModuleExportsBody', ReturnType> + +function tryRequirePrettier () { + try { + return require('prettier') + } catch (e) { + return null + } +} + +async function transformFileViaPlugin (filePath: string, babelPlugin: babel.PluginObj) { + try { + const initialCode = await fs.readFile(filePath, { encoding: 'utf-8' }) + + const updatedResult = await babel.transformAsync(initialCode, { + filename: path.basename(filePath), + filenameRelative: path.relative(process.cwd(), filePath), + plugins: [babelPlugin], + presets: [], + }) + + if (!updatedResult) { + return false + } + + let finalCode = updatedResult.code + + if (finalCode === initialCode) { + return false + } + + const maybePrettier = tryRequirePrettier() + + if (maybePrettier && maybePrettier.format) { + finalCode = maybePrettier.format(finalCode, { parser: 'babel' }) + } + + await fs.writeFile(filePath, finalCode) + + return true + } catch (e) { + return false + } +} + +export function createTransformPluginsFileBabelPlugin (ast: PluginsConfigAst): babel.PluginObj { + return { + visitor: { + Program: (path) => { + path.unshiftContainer('body', ast.Require) + }, + Function: (path) => { + if (!babelTypes.isAssignmentExpression(path.parent)) { + return + } + + const assignment = path.parent.left + + const isModuleExports = + babelTypes.isMemberExpression(assignment) + && babelTypes.isIdentifier(assignment.object) + && assignment.object.name === 'module' + && babelTypes.isIdentifier(assignment.property) + && assignment.property.name === 'exports' + + if (isModuleExports && babelTypes.isFunction(path.parent.right)) { + const paramsLength = path.parent.right.params.length + + if (paramsLength === 0) { + path.parent.right.params.push(babelTypes.identifier('on')) + path.parent.right.params.push(babelTypes.identifier('config')) + } + + if (paramsLength === 1) { + path.parent.right.params.push(babelTypes.identifier('config')) + } + + path.get('body').pushContainer('body' as never, ast.ModuleExportsBody) + } + }, + }, + } +} + +export async function injectPluginsCode (pluginsFilePath: string, ast: PluginsConfigAst) { + return transformFileViaPlugin(pluginsFilePath, createTransformPluginsFileBabelPlugin(ast)) +} + +export async function getPluginsSourceExample (ast: PluginsConfigAst) { + const exampleCode = [ + 'module.exports = (on, config) => {', + '', + '}', + ].join('\n') + + try { + const babelResult = await babel.transformAsync(exampleCode, { + filename: 'nothing.js', + plugins: [createTransformPluginsFileBabelPlugin(ast)], + presets: [], + }) + + if (!babelResult?.code) { + throw new Error() + } + + return babelResult.code + } catch (e) { + throw new Error('Can not generate code example for plugins file because of unhandled error. Please update the plugins file manually.') + } +} + +export function createSupportBabelPlugin (importCode: string): babel.PluginObj { + const template = babel.template.ast(importCode) + + const plugin: babel.PluginObj<{ + root: babel.NodePath + lastImport: babel.NodePath |null + }> = { + visitor: { + Program (path) { + this.root = path + }, + ImportDeclaration (path) { + this.lastImport = path + }, + }, + post () { + if (this.lastImport) { + this.lastImport.insertAfter(template) + } else if (this.root) { + this.root.unshiftContainer('body', template) + } + }, + } + + return plugin +} + +export async function injectImportSupportCode (supportFilePath: string, importCode: string) { + return transformFileViaPlugin(supportFilePath, createSupportBabelPlugin(importCode)) +} diff --git a/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts b/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts new file mode 100644 index 000000000000..1353623145f4 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts @@ -0,0 +1,266 @@ +import path from 'path' +import fs from 'fs-extra' +import snapshot from 'snap-shot-it' +import { expect, use } from 'chai' +import sinon, { SinonStub, SinonSpy } from 'sinon' +import chalk from 'chalk' +import mockFs from 'mock-fs' +import { initComponentTesting } from './init-component-testing' +import inquirer from 'inquirer' +import sinonChai from 'sinon-chai' +import childProcess from 'child_process' +import { someOfSpyCallsIncludes } from '../test-utils' + +use(sinonChai) + +describe('init component tests script', () => { + let promptSpy: SinonStub | null = null + let logSpy: SinonSpy | null = null + let processExitStub: SinonStub | null = null + let execStub: SinonStub | null = null + + const e2eTestOutputPath = path.resolve(__dirname, '..', 'test-output') + const cypressConfigPath = path.join(e2eTestOutputPath, 'cypress.json') + + beforeEach(async () => { + logSpy = sinon.spy(global.console, 'log') + // @ts-ignores + execStub = sinon.stub(childProcess, 'exec').callsFake((command, callback) => callback()) + processExitStub = sinon.stub(process, 'exit').callsFake(() => { + throw new Error(`${chalk.red('process.exit')} should not be called`) + }) + + await fs.remove(e2eTestOutputPath) + await fs.mkdir(e2eTestOutputPath) + }) + + afterEach(() => { + mockFs.restore() + logSpy?.restore() + promptSpy?.restore() + processExitStub?.restore() + execStub?.restore() + }) + + function createTempFiles (tempFiles: Record) { + Object.entries(tempFiles).forEach(([fileName, content]) => { + fs.outputFileSync( + path.join(e2eTestOutputPath, fileName), + content, + ) + }) + } + + function snapshotGeneratedFiles (name: string) { + snapshot( + `${name} cypress.json`, + fs.readFileSync( + path.join(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'), + { encoding: 'utf-8' }, + ), + ) + + snapshot( + `${name} plugins/index.js`, + fs.readFileSync( + path.join(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'), + { encoding: 'utf-8' }, + ), + ) + + snapshot( + `${name} support/index.js`, + fs.readFileSync( + path.join(e2eTestOutputPath, 'cypress', 'support', 'index.js'), + { encoding: 'utf-8' }, + ), + ) + } + + it('determines more presumable configuration to suggest', async () => { + createTempFiles({ + '/cypress.json': '{}', + '/cypress/support/index.js': '', + '/cypress/plugins/index.js': 'module.exports = (on, config) => {}', + // For next.js user will have babel config, but we want to suggest to use the closest config for the application code + '/babel.config.js': 'module.exports = { }', + '/package.json': JSON.stringify({ dependencies: { react: '^17.x', next: '^9.2.0' } }), + }) + + promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ + chosenTemplateName: 'next.js', + componentFolder: 'src', + }) as any) + + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) + + const [{ choices }] = (inquirer.prompt as any).args[0][0] + + expect(choices[0]).to.equal('next.js') + snapshotGeneratedFiles('Injects guessed next.js template') + }) + + it('automatically suggests to the user which config to use', async () => { + createTempFiles({ + '/cypress.json': '{}', + '/cypress/support/index.js': 'import "./commands.js";', + '/cypress/plugins/index.js': 'module.exports = () => {}', + '/package.json': JSON.stringify({ + dependencies: { + react: '^16.10.0', + }, + }), + '/webpack.config.js': 'module.exports = { }', + }) + + promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ + chosenTemplateName: 'create-react-app', + componentFolder: 'cypress/component', + }) as any) + + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) + const [{ choices, message }] = (inquirer.prompt as any).args[0][0] + + expect(choices[0]).to.equal('webpack') + expect(message).to.contain( + `Press ${chalk.inverse(' Enter ')} to continue with ${chalk.green( + 'webpack', + )} configuration`, + ) + + snapshotGeneratedFiles('Injected overridden webpack template') + }) + + it('Asks for preferred bundling tool if can not determine the right one', async () => { + createTempFiles({ + '/cypress.json': '{}', + '/webpack.config.js': 'module.exports = { }', + '/package.json': JSON.stringify({ dependencies: { } }), + }) + + promptSpy = sinon.stub(inquirer, 'prompt') + .onCall(0) + .returns(Promise.resolve({ + framework: 'vue', + }) as any) + .onCall(1) + .returns(Promise.resolve({ + chosenTemplateName: 'webpack', + componentFolder: 'src', + }) as any) + + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) + + expect( + someOfSpyCallsIncludes(global.console.log, 'We were unable to automatically determine your framework ๐Ÿ˜ฟ'), + ).to.be.true + }) + + it('Asks for framework if more than 1 option was auto detected', async () => { + createTempFiles({ + '/cypress.json': '{}', + '/webpack.config.js': 'module.exports = { }', + '/package.json': JSON.stringify({ dependencies: { react: '*', vue: '*' } }), + }) + + promptSpy = sinon.stub(inquirer, 'prompt') + .onCall(0) + .returns(Promise.resolve({ + framework: 'vue', + }) as any) + .onCall(1) + .returns(Promise.resolve({ + chosenTemplateName: 'webpack', + componentFolder: 'src', + }) as any) + + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) + + expect( + someOfSpyCallsIncludes(global.console.log, `It looks like all these frameworks: ${chalk.yellow('react, vue')} are available from this directory.`), + ).to.be.true + }) + + it('installs the right adapter', () => { + + }) + + it('suggest the right instruction based on user template choice', async () => { + createTempFiles({ + '/package.json': JSON.stringify({ + dependencies: { + react: '^16.0.0', + }, + }), + '/cypress.json': '{}', + }) + + promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ + chosenTemplateName: 'create-react-app', + componentFolder: 'src', + }) as any) + + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) + expect( + someOfSpyCallsIncludes( + global.console.log, + 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts', + ), + ).to.be.true + }) + + it('suggests right docs example and cypress.json config based on the `componentFolder` answer', async () => { + createTempFiles({ + '/cypress.json': '{}', + '/package.json': JSON.stringify({ + dependencies: { + react: '^16.0.0', + }, + }), + }) + + sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ + chosenTemplateName: 'create-react-app', + componentFolder: 'cypress/component', + }) as any) + + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) + + const injectedCode = fs.readFileSync(path.join(e2eTestOutputPath, 'cypress.json'), { encoding: 'utf-8' }) + + expect(injectedCode).to.equal(JSON.stringify( + { + experimentalComponentTesting: true, + componentFolder: 'cypress/component', + testFiles: '**/*.spec.{js,ts,jsx,tsx}', + }, + null, + 2, + )) + }) + + it('Shows help message if cypress files are not created', async () => { + createTempFiles({ + '/cypress.json': '{}', + '/package.json': JSON.stringify({ + dependencies: { + react: '^16.0.0', + }, + }), + }) + + sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ + chosenTemplateName: 'create-react-app', + componentFolder: 'cypress/component', + }) as any) + + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) + + expect( + someOfSpyCallsIncludes( + global.console.log, + 'was not updated automatically. Please add the following config manually:', + ), + ).to.be.true + }) +}) diff --git a/npm/create-cypress-tests/src/component-testing/init-component-testing.ts b/npm/create-cypress-tests/src/component-testing/init-component-testing.ts new file mode 100644 index 000000000000..a6a7dc86c486 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/init-component-testing.ts @@ -0,0 +1,216 @@ +import fs from 'fs-extra' +import path from 'path' +import chalk from 'chalk' +import inquirer from 'inquirer' +import highlight from 'cli-highlight' +import { Template } from './templates/Template' +import { guessTemplate } from './templates/guessTemplate' +import { installFrameworkAdapter } from './installFrameworkAdapter' +import { injectImportSupportCode, injectPluginsCode, getPluginsSourceExample } from './babel/babelTransform' + +async function injectOrShowConfigCode (injectFn: () => Promise, { + code, + filePath, + fallbackFileMessage, + language, +}: { + code: string + filePath: string + language: string + fallbackFileMessage: string +}) { + const fileExists = fs.existsSync(filePath) + const readableFilePath = fileExists ? path.relative(process.cwd(), filePath) : fallbackFileMessage + + const printCode = () => { + console.log() + console.log(highlight(code, { language })) + console.log() + } + + const printSuccess = () => { + console.log(`โœ… ${chalk.bold.green(readableFilePath)} was updated with the following config:`) + printCode() + } + + const printFailure = () => { + console.log(`โŒ ${chalk.bold.red(readableFilePath)} was not updated automatically. Please add the following config manually: `) + printCode() + } + + if (!fileExists) { + printFailure() + + return + } + + // something get completely wrong when using babel or something. Print error message. + const injected = await injectFn().catch(() => false) + + injected ? printSuccess() : printFailure() +} + +async function injectAndShowCypressJsonConfig ( + cypressJsonPath: string, + componentFolder: string, +) { + const configToInject = { + experimentalComponentTesting: true, + componentFolder, + testFiles: '**/*.spec.{js,ts,jsx,tsx}', + } + + async function autoInjectCypressJson () { + const currentConfig = JSON.parse(await fs.readFile(cypressJsonPath, { encoding: 'utf-8' })) + + await fs.writeFile(cypressJsonPath, JSON.stringify({ + ...currentConfig, + ...configToInject, + }, null, 2)) + + return true + } + + await injectOrShowConfigCode(autoInjectCypressJson, { + code: JSON.stringify(configToInject, null, 2), + language: 'js', + filePath: cypressJsonPath, + fallbackFileMessage: 'cypress.json config file', + }) +} + +async function injectAndShowSupportConfig (supportFilePath: string, framework: string) { + const importCode = framework === 'vue' + ? `import '@cypress/vue/dist/support'` // todo change vue bundle to output the right declaration + : `import \'@cypress/${framework}/support\'` + + await injectOrShowConfigCode(() => injectImportSupportCode(supportFilePath, importCode), { + code: importCode, + language: 'js', + filePath: supportFilePath, + fallbackFileMessage: 'support file (https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Support-file)', + }) +} + +async function injectAndShowPluginConfig (template: Template, { + templatePayload, + pluginsFilePath, + cypressProjectRoot, +}: { + templatePayload: T | null + pluginsFilePath: string + cypressProjectRoot: string +}) { + const ast = template.getPluginsCodeAst(templatePayload, { cypressProjectRoot }) + + await injectOrShowConfigCode(() => injectPluginsCode(pluginsFilePath, ast), { + code: await getPluginsSourceExample(ast), + language: 'js', + filePath: pluginsFilePath, + fallbackFileMessage: 'plugins file (https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Plugin-files)', + }) +} + +type InitComponentTestingOptions = { + config: Record + cypressConfigPath: string + useYarn: boolean +} + +export async function initComponentTesting ({ config, useYarn, cypressConfigPath }: InitComponentTestingOptions) { + const cypressProjectRoot = path.resolve(cypressConfigPath, '..') + + const framework = await installFrameworkAdapter(cypressProjectRoot, { useYarn }) + const { + possibleTemplates, + defaultTemplate, + defaultTemplateName, + templatePayload, + } = await guessTemplate(framework, cypressProjectRoot) + + const pluginsFilePath = path.resolve( + cypressProjectRoot, + config.pluginsFile ?? './cypress/plugins/index.js', + ) + + const supportFilePath = path.resolve( + cypressProjectRoot, + config.supportFile ?? './cypress/support/index.js', + ) + + const templateChoices = Object.keys(possibleTemplates).sort((key) => { + return key === defaultTemplateName ? -1 : 0 + }) + + const { + chosenTemplateName, + componentFolder, + }: Record = await inquirer.prompt([ + { + type: 'list', + name: 'chosenTemplateName', + choices: templateChoices, + default: defaultTemplate ? 0 : undefined, + message: defaultTemplate?.message + ? `${defaultTemplate?.message}\n\n Press ${chalk.inverse( + ' Enter ', + )} to continue with ${chalk.green( + defaultTemplateName, + )} configuration or select another template from the list:` + : 'We were not able to automatically determine which framework or bundling tool you are using. Please choose one from the list:', + }, + { + type: 'input', + name: 'componentFolder', + filter: (input) => input.trim(), + validate: (input) => { + return input === '' || !/^[a-zA-Z].*/.test(input) + ? `Directory "${input}" is invalid` + : true + }, + message: 'Which folder would you like to use for your component tests?', + default: (answers: { chosenTemplateName: keyof typeof possibleTemplates }) => { + return possibleTemplates[answers.chosenTemplateName].recommendedComponentFolder + }, + }, + ]) + + const chosenTemplate = possibleTemplates[chosenTemplateName] as Template + + console.log() + console.log(`Here are instructions of how to get started with component testing for ${chalk.cyan(chosenTemplateName)}:`) + console.log() + + await injectAndShowCypressJsonConfig(cypressConfigPath, componentFolder) + await injectAndShowSupportConfig(supportFilePath, framework) + await injectAndShowPluginConfig(chosenTemplate, { + templatePayload, + pluginsFilePath, + cypressProjectRoot, + }) + + if (chosenTemplate.printHelper) { + chosenTemplate.printHelper() + } + + console.log( + `Find examples of component tests for ${chalk.green( + chosenTemplateName, + )} in ${chalk.underline(chosenTemplate.getExampleUrl({ componentFolder }))}.`, + ) + + if (framework === 'react') { + console.log() + + console.log( + `Docs for different recipes of bundling tools: ${chalk.bold.underline( + 'https://github.com/cypress-io/cypress/tree/develop/npm/react/docs/recipes.md', + )}`, + ) + } + + // render delimiter + console.log() + console.log(new Array(process.stdout.columns).fill('โ•').join('')) + console.log() +} diff --git a/npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts b/npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts new file mode 100644 index 000000000000..c08f0074bc50 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts @@ -0,0 +1,56 @@ +import chalk from 'chalk' +import inquirer from 'inquirer' +import { scanFSForAvailableDependency } from '../findPackageJson' +import { installDependency } from '../utils' + +async function guessOrAskForFramework (cwd: string): Promise<'react' | 'vue'> { + // please sort this alphabetically + const frameworks = { + react: () => scanFSForAvailableDependency(cwd, ['react', 'react-dom']), + vue: () => scanFSForAvailableDependency(cwd, ['vue']), + } + + const guesses = Object.keys(frameworks).filter((framework) => { + return frameworks[framework as keyof typeof frameworks]() + }) as Array<'react' | 'vue'> + + // found 1 precise guess. Continue + if (guesses.length === 1) { + const framework = guesses[0] + + console.log(`\nThis project is using ${chalk.bold.cyan(framework)}. Let's install the right adapter:`) + + return framework + } + + if (guesses.length === 0) { + console.log(`We were unable to automatically determine your framework ๐Ÿ˜ฟ. ${chalk.grey('Make sure to run this command from the directory where your components located in order to make smart detection works. Or continue with manual setup:')}`) + } + + if (guesses.length > 0) { + console.log(`It looks like all these frameworks: ${chalk.yellow(guesses.join(', '))} are available from this directory. ${chalk.grey('Make sure to run this command from the directory where your components located in order to make smart detection works. Or continue with manual setup:')}`) + } + + const { framework } = await inquirer.prompt([ + { + type: 'list', + name: 'framework', + choices: Object.keys(frameworks), + message: `Which framework do you use?`, + }, + ]) + + return framework +} + +type InstallAdapterOptions = { + useYarn: boolean +} + +export async function installFrameworkAdapter (cwd: string, options: InstallAdapterOptions) { + const framework = await guessOrAskForFramework(cwd) + + await installDependency(`@cypress/${framework}`, options) + + return framework +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/Template.ts b/npm/create-cypress-tests/src/component-testing/templates/Template.ts new file mode 100644 index 000000000000..426b156bc544 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/Template.ts @@ -0,0 +1,13 @@ +import { PluginsConfigAst } from '../babel/babelTransform' + +export interface Template { + message: string + getExampleUrl: ({ componentFolder }: { componentFolder: string }) => string + recommendedComponentFolder: string + test(rootPath: string): { success: boolean, payload?: T } + getPluginsCodeAst: ( + payload: T | null, + options: { cypressProjectRoot: string }, + ) => PluginsConfigAst + printHelper?: () => void +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts b/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts new file mode 100644 index 000000000000..dffffe4e0e7f --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts @@ -0,0 +1,33 @@ +import { Template } from './Template' +import { reactTemplates } from './react' +import { vueTemplates } from './vue' + +const allTemplates = { + react: reactTemplates, + vue: vueTemplates, +} + +export async function guessTemplate (framework: keyof typeof allTemplates, cwd: string) { + const templates = allTemplates[framework] + + for (const [name, template] of Object.entries(templates)) { + const typedTemplate = template as Template + const { success, payload } = typedTemplate.test(cwd) + + if (success) { + return { + defaultTemplate: typedTemplate, + defaultTemplateName: name, + templatePayload: payload ?? null, + possibleTemplates: templates, + } + } + } + + return { + templatePayload: null, + defaultTemplate: null, + defaultTemplateName: null, + possibleTemplates: templates, + } +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts new file mode 100644 index 000000000000..1bff7af467af --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts @@ -0,0 +1,73 @@ +import { expect } from 'chai' +import mockFs from 'mock-fs' +import { BabelTemplate } from './babel' +import { snapshotPluginsAstCode } from '../../../test-utils' + +describe('babel installation template', () => { + beforeEach(mockFs.restore) + + it('resolves babel.config.json', () => { + mockFs({ + '/babel.config.json': JSON.stringify({ + presets: [], + plugins: [], + }), + }) + + const { success } = BabelTemplate.test('/') + + expect(success).to.equal(true) + }) + + it('resolves babel.config.js', () => { + mockFs({ + '/project/babel.config.js': + 'module.exports = { presets: [], plugins: [] };', + '/project/index/package.json': 'dev/null', + }) + + const { success } = BabelTemplate.test('/project/index') + + expect(success).to.equal(true) + }) + + it('resolves babel config from the deep folder', () => { + mockFs({ + '/some/.babelrc': JSON.stringify({ + presets: [], + plugins: [], + }), + '/some/deep/folder/text.txt': '1', + }) + + const { success } = BabelTemplate.test('/some/deep/folder') + + expect(success).to.equal(true) + }) + + it('fails if no babel config found', () => { + mockFs({ + '/some.txt': '1', + }) + + const { success } = BabelTemplate.test('/') + + expect(success).to.equal(false) + }) + + it('resolves babel.config from package.json', () => { + mockFs({ + '/package.json': JSON.stringify({ + babel: { + presets: [], + }, + }), + }) + + const { success } = BabelTemplate.test('/') + + expect(success).to.equal(true) + }) + + it('correctly generates plugins config', () => snapshotPluginsAstCode(BabelTemplate)) +}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts b/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts new file mode 100644 index 000000000000..5232653d17f8 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts @@ -0,0 +1,43 @@ +import chalk from 'chalk' +import findUp from 'find-up' +import * as babel from '@babel/core' +import { Template } from '../Template' +import { createFindPackageJsonIterator } from '../../../findPackageJson' + +export const BabelTemplate: Template = { + message: `It looks like you have babel config defined. We can use it to transpile your components for testing.\n ${chalk.red( + '>>', + )} This is not a replacement for bundling tool. We will use ${chalk.red( + 'webpack', + )} to bundle the components for testing.`, + recommendedComponentFolder: 'cypress/component', + getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/babel', + getPluginsCodeAst: () => { + return { + Require: babel.template.ast('const preprocessor = require(\'@cypress/react/plugins/babel\')'), + ModuleExportsBody: babel.template.ast([ + 'preprocessor(on, config)', + 'return config // IMPORTANT to return the config object', + ].join('\n'), { preserveComments: true }), + } + }, + test: (cwd) => { + const babelConfig = findUp.sync( + ['babel.config.js', 'babel.config.json', '.babelrc', '.babelrc.json'], + { type: 'file', cwd }, + ) + + if (babelConfig) { + return { success: true } + } + + // babel config can also be declared in package.json with `babel` key https://babeljs.io/docs/en/configuration#packagejson + const packageJsonIterator = createFindPackageJsonIterator(cwd) + + return packageJsonIterator.map(({ babel }) => { + return { + success: Boolean(babel), + } + }) + }, +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/index.ts b/npm/create-cypress-tests/src/component-testing/templates/react/index.ts new file mode 100644 index 000000000000..bbbfca5178fb --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/index.ts @@ -0,0 +1,14 @@ +import { Template } from '../Template' +import { NextTemplate } from './next' +import { WebpackTemplate } from './reactWebpackFile' +import { ReactScriptsTemplate } from './react-scripts' +import { BabelTemplate } from './babel' +import { WebpackOptions } from './webpack-options' + +export const reactTemplates: Record> = { + 'create-react-app': ReactScriptsTemplate, + 'next.js': NextTemplate, + webpack: WebpackTemplate, + babel: BabelTemplate, + 'default (webpack options)': WebpackOptions, +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts new file mode 100644 index 000000000000..4701791877e3 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts @@ -0,0 +1,77 @@ +import sinon, { SinonSpy } from 'sinon' +import { expect, use } from 'chai' +import sinonChai from 'sinon-chai' +import mockFs from 'mock-fs' +import { NextTemplate } from './next' +import { snapshotPluginsAstCode } from '../../../test-utils' + +use(sinonChai) + +describe('next.js install template', () => { + let warnSpy: SinonSpy | null = null + + beforeEach(() => { + warnSpy = sinon.spy(global.console, 'warn') + }) + + afterEach(() => { + mockFs.restore() + warnSpy?.restore() + }) + + it('finds the closest package.json and checks that next is declared as dependency', () => { + mockFs({ + '/package.json': JSON.stringify({ + dependencies: { + next: '^9.2.3', + }, + scripts: { + build: 'next', + }, + }), + }) + + const { success } = NextTemplate.test('/') + + expect(success).to.equal(true) + }) + + it('works if next is declared in the devDependencies as well', () => { + mockFs({ + './package.json': JSON.stringify({ + devDependencies: { + next: '^9.2.3', + }, + scripts: { + build: 'next', + }, + }), + }) + + const { success } = NextTemplate.test(process.cwd()) + + expect(success).to.equal(true) + }) + + it('warns and fails if version is not supported', () => { + mockFs({ + './package.json': JSON.stringify({ + devDependencies: { + next: '^8.2.3', + }, + scripts: { + build: 'next', + }, + }), + }) + + const { success } = NextTemplate.test('i/am/in/some/deep/folder') + + console.log(global.console.warn) + expect(success).to.equal(false) + + expect(global.console.warn).to.be.called + }) + + it('correctly generates plugins config', () => snapshotPluginsAstCode(NextTemplate)) +}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/next.ts b/npm/create-cypress-tests/src/component-testing/templates/react/next.ts new file mode 100644 index 000000000000..aee4ce57f2d5 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/next.ts @@ -0,0 +1,54 @@ +import * as babel from '@babel/core' +import { createFindPackageJsonIterator } from '../../../findPackageJson' +import { Template } from '../Template' +import { validateSemverVersion } from '../../../utils' +import { MIN_SUPPORTED_VERSION } from '../../versions' + +export const NextTemplate: Template = { + message: 'It looks like you are using next.js.', + getExampleUrl: () => { + return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/nextjs' + }, + recommendedComponentFolder: 'cypress/component', + getPluginsCodeAst: () => { + return { + Require: babel.template.ast('const preprocessor = require(\'@cypress/react/plugins/next\')'), + ModuleExportsBody: babel.template.ast([ + 'preprocessor(on, config)', + 'return config // IMPORTANT to return the config object', + ].join('\n'), { preserveComments: true }), + } + }, + test: (cwd) => { + const packageJsonIterator = createFindPackageJsonIterator(cwd) + + return packageJsonIterator.map(({ dependencies, devDependencies }, path) => { + if (!dependencies && !devDependencies) { + return { success: false } + } + + const allDeps = { + ...(devDependencies || {}), + ...(dependencies || {}), + } as Record + + const nextVersion = allDeps['next'] + + if (!nextVersion) { + return { success: false } + } + + if ( + !validateSemverVersion( + nextVersion, + MIN_SUPPORTED_VERSION['next'], + 'next.js', + ) + ) { + return { success: false } + } + + return { success: true } + }) + }, +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts new file mode 100644 index 000000000000..7bf01624006a --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts @@ -0,0 +1,66 @@ +import sinon, { SinonSpy } from 'sinon' +import { expect, use } from 'chai' +import sinonChai from 'sinon-chai' +import mockFs from 'mock-fs' +import { ReactScriptsTemplate } from './react-scripts' +import { snapshotPluginsAstCode } from '../../../test-utils' + +use(sinonChai) + +describe('create-react-app install template', () => { + let warnSpy: SinonSpy | null = null + + beforeEach(() => { + warnSpy = sinon.spy(global.console, 'warn') + }) + + afterEach(() => { + mockFs.restore() + warnSpy?.restore() + }) + + it('finds the closest package.json and checks that react-scripts is declared as dependency', () => { + mockFs({ + '/package.json': JSON.stringify({ + dependencies: { + 'react-scripts': '^3.2.3', + }, + }), + }) + + const { success } = ReactScriptsTemplate.test(process.cwd()) + + expect(success).to.equal(true) + }) + + it('works if react-scripts is declared in the devDependencies as well', () => { + mockFs({ + './package.json': JSON.stringify({ + devDependencies: { + 'react-scripts': '^3.2.3', + }, + }), + }) + + const { success } = ReactScriptsTemplate.test(process.cwd()) + + expect(success).to.equal(true) + }) + + it('warns and fails if version is not supported', () => { + mockFs({ + './package.json': JSON.stringify({ + devDependencies: { + 'react-scripts': '^2.2.3', + }, + }), + }) + + const { success } = ReactScriptsTemplate.test(process.cwd()) + + expect(success).to.equal(false) + expect(global.console.warn).to.be.called + }) + + it('correctly generates plugins config', () => snapshotPluginsAstCode(ReactScriptsTemplate)) +}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts new file mode 100644 index 000000000000..bddd0e5afb22 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts @@ -0,0 +1,61 @@ +import chalk from 'chalk' +import { createFindPackageJsonIterator } from '../../../findPackageJson' +import { Template } from '../Template' +import { validateSemverVersion } from '../../../utils' +import { MIN_SUPPORTED_VERSION } from '../../versions' +import * as babel from '@babel/core' + +export const ReactScriptsTemplate: Template = { + recommendedComponentFolder: 'src', + message: 'It looks like you are using create-react-app.', + getExampleUrl: ({ componentFolder }) => { + return componentFolder === 'src' + ? 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts' + : 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts-folder' + }, + getPluginsCodeAst: () => { + return { + Require: babel.template.ast('const preprocessor = require(\'@cypress/react/plugins/react-scripts\')'), + ModuleExportsBody: babel.template.ast([ + 'preprocessor(on, config)', + 'return config // IMPORTANT to return the config object', + ].join('\n'), { preserveComments: true }), + } + }, + test: () => { + // TODO also determine ejected create react app + const packageJsonIterator = createFindPackageJsonIterator(process.cwd()) + + return packageJsonIterator.map(({ dependencies, devDependencies }) => { + if (dependencies || devDependencies) { + const allDeps = { ...devDependencies, ...dependencies } || {} + + if (!allDeps['react-scripts']) { + return { success: false } + } + + if ( + !validateSemverVersion( + allDeps['react-scripts'], + MIN_SUPPORTED_VERSION['react-scripts'], + ) + ) { + console.warn( + `It looks like you are using ${chalk.green( + 'create-react-app', + )}, but we support only projects with version ${chalk.bold( + MIN_SUPPORTED_VERSION['react-scripts'], + )} of react-scripts.`, + ) + + // yey found the template + return { success: false } + } + + return { success: true } + } + + return { success: false } + }) + }, +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts new file mode 100644 index 000000000000..4212fcc04f51 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts @@ -0,0 +1,74 @@ +import { expect } from 'chai' +import mockFs from 'mock-fs' +import { snapshotPluginsAstCode } from '../../../test-utils' +import { WebpackTemplate } from './reactWebpackFile' + +describe('webpack-file install template', () => { + afterEach(mockFs.restore) + + it('resolves webpack.config.js', () => { + mockFs({ + '/webpack.config.js': 'module.exports = { }', + }) + + const { success, payload } = WebpackTemplate.test(process.cwd()) + + expect(success).to.equal(true) + expect(payload?.webpackConfigPath).to.equal('/webpack.config.js') + }) + + it('finds the closest package.json and tries to fetch webpack config path from scrips', () => { + mockFs({ + '/configs/webpack.js': 'module.exports = { }', + '/package.json': JSON.stringify({ + scripts: { + build: 'webpack --config configs/webpack.js', + }, + }), + }) + + const { success, payload } = WebpackTemplate.test(process.cwd()) + + expect(success).to.equal(true) + expect(payload?.webpackConfigPath).to.equal('/configs/webpack.js') + }) + + it('looks for package.json in the upper folder', () => { + mockFs({ + '/i/am/in/some/deep/folder/withFile': 'test', + '/somewhere/configs/webpack.js': 'module.exports = { }', + '/package.json': JSON.stringify({ + scripts: { + build: 'webpack --config somewhere/configs/webpack.js', + }, + }), + }) + + const { success, payload } = WebpackTemplate.test( + 'i/am/in/some/deep/folder', + ) + + expect(success).to.equal(true) + expect(payload?.webpackConfigPath).to.equal('/somewhere/configs/webpack.js') + }) + + it('returns success:false if cannot find webpack config', () => { + mockFs({ + '/a.js': '1', + '/b.js': '2', + }) + + const { success, payload } = WebpackTemplate.test('/') + + expect(success).to.equal(false) + expect(payload).to.equal(undefined) + }) + + it('correctly generates plugins config when webpack config path is missing', () => { + snapshotPluginsAstCode(WebpackTemplate) + }) + + it('correctly generates plugins config when webpack config path is provided', () => { + snapshotPluginsAstCode(WebpackTemplate, { webpackConfigPath: '/config/webpack.config.js' }) + }) +}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts new file mode 100644 index 000000000000..c977f31afc52 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts @@ -0,0 +1,41 @@ +import * as babel from '@babel/core' +import path from 'path' +import { Template } from '../Template' +import { findWebpackConfig } from '../templateUtils' + +export const WebpackTemplate: Template<{ webpackConfigPath: string }> = { + message: + 'It looks like you have custom `webpack.config.js`. We can use it to bundle the components for testing.', + getExampleUrl: () => { + return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/webpack-file' + }, + recommendedComponentFolder: 'cypress/component', + getPluginsCodeAst: (payload, { cypressProjectRoot }) => { + const includeWarnComment = !payload + const webpackConfigPath = payload + ? path.relative(cypressProjectRoot, payload.webpackConfigPath) + : './webpack.config.js' + + return { + Require: babel.template.ast('const preprocessor = require("@cypress/react/plugins/load-webpack")'), + ModuleExportsBody: babel.template.ast([ + includeWarnComment + ? '// TODO replace with valid webpack config path' + : '', + `config.env.webpackFilename = '${webpackConfigPath}'`, + 'preprocessor(on, config)', + 'return config // IMPORTANT to return the config object', + ].join('\n'), { preserveComments: true }), + } + }, + test: (root) => { + const webpackConfigPath = findWebpackConfig(root) + + return webpackConfigPath ? { + success: true, + payload: { webpackConfigPath }, + } : { + success: false, + } + }, +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/rollup.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/rollup.test.ts new file mode 100644 index 000000000000..789419d84da5 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/rollup.test.ts @@ -0,0 +1,72 @@ +import { expect } from 'chai' +import mockFs from 'mock-fs' +import { snapshotPluginsAstCode } from '../../../test-utils' +import { RollupTemplate } from './rollup' + +describe('rollup-file install template', () => { + afterEach(mockFs.restore) + + it('resolves rollup.config.js', () => { + mockFs({ + '/rollup.config.js': 'module.exports = { }', + }) + + const { success, payload } = RollupTemplate.test(process.cwd()) + + expect(success).to.equal(true) + expect(payload?.rollupConfigPath).to.equal('/rollup.config.js') + }) + + it('finds the closest package.json and tries to fetch rollup config path from scrips', () => { + mockFs({ + '/configs/rollup.js': 'module.exports = { }', + '/package.json': JSON.stringify({ + scripts: { + build: 'rollup --config configs/rollup.js', + }, + }), + }) + + const { success, payload } = RollupTemplate.test(process.cwd()) + + expect(success).to.equal(true) + expect(payload?.rollupConfigPath).to.equal('/configs/rollup.js') + }) + + it('looks for package.json in the upper folder', () => { + mockFs({ + '/i/am/in/some/deep/folder/withFile': 'test', + '/somewhere/configs/rollup.js': 'module.exports = { }', + '/package.json': JSON.stringify({ + scripts: { + build: 'rollup --config somewhere/configs/rollup.js', + }, + }), + }) + + const { success, payload } = RollupTemplate.test('i/am/in/some/deep/folder') + + expect(success).to.equal(true) + expect(payload?.rollupConfigPath).to.equal('/somewhere/configs/rollup.js') + }) + + it('returns success:false if cannot find rollup config', () => { + mockFs({ + '/b.js': '2', + '/a.js': '1', + }) + + const { success, payload } = RollupTemplate.test('/') + + expect(success).to.equal(false) + expect(payload).to.equal(undefined) + }) + + it('correctly generates plugins config when webpack config path is missing', () => { + snapshotPluginsAstCode(RollupTemplate) + }) + + it('correctly generates plugins config when webpack config path is provided', () => { + snapshotPluginsAstCode(RollupTemplate, { rollupConfigPath: '/config/rollup.config.js' }) + }) +}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/rollup.ts b/npm/create-cypress-tests/src/component-testing/templates/react/rollup.ts new file mode 100644 index 000000000000..b56b91dd74da --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/rollup.ts @@ -0,0 +1,122 @@ +import path from 'path' +import chalk from 'chalk' +import findUp from 'find-up' +import highlight from 'cli-highlight' +import { createFindPackageJsonIterator } from '../../../findPackageJson' +import { Template } from '../Template' +import * as babel from '@babel/core' + +export function extractRollupConfigPathFromScript (script: string) { + if (script.includes('rollup ')) { + const cliArgs = script.split(' ').map((part) => part.trim()) + const configArgIndex = cliArgs.findIndex( + (arg) => arg === '--config' || arg === '-c', + ) + + return configArgIndex === -1 ? null : cliArgs[configArgIndex + 1] + } + + return null +} + +export const RollupTemplate: Template<{ rollupConfigPath: string }> = { + message: + 'It looks like you have custom `rollup.config.js`. We can use it to bundle the components for testing.', + getExampleUrl: () => { + return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/rollup' + }, + recommendedComponentFolder: 'src', + getPluginsCodeAst: (payload, { cypressProjectRoot }) => { + const includeWarnComment = !payload + const rollupConfigPath = payload + ? path.relative(cypressProjectRoot, payload.rollupConfigPath) + : 'rollup.config.js' + + return { + Require: babel.template.ast('const rollupPreprocessor = require("@bahmutov/cy-rollup")'), + ModuleExportsBody: babel.template.ast([ + `on(`, + ` 'file:preprocessor',`, + ` rollupPreprocessor({`, + includeWarnComment + ? ' // TODO replace with valid rollup config path' + : '', + ` configFile: '${rollupConfigPath}',`, + ` }),`, + `)`, + ``, + `require('@cypress/code-coverage/task')(on, config)`, + `return config // IMPORTANT to return the config object`, + ].join('\n'), { preserveComments: true }), + } + }, + printHelper: () => { + console.log( + `Make sure that it is also required to add some additional configuration to the ${chalk.red( + 'rollup.config.js', + )}. Here is whats required:`, + ) + + const code = highlight( + [ + `import replace from '@rollup/plugin-replace'`, + `import commonjs from '@rollup/plugin-commonjs'`, + `import nodeResolve from '@rollup/plugin-node-resolve'`, + ``, + `export default [`, + ` {`, + ` plugins: [`, + ` nodeResolve(),`, + ` // process @cypress/react-code`, + ` commonjs(),`, + ` // required for react sources`, + ` replace({ 'process.env.NODE_ENV': JSON.stringify('development') }),`, + ` ]`, + ` }`, + `]`, + ].join('\n'), + { language: 'js' }, + ) + + console.log(`\n${code}\n`) + }, + test: (root) => { + const rollupConfigPath = findUp.sync('rollup.config.js', { cwd: root }) + + if (rollupConfigPath) { + return { + success: true, + payload: { rollupConfigPath }, + } + } + + const packageJsonIterator = createFindPackageJsonIterator(root) + + return packageJsonIterator.map(({ scripts }, packageJsonPath) => { + if (!scripts) { + return { success: false } + } + + for (const script of Object.values(scripts)) { + const rollupConfigRelativePath = extractRollupConfigPathFromScript( + script, + ) + + if (rollupConfigRelativePath) { + const directoryRoot = path.resolve(packageJsonPath, '..') + const rollupConfigPath = path.resolve( + directoryRoot, + rollupConfigRelativePath, + ) + + return { + success: true, + payload: { rollupConfigPath }, + } + } + } + + return { success: false } + }) + }, +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js new file mode 100644 index 000000000000..90a4d5b60a33 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js @@ -0,0 +1,29 @@ +const opts = webpackPreprocessor.defaultOptions +const babelLoader = opts.webpackOptions.module.rules[0].use[0] + +// add React preset to be able to transpile JSX +babelLoader.options.presets.push(require.resolve('@babel/preset-react')) + +// We can also push Babel istanbul plugin to instrument the code on the fly +// and get code coverage reports from component tests (optional) +if (!babelLoader.options.plugins) { + babelLoader.options.plugins = [] +} + +babelLoader.options.plugins.push(require.resolve('babel-plugin-istanbul')) + +// in order to mock named imports, need to include a plugin +babelLoader.options.plugins.push([ + require.resolve('@babel/plugin-transform-modules-commonjs'), + { + loose: true, + }, +]) + +// add code coverage plugin +require('@cypress/code-coverage/task')(on, config) + +on('file:preprocessor', webpackPreprocessor(opts)) + +// if adding code coverage, important to return updated config +return config diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts new file mode 100644 index 000000000000..7a4240d6fc33 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts @@ -0,0 +1,31 @@ +import fs from 'fs' +import path from 'path' +import * as babel from '@babel/core' +import chalk from 'chalk' +import { Template } from '../Template' + +export const WebpackOptions: Template = { + // this should never show ideally + message: `Unable to detect where webpack options are.`, + getExampleUrl: () => { + return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/webpack-options' + }, + test: () => ({ success: false }), + recommendedComponentFolder: 'src', + getPluginsCodeAst: () => { + return { + Require: babel.template.ast('const webpackPreprocessor = require("@cypress/webpack-preprocessor")'), + ModuleExportsBody: babel.template.ast( + fs.readFileSync(path.resolve(__dirname, 'webpack-options-module-exports.template.js'), { encoding: 'utf-8' }), + { preserveComments: true }, + ), + } + }, + printHelper: () => { + console.log( + `${chalk.inverse('Important:')} this configuration is using ${chalk.blue( + 'new webpack configuration ', + )}to bundle components. If you are using some framework (e.g. next) or bundling tool (e.g. rollup/parcel) consider using them to bundle component specs for cypress. \n`, + ) + }, +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts new file mode 100644 index 000000000000..c7b7e2e6f9a3 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts @@ -0,0 +1,6 @@ +import { WebpackOptions } from './webpack-options' +import { snapshotPluginsAstCode } from '../../../test-utils' + +describe('webpack-options template', () => { + it('correctly generates plugins config', () => snapshotPluginsAstCode(WebpackOptions)) +}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/templateUtils.ts b/npm/create-cypress-tests/src/component-testing/templates/templateUtils.ts new file mode 100644 index 000000000000..8b1f8e978f3e --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/templateUtils.ts @@ -0,0 +1,53 @@ +import findUp from 'find-up' +import path from 'path' +import { createFindPackageJsonIterator } from '../../findPackageJson' + +export function extractWebpackConfigPathFromScript (script: string) { + if (script.includes('webpack ') || script.includes('webpack-dev-server ')) { + const webpackCliArgs = script.split(' ').map((part) => part.trim()) + const configArgIndex = webpackCliArgs.findIndex((arg) => arg === '--config') + + return configArgIndex === -1 ? null : webpackCliArgs[configArgIndex + 1] + } + + return null +} + +export function findWebpackConfig (root: string) { + const webpackConfigPath = findUp.sync('webpack.config.js', { cwd: root }) + + if (webpackConfigPath) { + return webpackConfigPath + } + + const packageJsonIterator = createFindPackageJsonIterator(root) + + const { success, payload } = packageJsonIterator.map(({ scripts }, packageJsonPath) => { + if (!scripts) { + return { success: false } + } + + for (const script of Object.values(scripts)) { + const webpackConfigRelativePath = extractWebpackConfigPathFromScript( + script, + ) + + if (webpackConfigRelativePath) { + const directoryRoot = path.resolve(packageJsonPath, '..') + const webpackConfigPath = path.resolve( + directoryRoot, + webpackConfigRelativePath, + ) + + return { + success: true, + payload: { webpackConfigPath }, + } + } + } + + return { success: false } + }) + + return success ? payload?.webpackConfigPath : null +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/index.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/index.ts new file mode 100644 index 000000000000..512f4ff6ee8d --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/index.ts @@ -0,0 +1,8 @@ +import { Template } from '../Template' +import { VueCliTemplate } from './vueCli' +import { VueWebpackTemplate } from './vueWebpackFile' + +export const vueTemplates: Record> = { + webpack: VueWebpackTemplate, + 'vue-cli': VueCliTemplate, +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.test.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.test.ts new file mode 100644 index 000000000000..29bb76b76d2f --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.test.ts @@ -0,0 +1,44 @@ +import { expect } from 'chai' +import mockFs from 'mock-fs' +import { snapshotPluginsAstCode } from '../../../test-utils' +import { VueCliTemplate } from './vueCli' + +describe('vue webpack-file install template', () => { + beforeEach(mockFs.restore) + + it('resolves webpack.config.js', () => { + mockFs({ + '/package.json': JSON.stringify({ + 'devDependencies': { + '@vue/cli-plugin-babel': '~4.5.0', + '@vue/cli-plugin-eslint': '~4.5.0', + '@vue/cli-plugin-router': '~4.5.0', + '@vue/cli-service': '~4.5.0', + }, + }), + }) + + const { success } = VueCliTemplate.test('/') + + expect(success).to.equal(true) + }) + + it('returns success:false if vue-cli-service is not installed', () => { + mockFs({ + '/package.json': JSON.stringify({ + 'devDependencies': { + 'webpack': '*', + 'vue': '2.x', + }, + }), + }) + + const { success } = VueCliTemplate.test('/') + + expect(success).to.equal(false) + }) + + it('correctly generates plugins for vue-cli-service', () => { + snapshotPluginsAstCode(VueCliTemplate) + }) +}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.ts new file mode 100644 index 000000000000..16b117f3e772 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.ts @@ -0,0 +1,29 @@ +import * as babel from '@babel/core' +import { scanFSForAvailableDependency } from '../../../findPackageJson' +import { Template } from '../Template' + +export const VueCliTemplate: Template = { + message: + 'It looks like you are using vue-cli-service to run and build an application.', + getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/vue/examples/cli', + recommendedComponentFolder: 'src', + getPluginsCodeAst: () => { + return { + Require: babel.template.ast( + 'const preprocessor = require("@cypress/vue/dist/plugins/webpack");', + ), + ModuleExportsBody: babel.template.ast([ + 'preprocessor(on, config);', + '// IMPORTANT return the config object', + 'return config', + ].join('\n'), { preserveComments: true }), + } + }, + test: (root) => { + const hasVueCliService = scanFSForAvailableDependency(root, ['@vue/cli-service']) + + return { + success: hasVueCliService, + } + }, +} diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts new file mode 100644 index 000000000000..e0b058958ef2 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts @@ -0,0 +1,74 @@ +import { expect } from 'chai' +import mockFs from 'mock-fs' +import { snapshotPluginsAstCode } from '../../../test-utils' +import { VueWebpackTemplate } from './vueWebpackFile' + +describe('vue webpack-file install template', () => { + beforeEach(mockFs.restore) + + it('resolves webpack.config.js', () => { + mockFs({ + '/webpack.config.js': 'module.exports = { }', + }) + + const { success, payload } = VueWebpackTemplate.test(process.cwd()) + + expect(success).to.equal(true) + expect(payload?.webpackConfigPath).to.equal('/webpack.config.js') + }) + + it('finds the closest package.json and tries to fetch webpack config path from scrips', () => { + mockFs({ + '/configs/webpack.js': 'module.exports = { }', + '/package.json': JSON.stringify({ + scripts: { + build: 'webpack --config configs/webpack.js', + }, + }), + }) + + const { success, payload } = VueWebpackTemplate.test(process.cwd()) + + expect(success).to.equal(true) + expect(payload?.webpackConfigPath).to.equal('/configs/webpack.js') + }) + + it('looks for package.json in the upper folder', () => { + mockFs({ + '/some/deep/folder/withFile': 'test', + '/somewhere/configs/webpack.js': 'module.exports = { }', + '/package.json': JSON.stringify({ + scripts: { + build: 'webpack --config somewhere/configs/webpack.js', + }, + }), + }) + + const { success, payload } = VueWebpackTemplate.test( + '/some/deep/folder', + ) + + expect(success).to.equal(true) + expect(payload?.webpackConfigPath).to.equal('/somewhere/configs/webpack.js') + }) + + it('returns success:false if cannot find webpack config', () => { + mockFs({ + '/a.js': '1', + '/b.js': '2', + }) + + const { success, payload } = VueWebpackTemplate.test('/') + + expect(success).to.equal(false) + expect(payload).to.equal(undefined) + }) + + it('correctly generates plugins config when webpack config path is missing', () => { + snapshotPluginsAstCode(VueWebpackTemplate) + }) + + it('correctly generates plugins config when webpack config path is provided', () => { + snapshotPluginsAstCode(VueWebpackTemplate, { webpackConfigPath: '/build/webpack.config.js' }) + }) +}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts new file mode 100644 index 000000000000..1e49d1a9ddc8 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts @@ -0,0 +1,41 @@ +import * as babel from '@babel/core' +import path from 'path' +import { Template } from '../Template' +import { findWebpackConfig } from '../templateUtils' + +export const VueWebpackTemplate: Template<{ webpackConfigPath: string }> = { + message: + 'It looks like you have custom `webpack.config.js`. We can use it to bundle the components for testing.', + getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/vue/examples/cli', + recommendedComponentFolder: 'cypress/component', + getPluginsCodeAst: (payload, { cypressProjectRoot }) => { + const includeWarnComment = !payload + const webpackConfigPath = payload + ? path.relative(cypressProjectRoot, payload.webpackConfigPath) + : './webpack.config.js' + + return { + Require: babel.template.ast([ + 'const {', + ' onFilePreprocessor', + '} = require(\'@cypress/vue/dist/preprocessor/webpack\')', + ].join('\n')), + ModuleExportsBody: babel.template.ast([ + includeWarnComment + ? '// TODO replace with valid webpack config path' + : '', + `on('file:preprocessor', onFilePreprocessor('${webpackConfigPath}'))`, + ].join('\n'), { preserveComments: true }), + } + }, + test: (root) => { + const webpackConfigPath = findWebpackConfig(root) + + return webpackConfigPath ? { + success: true, + payload: { webpackConfigPath }, + } : { + success: false, + } + }, +} diff --git a/npm/create-cypress-tests/src/component-testing/versions.ts b/npm/create-cypress-tests/src/component-testing/versions.ts new file mode 100644 index 000000000000..ce3ac3ae04d5 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/versions.ts @@ -0,0 +1,4 @@ +export const MIN_SUPPORTED_VERSION = { + 'react-scripts': '^=3.x || ^=4.x', + next: '^=9.x', +} diff --git a/npm/create-cypress-tests/src/findPackageJson.ts b/npm/create-cypress-tests/src/findPackageJson.ts new file mode 100644 index 000000000000..f52fcdd52fa6 --- /dev/null +++ b/npm/create-cypress-tests/src/findPackageJson.ts @@ -0,0 +1,110 @@ +import path from 'path' +import fs from 'fs' +import findUp from 'find-up' + +type PackageJsonLike = { + name?: string + scripts?: Record + dependencies?: Record + devDependencies?: Record + [key: string]: unknown +} + +type FindPackageJsonResult = + | { + packageData: PackageJsonLike + filename: string + done: false + } + | { + packageData: undefined + filename: undefined + done: true + } + +/** + * Return the parsed package.json that we find in a parent folder. + * + * @returns {Object} Value, filename and indication if the iteration is done. + */ +export function createFindPackageJsonIterator (rootPath = process.cwd()) { + function scanForPackageJson (cwd: string): FindPackageJsonResult { + const packageJsonPath = findUp.sync('package.json', { cwd }) + + if (!packageJsonPath) { + return { + packageData: undefined, + filename: undefined, + done: true, + } + } + + const packageData = JSON.parse( + fs.readFileSync(packageJsonPath, { + encoding: 'utf-8', + }), + ) + + return { + packageData, + filename: packageJsonPath, + done: false, + } + } + + return { + map: ( + cb: ( + data: PackageJsonLike, + packageJsonPath: string, + ) => { success: boolean, payload?: TPayload }, + ) => { + let stepPathToScan = rootPath + + // eslint-disable-next-line + while (true) { + const result = scanForPackageJson(stepPathToScan) + + if (result.done) { + // didn't find the package.json + return { success: false } + } + + if (result.packageData) { + const cbResult = cb(result.packageData, result.filename) + + if (cbResult.success) { + return { success: true, payload: cbResult.payload } + } + } + + const nextStepPathToScan = path.resolve(stepPathToScan, '..') + + if (nextStepPathToScan === stepPathToScan) { + // we are at the root. Give up + return { success: false } + } + + stepPathToScan = nextStepPathToScan + } + }, + } +} + +export function scanFSForAvailableDependency (cwd: string, deps: string[]) { + const { success } = createFindPackageJsonIterator(cwd) + .map(({ dependencies, devDependencies }, path) => { + if (!dependencies && !devDependencies) { + return { success: false } + } + + return { + success: Object.keys({ ...dependencies, ...devDependencies }) + .some((dependency) => deps.includes(dependency)), + } + }) + + return success +} + +export type PackageJsonIterator = ReturnType diff --git a/npm/create-cypress-tests/src/index.ts b/npm/create-cypress-tests/src/index.ts new file mode 100644 index 000000000000..9e6e68e3f4ec --- /dev/null +++ b/npm/create-cypress-tests/src/index.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env node +import { program } from 'commander' +import { main } from './main' +import { version } from '../package.json' + +program +.option('--ignore-examples', 'Ignore generating example tests and fixtures by creating one ready-to-fill spec file') +.option('--use-npm', 'Use npm even if yarn is available') +.option('--ignore-ts', 'Ignore typescript if available') +.option('--component-tests', 'Run component testing installation without asking') + +program.version(version, '-v --version') +program.parse(process.argv) + +main({ + useNpm: program.useNpm, + ignoreTs: program.ignoreTs, + ignoreExamples: Boolean(program.ignoreExamples), + setupComponentTesting: program.componentTests, +}).catch(console.error) diff --git a/npm/create-cypress-tests/src/installCypress.ts b/npm/create-cypress-tests/src/installCypress.ts new file mode 100644 index 000000000000..d44171dd7305 --- /dev/null +++ b/npm/create-cypress-tests/src/installCypress.ts @@ -0,0 +1,71 @@ +import fs from 'fs-extra' +import findUp from 'find-up' +import path from 'path' +import example from '../initial-template' +import { installDependency } from './utils' +import chalk from 'chalk' +import ora from 'ora' + +type InstallCypressOpts = { + useYarn: boolean + useTypescript: boolean + ignoreExamples: boolean +} + +async function copyFiles ({ ignoreExamples, useTypescript }: InstallCypressOpts) { + let fileSpinner = ora('Creating config files').start() + + await fs.outputFile(path.resolve(process.cwd(), 'cypress.json'), '{}\n') + await fs.copy(example.getPathToPlugins(), path.resolve('cypress', 'plugins/index.js')) + const supportFiles: string[] = await example.getPathToSupportFiles() + + await Promise.all( + supportFiles.map((supportFilePath) => { + const newSupportFilePath = path.resolve('cypress', 'support', path.basename(supportFilePath)) + + return fs.copy(supportFilePath, newSupportFilePath) + }), + ) + + if (useTypescript) { + await fs.copy(example.getPathToTsConfig(), path.resolve('cypress', 'tsconfig.json')) + } + + // TODO think about better approach + if (ignoreExamples) { + const dummySpec = [ + 'describe("Spec", () => {', + '', + '})', + '', + ] + const specFileToCreate = path.resolve('cypress', 'integration', useTypescript ? 'spec.ts' : 'spec.js') + + await fs.outputFile(path.resolve('cypress', 'integration', useTypescript ? 'spec.js' : 'spec.ts'), dummySpec) + console.log(`In order to ignore examples a spec file ${chalk.green(path.relative(process.cwd(), specFileToCreate))}.`) + } + + fileSpinner.succeed() +} + +export async function findInstalledOrInstallCypress (options: InstallCypressOpts) { + let cypressJsonPath = await findUp('cypress.json') + + if (!cypressJsonPath) { + await installDependency('cypress', options) + await copyFiles(options) + + cypressJsonPath = await findUp('cypress.json') + } + + if (!cypressJsonPath) { + throw new Error('Unexpected error during cypress installation.') + } + + return { + cypressConfigPath: cypressJsonPath, + config: JSON.parse( + fs.readFileSync(cypressJsonPath, { encoding: 'utf-8' }).toString(), + ) as Record, + } +} diff --git a/npm/create-cypress-tests/src/main.test.ts b/npm/create-cypress-tests/src/main.test.ts new file mode 100644 index 000000000000..324c33f8db27 --- /dev/null +++ b/npm/create-cypress-tests/src/main.test.ts @@ -0,0 +1,160 @@ +import { expect, use } from 'chai' +import path from 'path' +import sinon, { SinonStub, SinonSpy, SinonSpyCallApi, restore } from 'sinon' +import mockFs from 'mock-fs' +import fsExtra from 'fs-extra' +import { main } from './main' +import sinonChai from 'sinon-chai' +import childProcess from 'child_process' + +use(sinonChai) + +function someOfSpyCallsIncludes (spy: any, logPart: string) { + return spy.getCalls().some( + (spy: SinonSpyCallApi) => { + return spy.args.some((callArg) => typeof callArg === 'string' && callArg.includes(logPart)) + }, + ) +} + +describe('create-cypress-tests', () => { + let promptSpy: SinonStub | null = null + let logSpy: SinonSpy | null = null + let errorSpy: SinonSpy | null = null + let execStub: SinonStub | null = null + let fsCopyStub: SinonStub | null = null + let processExitStub: SinonStub | null = null + + beforeEach(() => { + logSpy = sinon.spy(global.console, 'log') + errorSpy = sinon.spy(global.console, 'error') + // @ts-ignore + execStub = sinon.stub(childProcess, 'exec').callsFake((command, callback) => callback()) + // @ts-ignore + fsCopyStub = sinon.stub(fsExtra, 'copy').returns(Promise.resolve()) + processExitStub = sinon.stub(process, 'exit').callsFake(() => { + throw new Error('process.exit should not be called') + }) + }) + + afterEach(() => { + mockFs.restore() + logSpy?.restore() + promptSpy?.restore() + execStub?.restore() + fsCopyStub?.restore() + processExitStub?.restore() + execStub?.restore() + errorSpy?.restore() + }) + + it('Install cypress if no config found', async () => { + mockFs({ + '/package.json': JSON.stringify({ }), + }) + + await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) + + expect(execStub).calledWith('yarn add cypress --dev') + }) + + it('Uses npm if yarn is not available', async () => { + execStub + ?.onFirstCall().callsFake((command, callback) => callback('yarn is not available')) + ?.onSecondCall().callsFake((command, callback) => callback()) + ?.onThirdCall().callsFake((command, callback) => callback()) + + mockFs({ + '/package.json': JSON.stringify({ }), + }) + + await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) + expect(execStub).calledWith('npm install -D cypress') + }) + + it('Uses npm if --use-npm was provided', async () => { + mockFs({ + '/package.json': JSON.stringify({ }), + }) + + await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) + + expect(execStub).calledWith('npm install -D cypress') + }) + + it('Prints correct commands helper for npm', async () => { + mockFs({ + '/package.json': JSON.stringify({ }), + }) + + await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) + expect(someOfSpyCallsIncludes(logSpy, 'npx cypress open')).to.be.true + }) + + it('Prints correct commands helper for yarn', async () => { + mockFs({ + '/package.json': JSON.stringify({ }), + }) + + await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) + expect(someOfSpyCallsIncludes(logSpy, 'yarn cypress open')).to.be.true + }) + + it('Fails if git repository have untracked or uncommited files', async () => { + mockFs({ + '/package.json': JSON.stringify({ }), + }) + + execStub?.callsFake((_, callback) => callback(null, { stdout: 'test' })) + processExitStub?.callsFake(() => {}) + + await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) + + expect( + someOfSpyCallsIncludes(errorSpy, 'This repository has untracked files or uncommmited changes.'), + ).to.equal(true) + + expect(processExitStub).to.be.called + }) + + context('e2e fs tests', () => { + const e2eTestOutputPath = path.resolve(__dirname, 'test-output') + + beforeEach(async () => { + fsCopyStub?.restore() + mockFs.restore() + sinon.stub(process, 'cwd').returns(e2eTestOutputPath) + + await fsExtra.remove(e2eTestOutputPath) + await fsExtra.mkdir(e2eTestOutputPath) + }) + + it('Copies plugins and support files', async () => { + await fsExtra.outputFile( + path.join(e2eTestOutputPath, 'package.json'), + JSON.stringify({ name: 'test' }, null, 2), + ) + + await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) + + expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'))).to.equal(true) + expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'support', 'index.js'))).to.equal(true) + expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'support', 'commands.js'))).to.equal(true) + expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress.json'))).to.equal(true) + }) + + it('Copies tsconfig if typescript is installed', async () => { + await fsExtra.outputFile( + path.join(e2eTestOutputPath, 'package.json'), + JSON.stringify({ + name: 'test-typescript', + dependencies: { typescript: '^4.0.0' }, + }, null, 2), + ) + + await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) + await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'tsconfig.json')) + console.log(path.resolve(e2eTestOutputPath, 'cypress', 'tsconfig.json')) + }) + }) +}) diff --git a/npm/create-cypress-tests/src/main.ts b/npm/create-cypress-tests/src/main.ts new file mode 100644 index 000000000000..d5fedf74a0fe --- /dev/null +++ b/npm/create-cypress-tests/src/main.ts @@ -0,0 +1,98 @@ +import fs from 'fs' +import findUp from 'find-up' +import chalk from 'chalk' +import util from 'util' +import inquirer from 'inquirer' +import { initComponentTesting } from './component-testing/init-component-testing' +import { exec } from 'child_process' +import { scanFSForAvailableDependency } from './findPackageJson' +import { findInstalledOrInstallCypress } from './installCypress' + +type MainArgv = { + useNpm: boolean + ignoreTs: boolean + ignoreExamples: boolean + setupComponentTesting: boolean +} + +async function getGitStatus () { + const execAsync = util.promisify(exec) + + try { + let { stdout } = await execAsync(`git status --porcelain`) + + console.log(stdout) + + return stdout.trim() + } catch (e) { + return '' + } +} + +async function shouldUseYarn () { + const execAsync = util.promisify(exec) + + return execAsync('yarn --version') + .then(() => true) + .catch(() => false) +} + +function shouldUseTypescript () { + return scanFSForAvailableDependency(process.cwd(), ['typescript']) +} + +async function askForComponentTesting () { + const { shouldSetupComponentTesting } = await inquirer.prompt({ + type: 'confirm', + name: 'shouldSetupComponentTesting', + message: `Do you want to setup ${chalk.cyan('component testing')}? ${chalk.grey('You can do this later by rerunning this command')}.`, + }) + + return shouldSetupComponentTesting +} + +function printCypressCommandsHelper ({ useYarn }: { useYarn: boolean }) { + const displayedCommand = useYarn ? 'yarn' : 'npx' + + console.log('Inside this directory, you can run several commands:') + console.log() + console.log(chalk.cyan(` ${displayedCommand} cypress open`)) + console.log(' Opens cypress local development app.') + console.log() + console.log(chalk.cyan(` ${displayedCommand} cypress run`)) + console.log(' Runs tests in headless mode.') +} + +export async function main ({ useNpm, ignoreTs, setupComponentTesting, ignoreExamples }: MainArgv) { + const rootPackageJsonPath = await findUp('package.json') + const useYarn = useNpm === true ? false : await shouldUseYarn() + const useTypescript = ignoreTs ? false : shouldUseTypescript() + + if (!rootPackageJsonPath) { + console.log(`${chalk.bold.red(`It looks like you are running cypress installation wizard outside of npm module.`)}\nIf you would like to setup a new project for cypress tests please run the ${chalk.inverse(useNpm ? ' npm init ' : ' yarn init ')} first.`) + process.exit(1) + } + + const { name = 'unknown', version = '0.0.0' } = JSON.parse(fs.readFileSync(rootPackageJsonPath).toString()) + + console.log(`Running ${chalk.green('cypress ๐ŸŒฒ')} installation wizard for ${chalk.cyan(`${name}@${version}`)}`) + + const gitStatus = await getGitStatus() + + if (gitStatus) { + console.error(`\n${chalk.bold.red('This repository has untracked files or uncommmited changes.')}\nThis command will ${chalk.cyan('make changes in the codebase')}, so please remove untracked files, stash or commit any changes, and try again.`) + process.exit(1) + } + + const { config, cypressConfigPath } = await findInstalledOrInstallCypress({ useYarn, useTypescript, ignoreExamples }) + const shouldSetupComponentTesting = setupComponentTesting ?? await askForComponentTesting() + + if (shouldSetupComponentTesting) { + await initComponentTesting({ config, cypressConfigPath, useYarn }) + } + + console.log(`\n๐Ÿ‘ Success! Cypress is installed and ready to run tests.`) + printCypressCommandsHelper({ useYarn }) + + console.log(`\nHappy testing with ${chalk.green('cypress.io')} ๐ŸŒฒ\n`) +} diff --git a/npm/create-cypress-tests/src/test-utils.ts b/npm/create-cypress-tests/src/test-utils.ts new file mode 100644 index 000000000000..6021690678b9 --- /dev/null +++ b/npm/create-cypress-tests/src/test-utils.ts @@ -0,0 +1,34 @@ +import * as babel from '@babel/core' +import snapshot from 'snap-shot-it' +import mockFs from 'mock-fs' +import { SinonSpyCallApi } from 'sinon' +import { createTransformPluginsFileBabelPlugin } from './component-testing/babel/babelTransform' +import { Template } from './component-testing/templates/Template' + +export function someOfSpyCallsIncludes (spy: any, logPart: string) { + return spy.getCalls().some( + (spy: SinonSpyCallApi) => { + return spy.args.some((callArg) => typeof callArg === 'string' && callArg.includes(logPart)) + }, + ) +} + +export function snapshotPluginsAstCode (template: Template, payload?: T) { + mockFs.restore() + const code = [ + 'const something = require("something")', + 'module.exports = (on) => {', + '};', + ].join('\n') + + const babelPlugin = createTransformPluginsFileBabelPlugin(template.getPluginsCodeAst(payload ?? null, { cypressProjectRoot: '/' })) + const output = babel.transformSync(code, { + plugins: [babelPlugin], + }) + + if (!output || !output.code) { + throw new Error('Babel transform output is empty.') + } + + snapshot(output.code) +} diff --git a/npm/create-cypress-tests/src/utils.ts b/npm/create-cypress-tests/src/utils.ts new file mode 100644 index 000000000000..c42a92855e93 --- /dev/null +++ b/npm/create-cypress-tests/src/utils.ts @@ -0,0 +1,62 @@ +import semver from 'semver' +import chalk from 'chalk' +import ora from 'ora' +import util from 'util' +import { exec } from 'child_process' + +/** + * Compare available version range with the provided version from package.json + * @param packageName Package name used to display a helper message to user. + */ +export function validateSemverVersion ( + version: string, + allowedVersionRange: string, + packageName?: string, +) { + let isValid: boolean + + try { + const minAvailableVersion = semver.minVersion(version)?.raw + + isValid = Boolean( + minAvailableVersion && + semver.satisfies(minAvailableVersion, allowedVersionRange), + ) + } catch (e) { + // handle not semver versions like "latest", "git:" or "file:" + isValid = false + } + + if (!isValid && packageName) { + const packageNameSymbol = chalk.green(packageName) + + console.warn( + `It seems like you are using ${packageNameSymbol} with version ${chalk.bold( + version, + )}, however we support only ${packageNameSymbol} projects with version ${chalk.bold( + allowedVersionRange, + )}. \n`, + ) + } + + return isValid +} + +export async function installDependency (name: string, options: { useYarn: boolean}) { + const commandToRun = options.useYarn ? `yarn add ${name} --dev` : `npm install -D ${name}` + let cliSpinner = ora(`Installing ${name} ${chalk.gray(`(${commandToRun})`)}`).start() + + try { + // do this inside function for test stubbing + const execAsync = util.promisify(exec) + + await execAsync(commandToRun) + } catch (e) { + cliSpinner.fail(`Can not install ${name} using ${chalk.inverse(commandToRun)})}`) + console.log(e) + + process.exit(1) + } + + cliSpinner.succeed() +} diff --git a/npm/create-cypress-tests/tsconfig.json b/npm/create-cypress-tests/tsconfig.json new file mode 100644 index 000000000000..ea3235694af0 --- /dev/null +++ b/npm/create-cypress-tests/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "esModuleInterop": true, + "allowJs": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "moduleResolution": "node", + "strict": true, + "strictNullChecks": true, + "resolveJsonModule": true, + "module": "CommonJS", + "target": "ES2018", + "types": [ + "node", + ], + "lib": [ + "ES2018" + ], + "noImplicitAny": true + }, + "exclude": [ + "./src/**/*.test.ts", + "node_modules" + ], + "include": [ + "./src/**/*.ts", + "./initial-template/**/*.{js}" + ] +} diff --git a/npm/create-cypress-tests/tsconfig.test.json b/npm/create-cypress-tests/tsconfig.test.json new file mode 100644 index 000000000000..df01205d9a40 --- /dev/null +++ b/npm/create-cypress-tests/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "types": [ + "mocha" + ] + }, + "include": [ + "./src/**/*.test.ts" + ] +} \ No newline at end of file diff --git a/npm/react/circle.yml b/npm/react/circle.yml deleted file mode 100644 index 7c3c683094ef..000000000000 --- a/npm/react/circle.yml +++ /dev/null @@ -1,371 +0,0 @@ -version: 2.1 -orbs: - cypress: cypress-io/cypress@1.26.0 - -workflows: - build: - jobs: - # install and cache dependencies in this job - # AND build the library once - # then the workspace will be passed to other jobs - - cypress/install: - name: Install - executor: cypress/base-12 - build: npm run transpile - post-steps: - - run: - name: Show info ๐Ÿ“บ - command: npx cypress info - - run: - name: Linting code ๐Ÿงน - command: npm run lint - - run: - name: Stop exclusive tests 1๏ธโƒฃ - command: npm run stop-only - - run: - name: Build folder ๐Ÿ— - command: npm run build - - run: - name: Run unit tests ๐Ÿ‘ท - command: npm run test:unit - - - cypress/run: - name: Example A11y - requires: - - Install - executor: cypress/base-12 - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/a11y - command: npm test - store_artifacts: true - - - cypress/run: - name: Example Babel - requires: - - Install - executor: cypress/base-12 - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: false - working_directory: examples/using-babel - command: npm test - store_artifacts: true - - - cypress/run: - name: Example Babel + Typescript - requires: - - Install - executor: cypress/base-12 - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/using-babel-typescript - command: npm test - store_artifacts: true - - - cypress/run: - name: Example React Scripts - requires: - - Install - executor: cypress/base-12 - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/react-scripts - command: npm test - store_artifacts: true - post-steps: - - run: - name: Check coverage ๐Ÿ“ˆ - command: | - npm run check-coverage - npm run only-covered - working_directory: examples/react-scripts - - - cypress/run: - name: Example Next.js - requires: - - Install - executor: cypress/base-12 - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/nextjs - command: npm test - store_artifacts: true - post-steps: - - run: - name: Check coverage ๐Ÿ“ˆ - command: | - npm run check-coverage - npm run only-covered - working_directory: examples/nextjs - - - cypress/run: - # react-scripts example with component tests not in "src" folder - # but in "cypress/component" folder - name: Example Component Folder - executor: cypress/base-12 - requires: - - Install - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/react-scripts-folder - command: npm test - store_artifacts: true - post-steps: - - run: - name: Check coverage ๐Ÿ“ˆ - command: | - npm run check-coverage - npm run only-covered - working_directory: examples/react-scripts-folder - - - cypress/run: - name: Example Tailwind - requires: - - Install - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - executor: cypress/base-12 - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/tailwind - command: | - DEBUG=cypress-react-unit-test,find-webpack npm test - store_artifacts: true - post-steps: - - run: - name: Check coverage ๐Ÿ“ˆ - command: | - ls -la - npm run check-coverage - npm run only-covered - working_directory: examples/tailwind - - - cypress/run: - name: Example Webpack file - requires: - - Install - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - executor: cypress/base-12 - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/webpack-file - command: npm test - store_artifacts: true - post-steps: - - run: - name: Check coverage ๐Ÿ“ˆ - command: | - npm run check-coverage - npm run only-covered - working_directory: examples/webpack-file - - - cypress/run: - name: Example Rollup - requires: - - Install - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - executor: cypress/base-12 - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/rollup - command: npm test - store_artifacts: true - - - cypress/run: - name: Example Webpack options - requires: - - Install - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - executor: cypress/base-12 - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/webpack-options - command: npm test - store_artifacts: true - post-steps: - - run: - name: Check coverage ๐Ÿ“ˆ - command: | - npm run check-coverage - npm run only-covered - working_directory: examples/webpack-options - -# - cypress/run: -# name: Example Sass -# requires: -# - Install -# # we need the same OS version as in install job -# # because we will use native Sass dependency -# executor: cypress/base-12 -# # each example installs "cypress-react-unit-test" as a local dependency (symlink) -# install-command: npm install --no-bin-links -# verify-command: echo 'Already verified' -# no-workspace: true -# working_directory: examples/sass-and-ts -# command: npm test -# store_artifacts: true -# post-steps: -# - run: -# name: Check coverage ๐Ÿ“ˆ -# command: | -# npm run check-coverage -# npm run only-covered -# working_directory: examples/sass-and-ts - - - cypress/run: - name: Example Snapshots - requires: - - Install - executor: cypress/base-12 - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/snapshots - command: npm test - store_artifacts: true - - - cypress/run: - name: Visual Sudoku - executor: cypress/base-12 - requires: - - Install - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/visual-sudoku - command: npm test - store_artifacts: true - post-steps: - - store_artifacts: - path: examples/visual-sudoku/cypress/snapshots - - - cypress/run: - name: Visual with Applitools - executor: cypress/base-12 - requires: - - Install - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/visual-testing-with-applitools - # to correctly run this job, we need Applitools token - # external pull requests do not have environment variables set - # thus the job will always fail. Let's skip this job if the - # environment variable is missing - command: | - if [ -z "$APPLITOOLS_API_KEY" ]; then - echo "Skipping Applitools test job, missing environment variable APPLITOOLS_API_KEY" - else - npm test - fi - store_artifacts: true - - - cypress/run: - name: Visual with Percy - executor: cypress/base-12 - requires: - - Install - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/visual-testing-with-percy - # run Percy agent and then run Cypress component tests - # https://docs.percy.io/docs/cypress - command: npx percy exec -- npm test - store_artifacts: true - - - cypress/run: - name: Visual with Happo - executor: cypress/base-12 - requires: - - Install - # each example installs "cypress-react-unit-test" as a local dependency (symlink) - install-command: npm install --no-bin-links - verify-command: echo 'Already verified' - no-workspace: true - working_directory: examples/visual-testing-with-happo - command: npm run test:happo - store_artifacts: true - - - cypress/run: - name: Component Tests - executor: cypress/base-12 - parallelism: 4 - requires: - - Install - # notice a trick to avoid re-installing dependencies - # in this job - a do-nothing "install-command" parameter - install-command: echo 'Nothing to install in this job' - # we are not going to use results from this job anywhere else - no-workspace: true - record: false - store_artifacts: true - # following examples from - # https://circleci.com/docs/2.0/parallelism-faster-jobs/ - # TODO probably only run component tests and move integration sanity checks into own job - command: | - TESTFILES=$(circleci tests glob "cypress/{component,integration}/**/*spec.{js,jsx,ts,tsx}" | circleci tests split --total=4) - echo "Test files for this machine are $TESTFILES" - npx cypress run --spec $TESTFILES - - # this job attaches the workspace left by the install job - # so it is ready to run Cypress tests - # only we will run semantic release script instead - - cypress/run: - name: NPM release - # only run NPM release from specific branch(es) - filters: - branches: - only: - - main - # we need newer Node for semantic release - executor: cypress/base-12 - requires: - - Install - - Component Tests - - Example A11y - - Example Babel - - Example Component Folder - - Example React Scripts - # - Example Sass - - Example Snapshots - - Example Tailwind - - Example Webpack file - - Example Webpack options - - Example Rollup - - Visual Sudoku - - Visual with Percy - - Visual with Happo - - Visual with Applitools - install-command: echo 'Already installed' - verify-command: echo 'Already verified' - no-workspace: true - # instead of "cypress run" do NPM release ๐Ÿ˜ - # clear environment variables to trick semantic-release - # into thinking this is NOT a pull request. - # (under the hood the module env-ci is used to check if this is a PR) - command: | - npm run build - CIRCLE_PR_NUMBER= \ - CIRCLE_PULL_REQUEST= \ - CI_PULL_REQUEST= \ - npm run semantic-release diff --git a/npm/react/package.json b/npm/react/package.json index 9db22c9803f6..2d4f862e90c7 100644 --- a/npm/react/package.json +++ b/npm/react/package.json @@ -43,9 +43,6 @@ "@percy/cypress": "2.3.2", "@testing-library/cypress": "7.0.1", "@types/chalk": "2.2.0", - "@types/inquirer": "7.3.1", - "@types/mock-fs": "4.10.0", - "@types/node": "9.6.49", "@types/semver": "7.3.4", "arg": "4.1.3", "autoprefixer": "9.7.6", @@ -67,8 +64,6 @@ "lodash": "4.17.15", "mobx": "6.0.0", "mobx-react-lite": "3.0.0", - "mocha": "7.1.1", - "mock-fs": "4.13.0", "next": "^9.5.3", "pretty": "2.0.0", "prop-types": "15.7.2", @@ -100,7 +95,7 @@ "@types/react": "^16.9.16", "babel-loader": "^=8.x", "cypress": "*", - "next": "^=8.x", + "next": "^=9.x", "react": "^=16.x", "react-dom": "^=16.x", "webpack": "^=3.x" diff --git a/npm/vue/src/preprocessor/webpack.js b/npm/vue/src/preprocessor/webpack.js index 6a28bc6ec663..4874533f0936 100644 --- a/npm/vue/src/preprocessor/webpack.js +++ b/npm/vue/src/preprocessor/webpack.js @@ -60,7 +60,6 @@ function compileTemplate (options = {}) { /** * Warning: modifies the input object -<<<<<<< HEAD * @param {WebpackOptions} options */ @@ -76,8 +75,6 @@ function removeForkTsCheckerWebpackPlugin (options) { /** * Warning: modifies the input object -======= ->>>>>>> origin * @param {Cypress.ConfigOptions} config * @param {WebpackOptions} options */ diff --git a/packages/example/README.md b/packages/example/README.md index 8a36c6cd4150..a9f94349e587 100644 --- a/packages/example/README.md +++ b/packages/example/README.md @@ -1,4 +1,9 @@ -## Example + +## Scaffold config files + +The `cypress/plugins/index.js`, `cypress/support/*` and `cypress/tsconfig.json` from this package are used for user scaffolding in `packages/server` and `npm/create-cypress-tests`. This configuration files are by default injected when user instals Cypress. + +## Examples This repo contains the source code for pushing out [https://example.cypress.io](https://example.cypress.io). diff --git a/packages/example/bin/build.js b/packages/example/bin/build.js index e46b80331f1e..85454c6c3ea5 100644 --- a/packages/example/bin/build.js +++ b/packages/example/bin/build.js @@ -11,8 +11,8 @@ shell.rm('-rf', 'app') shell.mkdir('app') shell.cp('-r', join(resolvePkg('cypress-example-kitchensink'), 'app'), '.') -shell.rm('-rf', 'cypress') +shell.rm('-rf', 'cypress/integration') -shell.cp('-r', join(resolvePkg('cypress-example-kitchensink'), 'cypress'), '.') +shell.cp('-r', join(resolvePkg('cypress-example-kitchensink'), 'cypress', 'integration'), 'cypress/integration') shell.exec('node ./bin/convert.js') diff --git a/packages/server/lib/scaffold/fixtures/example.json b/packages/example/cypress/fixtures/example.json similarity index 98% rename from packages/server/lib/scaffold/fixtures/example.json rename to packages/example/cypress/fixtures/example.json index da18d9352a17..02e4254378e9 100644 --- a/packages/server/lib/scaffold/fixtures/example.json +++ b/packages/example/cypress/fixtures/example.json @@ -2,4 +2,4 @@ "name": "Using fixtures to represent data", "email": "hello@cypress.io", "body": "Fixtures are a great way to mock data for responses to routes" -} \ No newline at end of file +} diff --git a/packages/server/lib/scaffold/plugins/index.js b/packages/example/cypress/plugins/index.js similarity index 100% rename from packages/server/lib/scaffold/plugins/index.js rename to packages/example/cypress/plugins/index.js diff --git a/packages/server/lib/scaffold/support/commands.js b/packages/example/cypress/support/commands.js similarity index 84% rename from packages/server/lib/scaffold/support/commands.js rename to packages/example/cypress/support/commands.js index ca4d256f3eb1..b39d4ca4954d 100644 --- a/packages/server/lib/scaffold/support/commands.js +++ b/packages/example/cypress/support/commands.js @@ -14,11 +14,11 @@ // // // -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// Cypress.Commands.add("drag", { prevSubject: "element"}, (subject, options) => { ... }) // // // -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// Cypress.Commands.add("dismiss", { prevSubject: "optional"}, (subject, options) => { ... }) // // // -- This will overwrite an existing command -- diff --git a/packages/server/lib/scaffold/support/index.js b/packages/example/cypress/support/index.js similarity index 100% rename from packages/server/lib/scaffold/support/index.js rename to packages/example/cypress/support/index.js diff --git a/packages/example/cypress/tsconfig.json b/packages/example/cypress/tsconfig.json new file mode 100644 index 000000000000..4109e0ec1aff --- /dev/null +++ b/packages/example/cypress/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress"] + }, + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/example/index.d.ts b/packages/example/index.d.ts new file mode 100644 index 000000000000..6319bc5ceb03 --- /dev/null +++ b/packages/example/index.d.ts @@ -0,0 +1 @@ +export { default } from './lib/example' \ No newline at end of file diff --git a/packages/example/lib/example.d.ts b/packages/example/lib/example.d.ts new file mode 100644 index 000000000000..35ea4dbef283 --- /dev/null +++ b/packages/example/lib/example.d.ts @@ -0,0 +1,10 @@ +declare const example: { + getPathToExamples(): Promise; + getFolderName(): string; + getPathToPlugins(): string; + getPathToSupportFiles(): Promise; + getPathToTsConfig(): string; + getPathToFixture(): string; +} + +export default example; \ No newline at end of file diff --git a/packages/example/lib/example.js b/packages/example/lib/example.js index 92fb5e0422b4..e5e95a541c1f 100644 --- a/packages/example/lib/example.js +++ b/packages/example/lib/example.js @@ -16,8 +16,33 @@ module.exports = { ) ) }, - + getFolderName () { return 'examples' }, + + getPathToPlugins() { + return path.resolve(__dirname, '..', 'cypress', 'plugins', 'index.js') + }, + + getPathToSupportFiles() { + return glob( + path.join( + __dirname, + '..', + 'cypress', + 'support', + '**', + '*' + ) + ) + }, + + getPathToTsConfig() { + return path.resolve(__dirname, '..', 'cypress', 'tsconfig.json') + }, + + getPathToFixture() { + return path.resolve(__dirname, '..', 'cypress', 'fixtures', 'example.json') + } } diff --git a/packages/example/package.json b/packages/example/package.json index c8da7bf9aea4..f3dd921b24d9 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "private": true, "main": "index.js", + "types": "index.d.ts", "scripts": { "postinstall": "echo '@packages/example needs: yarn build'", "clean-deps": "rm -rf node_modules", diff --git a/packages/server/__snapshots__/scaffold_spec.js b/packages/server/__snapshots__/scaffold_spec.js index 9aa72059d23e..371a2fb1e2f6 100644 --- a/packages/server/__snapshots__/scaffold_spec.js +++ b/packages/server/__snapshots__/scaffold_spec.js @@ -342,11 +342,11 @@ exports['lib/scaffold .support creates supportFolder and commands.js and index.j // // // -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// Cypress.Commands.add("drag", { prevSubject: "element"}, (subject, options) => { ... }) // // // -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// Cypress.Commands.add("dismiss", { prevSubject: "optional"}, (subject, options) => { ... }) // // // -- This will overwrite an existing command -- diff --git a/packages/server/lib/scaffold.js b/packages/server/lib/scaffold.js index 8bbfbbbf8d2c..657ed7f56bad 100644 --- a/packages/server/lib/scaffold.js +++ b/packages/server/lib/scaffold.js @@ -157,7 +157,7 @@ module.exports = { return this.verifyScaffolding(folder, () => { debug(`copying example.json into ${folder}`) - return this._copy('fixtures/example.json', folder, config) + return this._copy(cypressEx.getPathToFixture(), folder, config) }) }, @@ -172,10 +172,14 @@ module.exports = { return this.verifyScaffolding(folder, () => { debug(`copying commands.js and index.js to ${folder}`) - return Promise.join( - this._copy('support/commands.js', folder, config), - this._copy('support/index.js', folder, config), - ) + return cypressEx.getPathToSupportFiles() + .then((supportFiles) => { + return Promise.all( + supportFiles.map((supportFilePath) => { + return this._copy(supportFilePath, folder, config) + }), + ) + }) }) }, @@ -190,7 +194,7 @@ module.exports = { return this.verifyScaffolding(folder, () => { debug(`copying index.js into ${folder}`) - return this._copy('plugins/index.js', folder, config) + return this._copy(cypressEx.getPathToPlugins(), folder, config) }) }, diff --git a/packages/server/test/unit/scaffold_spec.js b/packages/server/test/unit/scaffold_spec.js index ed5083d819a2..12b9c8bdda03 100644 --- a/packages/server/test/unit/scaffold_spec.js +++ b/packages/server/test/unit/scaffold_spec.js @@ -342,7 +342,7 @@ describe('lib/scaffold', () => { "name": "Using fixtures to represent data", "email": "hello@cypress.io", "body": "Fixtures are a great way to mock data for responses to routes" -}\ +} `) }) }) diff --git a/yarn.lock b/yarn.lock index fd2f98436c23..cff4cc8b4c1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -213,6 +213,28 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@^7.5.4": + version "7.12.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.3.tgz#1b436884e1e3bff6fb1328dc02b208759de92ad8" + integrity sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.1" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helpers" "^7.12.1" + "@babel/parser" "^7.12.3" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/generator@^7.11.5", "@babel/generator@^7.11.6", "@babel/generator@^7.4.0", "@babel/generator@^7.4.4", "@babel/generator@^7.6.0", "@babel/generator@^7.9.0", "@babel/generator@^7.9.3": version "7.11.6" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" @@ -222,6 +244,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.1.tgz#0d70be32bdaa03d7c51c8597dda76e0df1f15468" + integrity sha512-DB+6rafIdc9o72Yc3/Ph5h+6hUjeOp66pF0naQBgUFFuPqzQwIlPTm3xZR7YNvduIMtkDIj2t21LSQwnbCrXvg== + dependencies: + "@babel/types" "^7.12.1" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/generator@^7.12.5", "@babel/generator@^7.7.7": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de" @@ -286,6 +317,17 @@ "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" +"@babel/helper-create-class-features-plugin@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e" + integrity sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.12.1" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/helper-create-regexp-features-plugin@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" @@ -342,6 +384,13 @@ dependencies: "@babel/types" "^7.11.0" +"@babel/helper-member-expression-to-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz#fba0f2fcff3fba00e6ecb664bb5e6e26e2d6165c" + integrity sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ== + dependencies: + "@babel/types" "^7.12.1" + "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" @@ -349,6 +398,13 @@ dependencies: "@babel/types" "^7.10.4" +"@babel/helper-module-imports@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.1.tgz#1644c01591a15a2f084dd6d092d9430eb1d1216c" + integrity sha512-ZeC1TlMSvikvJNy1v/wPIazCu3NdOwgYZLIkmIyAsGhqkNpiDoQQRmaCK8YP4Pq3GPTLPV9WXaPCJKvx06JxKA== + dependencies: + "@babel/types" "^7.12.1" + "@babel/helper-module-transforms@^7.10.1", "@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.11.0", "@babel/helper-module-transforms@^7.9.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" @@ -362,6 +418,21 @@ "@babel/types" "^7.11.0" lodash "^4.17.19" +"@babel/helper-module-transforms@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" + integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w== + dependencies: + "@babel/helper-module-imports" "^7.12.1" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-simple-access" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/helper-validator-identifier" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + lodash "^4.17.19" + "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" @@ -402,6 +473,16 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-replace-supers@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.1.tgz#f15c9cc897439281891e11d5ce12562ac0cf3fa9" + integrity sha512-zJjTvtNJnCFsCXVi5rUInstLd/EIVNmIKA1Q9ynESmMBWPWd+7sdR+G4/wdu+Mppfep0XLyG2m7EBPvjCeFyrw== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.12.1" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + "@babel/helper-simple-access@^7.10.1", "@babel/helper-simple-access@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" @@ -410,6 +491,13 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-simple-access@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136" + integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA== + dependencies: + "@babel/types" "^7.12.1" + "@babel/helper-skip-transparent-expression-wrappers@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" @@ -448,6 +536,15 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helpers@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.1.tgz#8a8261c1d438ec18cb890434df4ec768734c1e79" + integrity sha512-9JoDSBGoWtmbay98efmT2+mySkwjzeFeAL9BuWNoVQpkPFQF8SIIFUfY5os9u8wVzglzoiPRSW7cuJmBDUt43g== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + "@babel/helpers@^7.7.4": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e" @@ -471,6 +568,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== +"@babel/parser@^7.12.1", "@babel/parser@^7.12.3": + version "7.12.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.3.tgz#a305415ebe7a6c7023b40b5122a0662d928334cd" + integrity sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw== + "@babel/parser@^7.12.7", "@babel/parser@^7.7.7": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.7.tgz#fee7b39fe809d0e73e5b25eecaf5780ef3d73056" @@ -775,6 +877,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-typescript@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.1.tgz#460ba9d77077653803c3dd2e673f76d66b4029e5" + integrity sha512-UZNEcCY+4Dp9yYRCAHrHDU+9ZXLYaY9MgBXSRLkB9WjYFRR6quJBumfVrEkUxrePPBwFcpWfNKXqVRQQtm7mMA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-arrow-functions@^7.10.4", "@babel/plugin-transform-arrow-functions@^7.2.0", "@babel/plugin-transform-arrow-functions@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" @@ -1141,6 +1250,15 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-typescript" "^7.10.4" +"@babel/plugin-transform-typescript@^7.2.0": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz#d92cc0af504d510e26a754a7dbc2e5c8cd9c7ab4" + integrity sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript" "^7.12.1" + "@babel/plugin-transform-unicode-escapes@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007" @@ -1550,7 +1668,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.10.4", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0", "@babel/template@^7.8.6": +"@babel/template@^7.10.4", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.5.4", "@babel/template@^7.6.0", "@babel/template@^7.8.6": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== @@ -1583,10 +1701,25 @@ globals "^11.1.0" lodash "^4.17.19" +"@babel/traverse@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.1.tgz#941395e0c5cc86d5d3e75caa095d3924526f0c1e" + integrity sha512-MA3WPoRt1ZHo2ZmoGKNqi20YnPt0B1S0GTZEPhhd+hw2KGUzBlHuVunj6K4sNuK+reEvyiPwtp0cpaqLzJDmAw== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.1" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.12.1" + "@babel/types" "^7.12.1" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/traverse@^7.12.5", "@babel/traverse@^7.7.4": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.7.tgz#572a722408681cef17d6b0bef69ef2e728ca69f1" - integrity sha512-nMWaqsQEeSvMNypswUDzjqQ+0rR6pqCtoQpsqGJC4/Khm9cISwPTSpai57F6/jDaOoEGz8yE/WxcO3PV6tKSmQ== + version "7.12.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.8.tgz#c1c2983bf9ba0f4f0eaa11dff7e77fa63307b2a4" + integrity sha512-EIRQXPTwFEGRZyu6gXbjfpNORN1oZvwuzJbxcXjAgWV0iqXYDszN1Hx3FVm6YgZfu1ZQbCVAk3l+nIw95Xll9Q== dependencies: "@babel/code-frame" "^7.10.4" "@babel/generator" "^7.12.5" @@ -1616,6 +1749,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.12.1", "@babel/types@^7.5.0": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.1.tgz#e109d9ab99a8de735be287ee3d6a9947a190c4ae" + integrity sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.7.4": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.7.tgz#6039ff1e242640a29452c9ae572162ec9a8f5d13" @@ -5006,6 +5148,17 @@ "@types/babel__template" "*" "@types/babel__traverse" "*" +"@types/babel__core@^7.1.2": + version "7.1.11" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.11.tgz#7fae4660a009a4031e293f25b213f142d823b3c4" + integrity sha512-E5nSOzrjnvhURYnbOR2dClTqcyhPbPvtEwLHf7JJADKedPbcZsoJVfP+I2vBNfBjz4bnZIuhL/tNmRi5nJ7Jlw== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + "@types/babel__generator@*": version "7.6.1" resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.1.tgz#4901767b397e8711aeb99df8d396d7ba7b7f0e04" @@ -5469,6 +5622,13 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/ora@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@types/ora/-/ora-3.2.0.tgz#b2f65d1283a8f36d8b0f9ee767e0732a2f429362" + integrity sha512-jll99xUKpiFbIFZSQcxm4numfsLaOWBzWNaRk3PvTSE7BPqTzzOCFmS0mQ7m8qkTfmYhuYbehTGsxkvRLPC++w== + dependencies: + ora "*" + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -8691,7 +8851,7 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^4.0.1, bl@^4.0.3: +bl@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== @@ -9771,9 +9931,9 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30000989, can integrity sha512-EHfInJHoQTmlMdVZrEc5gmwPc0zyN/hVufmGHPbVNQwlk7tJfCmQ2ysRZMY2MeleBivALUTyyxXnQjK18XrVpA== caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001113: - version "1.0.30001159" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001159.tgz#bebde28f893fa9594dadcaa7d6b8e2aa0299df20" - integrity sha512-w9Ph56jOsS8RL20K9cLND3u/+5WASWdhC/PPrf+V3/HsM3uHOavWOR1Xzakbv4Puo/srmPHudkmCRWM7Aq+/UA== + version "1.0.30001161" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001161.tgz#64f7ffe79ee780b8c92843ff34feb36cea4651e0" + integrity sha512-JharrCDxOqPLBULF9/SPa6yMcBRTjZARJ6sc3cuKrPfyIk64JN6kuMINWqA99Xc8uElMFcROliwtz0n9pYej+g== capture-exit@^2.0.0: version "2.0.0" @@ -9888,7 +10048,7 @@ chai@4.2.0: pathval "^1.1.0" type-detect "^4.0.5" -chalk@*, chalk@^4.0.0, chalk@^4.1.0: +chalk@*, chalk@4.1.0, chalk@^4.0.0, chalk@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== @@ -10358,6 +10518,23 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-highlight@2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.4.tgz#098cb642cf17f42adc1c1145e07f960ec4d7522b" + integrity sha512-s7Zofobm20qriqDoU9sXptQx0t2R9PEgac92mENNm7xaEe1hn71IIMsXMK+6encA6WRCWWxIGQbipr3q998tlQ== + dependencies: + chalk "^3.0.0" + highlight.js "^9.6.0" + mz "^2.4.0" + parse5 "^5.1.1" + parse5-htmlparser2-tree-adapter "^5.1.1" + yargs "^15.0.0" + +cli-spinners@^2.4.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047" + integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ== + cli-table3@0.5.1, cli-table3@^0.5.0, cli-table3@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" @@ -10811,6 +10988,11 @@ commander@2.x.x, commander@^2.11.0, commander@^2.12.1, commander@^2.13.0, comman resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc" + integrity sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA== + commander@^4.0.1, commander@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -13378,9 +13560,9 @@ electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378, electron-to-chromi integrity sha512-uSEI0XZ//5ic+0NdOqlxp0liCD44ck20OAGyLMSymIWTEAtHKVJi6JM18acOnRgUgX7Q65QqnI+sNncNvIy8ew== electron-to-chromium@^1.3.488: - version "1.3.603" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.603.tgz#1b71bec27fb940eccd79245f6824c63d5f7e8abf" - integrity sha512-J8OHxOeJkoSLgBXfV9BHgKccgfLMHh+CoeRo6wJsi6m0k3otaxS/5vrHpMNSEYY4MISwewqanPOuhAtuE8riQQ== + version "1.3.606" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.606.tgz#6ef2655d9a7c1b447dfdd6344657d00461a65e26" + integrity sha512-+/2yPHwtNf6NWKpaYt0KoqdSZ6Qddt6nDfH/pnhcrHq9hSb23e5LFy06Mlf0vF2ykXvj7avJ597psqcbKnG5YQ== electron@11.0.2: version "11.0.2" @@ -17132,6 +17314,11 @@ highlight.js@^8.5.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-8.9.1.tgz#b8a9c5493212a9392f0222b649c9611497ebfb88" integrity sha1-uKnFSTISqTkvAiK2SclhFJfr+4g= +highlight.js@^9.6.0: + version "9.18.5" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" + integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA== + history@5.0.0-beta.4: version "5.0.0-beta.4" resolved "https://registry.yarnpkg.com/history/-/history-5.0.0-beta.4.tgz#7fd3bb1f6c75d00d9b5112a816766bfc72d1a3cd" @@ -17946,6 +18133,25 @@ inquirer@7.0.4: strip-ansi "^5.1.0" through "^2.3.6" +inquirer@7.3.3, inquirer@^7.0.0: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + inquirer@^6.2.0: version "6.5.2" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" @@ -17965,25 +18171,6 @@ inquirer@^6.2.0: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^7.0.0: - version "7.3.3" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" - integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.19" - mute-stream "0.0.8" - run-async "^2.4.0" - rxjs "^6.6.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - insert-module-globals@^7.0.0: version "7.2.0" resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.2.0.tgz#ec87e5b42728479e327bd5c5c71611ddfb4752ba" @@ -18388,6 +18575,11 @@ is-integer@^1.0.4: dependencies: is-finite "^1.0.0" +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + is-map@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" @@ -23900,6 +24092,20 @@ optionator@^0.8.1, optionator@^0.8.2, optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" +ora@*, ora@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.1.0.tgz#b188cf8cd2d4d9b13fd25383bc3e5cba352c94f8" + integrity sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w== + dependencies: + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.4.0" + is-interactive "^1.0.0" + log-symbols "^4.0.0" + mute-stream "0.0.8" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + ordered-read-streams@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" @@ -24472,6 +24678,13 @@ parse5-html-rewriting-stream@5.1.1: parse5 "^5.1.1" parse5-sax-parser "^5.1.1" +parse5-htmlparser2-tree-adapter@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-5.1.1.tgz#e8c743d4e92194d5293ecde2b08be31e67461cbc" + integrity sha512-CF+TKjXqoqyDwHqBhFQ+3l5t83xYi6fVT1tQNg+Ye0JRLnTxWvIroCjEp1A0k4lneHNBGnICUf0cfYVYGEazqw== + dependencies: + parse5 "^5.1.1" + parse5-sax-parser@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/parse5-sax-parser/-/parse5-sax-parser-5.1.1.tgz#02834a9d08b23ea2d99584841c38be09d5247a15" @@ -30656,18 +30869,7 @@ tar-fs@^2.0.0: pump "^3.0.0" tar-stream "^2.0.0" -tar-stream@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41" - integrity sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA== - dependencies: - bl "^4.0.1" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tar-stream@^2.1.4: +tar-stream@^2.0.0, tar-stream@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== @@ -32888,7 +33090,7 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -wcwidth@^1.0.0: +wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= @@ -34274,7 +34476,7 @@ yargs@^14.2.2, yargs@^14.2.3: y18n "^4.0.0" yargs-parser "^15.0.1" -yargs@^15.0.1, yargs@^15.0.2, yargs@^15.3.1: +yargs@^15.0.0, yargs@^15.0.1, yargs@^15.0.2, yargs@^15.3.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==