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 820bc5c8c7f3..8071f0646910 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,10 @@ packages/server/test/support/fixtures/server/libs # from runner-ct /packages/runner-ct/cypress/screenshots +# 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/appveyor.yml b/appveyor.yml index 4d6d12475ebe..554f87d06936 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,6 +4,7 @@ branches: - develop - sem-next-ver - /win*/ + - feature/cross-platform-wizard # https://www.appveyor.com/docs/lang/nodejs-iojs/ environment: diff --git a/circle.yml b/circle.yml index 1bd32cc941fe..0abf89a1c531 100644 --- a/circle.yml +++ b/circle.yml @@ -1129,6 +1129,17 @@ jobs: path: npm/react/test_results - store-npm-logs + + 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: @@ -1758,6 +1769,9 @@ linux-workflow: &linux-workflow - npm-react: requires: - build + - npm-create-cypress-tests: + requires: + - build - npm-eslint-plugin-dev: requires: - build 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..5b71f707acbf --- /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) 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__/vueVite.test.ts.js b/npm/create-cypress-tests/__snapshots__/vueVite.test.ts.js new file mode 100644 index 000000000000..9fcb7fe62166 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/vueVite.test.ts.js @@ -0,0 +1,13 @@ +exports['vue: vite template correctly generates plugins config 1'] = ` +const { + startDevServer +} = require("@cypress/vite-dev-server"); + +const something = require("something"); + +module.exports = (on, config) => { + on("dev-server:start", async options => startDevServer({ + options + })); +}; +` 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..43d2c56b3c2e --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js @@ -0,0 +1,34 @@ +exports['webpack-options template correctly generates plugins config 1'] = ` +const webpackPreprocessor = require("@cypress/webpack-preprocessor"); + +const something = require("something"); + +module.exports = (on, config) => { + /** @type import("webpack").Configuration */ + const webpackConfig = { + resolve: { + extensions: ['.js', '.ts', '.jsx', '.tsx'] + }, + mode: 'development', + devtool: false, + output: { + publicPath: '/', + chunkFilename: '[name].bundle.js' + }, + // TODO: update with valid configuration for your app + module: { + rules: [{ + test: /\\.(js|jsx|mjs|ts|tsx)$/, + loader: 'babel-loader', + options: { ...babelConfig, + cacheDirectory: path.resolve(__dirname, '..', '..', '.babel-cache') + } + }] + } + }; + on('dev-server:start', options => startDevServer({ + options, + webpackConfig + })); +}; +` 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..0fa8b49c8128 --- /dev/null +++ b/npm/create-cypress-tests/package.json @@ -0,0 +1,45 @@ +{ + "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 && copy src/**/*.template.js dist/src", + "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.10", + "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", + "copy": "0.3.2", + "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-tsconfig.json b/npm/create-cypress-tests/scripts/example-tsconfig.json new file mode 100644 index 000000000000..08c029d44419 --- /dev/null +++ b/npm/create-cypress-tests/scripts/example-tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "es5", + "dom" + ], + "types": [ + "cypress" + ] + }, + "include": [ + "**/*.ts*" + ] +} \ No newline at end of file diff --git a/npm/create-cypress-tests/scripts/example.js b/npm/create-cypress-tests/scripts/example.js new file mode 100644 index 000000000000..49b35fd7ee21 --- /dev/null +++ b/npm/create-cypress-tests/scripts/example.js @@ -0,0 +1,23 @@ +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', 'cypress') + 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)}`) + + await fs.copy(path.join(__dirname, 'example-tsconfig.json'), path.join(destination, 'tsconfig.json')) + + console.log(`✅ tsconfig.json was created for ${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..258915c6ebfd --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/init-component-testing.ts @@ -0,0 +1,225 @@ +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' +import { installDependency } from '../utils' + +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(`Installing required dependencies`) + console.log() + + for (const dependency of chosenTemplate.dependencies) { + await installDependency(dependency, { useYarn }) + } + + console.log() + console.log(`Let's setup everything for component testing with ${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..72ea8de09d2b --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/Template.ts @@ -0,0 +1,14 @@ +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 + dependencies: string[] + 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..26ef039e4072 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts @@ -0,0 +1,44 @@ +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', + dependencies: ['@cypress/webpack-dev-server'], + 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..7c31b686172d --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/next.ts @@ -0,0 +1,55 @@ +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', + dependencies: ['@cypress/webpack-dev-server'], + 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..a4c69d061225 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts @@ -0,0 +1,62 @@ +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.', + dependencies: ['@cypress/webpack-dev-server'], + 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..be62474474cb --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts @@ -0,0 +1,42 @@ +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', + dependencies: ['@cypress/webpack-dev-server'], + 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..b0ddd4cd299a --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/rollup.ts @@ -0,0 +1,123 @@ +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', + dependencies: ['@cypress/rollup-dev-server'], + 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..5d58b31cb2b9 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js @@ -0,0 +1,24 @@ +/** @type import("webpack").Configuration */ +const webpackConfig = { + resolve: { + extensions: ['.js', '.ts', '.jsx', '.tsx'], + }, + mode: 'development', + devtool: false, + output: { + publicPath: '/', + chunkFilename: '[name].bundle.js', + }, + // TODO: update with valid configuration for your app + module: { + rules: [ + { + test: /\.(js|jsx|mjs|ts|tsx)$/, + loader: 'babel-loader', + options: { ...babelConfig, cacheDirectory: path.resolve(__dirname, '..', '..', '.babel-cache') }, + }, + ] + }, +} + +on('dev-server:start', (options) => startDevServer({ options, webpackConfig })) 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..8f90600a3b4e --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts @@ -0,0 +1,32 @@ +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', + dependencies: ['@cypress/webpack-dev-server'], + 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/vite) 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..771a39323836 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/index.ts @@ -0,0 +1,10 @@ +import { Template } from '../Template' +import { VueCliTemplate } from './vueCli' +import { VueViteTemplate } from './vueVite' +import { VueWebpackTemplate } from './vueWebpackFile' + +export const vueTemplates: Record> = { + webpack: VueWebpackTemplate, + 'vue-cli': VueCliTemplate, + vite: VueViteTemplate, +} 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..8d016675aa6f --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.ts @@ -0,0 +1,30 @@ +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', + dependencies: ['@cypress/webpack-dev-server'], + 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/vueVite.test.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueVite.test.ts new file mode 100644 index 000000000000..d40fbbdefb96 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/vueVite.test.ts @@ -0,0 +1,6 @@ +import { VueViteTemplate } from './vueVite' +import { snapshotPluginsAstCode } from '../../../test-utils' + +describe('vue: vite template', () => { + it('correctly generates plugins config', () => snapshotPluginsAstCode(VueViteTemplate)) +}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueVite.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueVite.ts new file mode 100644 index 000000000000..e183f512687e --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/vueVite.ts @@ -0,0 +1,26 @@ +import * as babel from '@babel/core' +import { scanFSForAvailableDependency } from '../../../findPackageJson' +import { Template } from '../Template' + +export const VueViteTemplate: Template = { + message: + 'It looks like you are using vitejs to run and build an application.', + getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/vue/examples/vite', + recommendedComponentFolder: 'src', + dependencies: ['@cypress/vite-dev-server'], + getPluginsCodeAst: () => { + return { + Require: babel.template.ast( + 'const { startDevServer } = require("@cypress/vite-dev-server");', + ), + ModuleExportsBody: babel.template.ast([ + 'on("dev-server:start", async (options) => startDevServer({ options }))', + ].join('\n'), { preserveComments: true }), + } + }, + test: (root) => { + return { + success: scanFSForAvailableDependency(root, ['vite']), + } + }, +} 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..45023cd39b14 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts @@ -0,0 +1,42 @@ +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', + dependencies: ['@cypress/webpack-dev-server'], + 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/initialTemplate.ts b/npm/create-cypress-tests/src/initialTemplate.ts new file mode 100644 index 000000000000..332df33e3899 --- /dev/null +++ b/npm/create-cypress-tests/src/initialTemplate.ts @@ -0,0 +1,18 @@ +import path from 'path' +import fs from 'fs-extra' + +const INITIAL_TEMPLATE_PATH = path.resolve(__dirname, '..', 'initial-template') + +export async function getInitialSupportFilesPaths () { + return ( + await fs.readdir(path.join(INITIAL_TEMPLATE_PATH, 'support')) + ).map((filename) => path.join(INITIAL_TEMPLATE_PATH, 'support', filename)) +} + +export function getInitialPluginsFilePath () { + return path.join(INITIAL_TEMPLATE_PATH, 'plugins', 'index.js') +} + +export function getInitialTsConfigPath () { + return path.join(INITIAL_TEMPLATE_PATH, 'tsconfig.json') +} diff --git a/npm/create-cypress-tests/src/installCypress.ts b/npm/create-cypress-tests/src/installCypress.ts new file mode 100644 index 000000000000..ffb2727b1c4a --- /dev/null +++ b/npm/create-cypress-tests/src/installCypress.ts @@ -0,0 +1,76 @@ +import fs from 'fs-extra' +import findUp from 'find-up' +import path from 'path' +import { installDependency } from './utils' +import chalk from 'chalk' +import ora from 'ora' +import * as initialTemplate from './initialTemplate' + +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( + initialTemplate.getInitialPluginsFilePath(), + path.resolve('cypress', 'plugins/index.js'), + ) + + const supportFiles: string[] = await initialTemplate.getInitialSupportFilesPaths() + + 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(initialTemplate.getInitialTsConfigPath(), path.resolve('cypress', 'tsconfig.json')) + } + + // TODO think about better approach + if (ignoreExamples) { + const dummySpec = [ + 'describe("Spec", () => {', + '', + '})', + '', + ].join('\n') + + 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..0381b5ff55fd --- /dev/null +++ b/npm/create-cypress-tests/src/main.test.ts @@ -0,0 +1,170 @@ +import { expect, use } from 'chai' +import path from 'path' +import sinon, { SinonStub, SinonSpy, SinonSpyCallApi } 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 mockFsWithInitialTemplate (...args: Parameters) { + const [fsConfig, options] = args + + mockFs({ + ...fsConfig, + // @ts-expect-error Load required template files + [path.resolve(__dirname, '..', 'initial-template')]: mockFs.load(path.resolve(__dirname, '..', 'initial-template')), + }, options) +} + +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 () => { + mockFsWithInitialTemplate({ + '/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()) + + mockFsWithInitialTemplate({ + '/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 () => { + mockFsWithInitialTemplate({ + '/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 () => { + mockFsWithInitialTemplate({ + '/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 () => { + mockFsWithInitialTemplate({ + '/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 () => { + mockFsWithInitialTemplate({ + '/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..5cf13aaaaa09 --- /dev/null +++ b/npm/create-cypress-tests/tsconfig.json @@ -0,0 +1,30 @@ +{ + "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", + ] +} 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/package.json b/npm/react/package.json index ea2f6455d320..8314fe59d5d4 100644 --- a/npm/react/package.json +++ b/npm/react/package.json @@ -45,9 +45,6 @@ "@rollup/plugin-node-resolve": "^11.1.1", "@testing-library/cypress": "7.0.1", "@types/chalk": "2.2.0", - "@types/inquirer": "7.3.1", - "@types/mock-fs": "4.10.0", - "@types/node": "12.12.50", "@types/semver": "7.3.4", "arg": "4.1.3", "autoprefixer": "9.7.6", @@ -71,8 +68,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", diff --git a/npm/vite-dev-server/cypress/plugins.js b/npm/vite-dev-server/cypress/plugins.js index 9764b3ffaf61..16054ce032f7 100644 --- a/npm/vite-dev-server/cypress/plugins.js +++ b/npm/vite-dev-server/cypress/plugins.js @@ -1,9 +1,7 @@ import { startDevServer } from '@cypress/vite-dev-server' module.exports = (on, config) => { - on('dev-server:start', async (options) => { - return startDevServer({ options }) - }) + on('dev-server:start', async (options) => startDevServer({ options })) return config } 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/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 94% rename from packages/server/lib/scaffold/plugins/index.js rename to packages/example/cypress/plugins/index.js index aa9918d21530..59b2bab6e4e6 100644 --- a/packages/server/lib/scaffold/plugins/index.js +++ b/packages/example/cypress/plugins/index.js @@ -15,6 +15,7 @@ /** * @type {Cypress.PluginConfig} */ +// eslint-disable-next-line no-unused-vars module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config diff --git a/packages/server/lib/scaffold/support/commands.js b/packages/example/cypress/support/commands.js similarity index 69% rename from packages/server/lib/scaffold/support/commands.js rename to packages/example/cypress/support/commands.js index ca4d256f3eb1..119ab03f7cda 100644 --- a/packages/server/lib/scaffold/support/commands.js +++ b/packages/example/cypress/support/commands.js @@ -10,16 +10,16 @@ // // // -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) +// Cypress.Commands.add('login', (email, password) => { ... }) // // // -- 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 -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 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/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 d71dc72bf21f..7b9a4cbc3b1f 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -3,6 +3,7 @@ "version": "0.0.0-development", "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..997aff6a9693 100644 --- a/packages/server/__snapshots__/scaffold_spec.js +++ b/packages/server/__snapshots__/scaffold_spec.js @@ -338,19 +338,19 @@ exports['lib/scaffold .support creates supportFolder and commands.js and index.j // // // -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) +// Cypress.Commands.add('login', (email, password) => { ... }) // // // -- 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 -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) ` @@ -485,6 +485,7 @@ exports['lib/scaffold .plugins creates pluginsFile when pluginsFolder does not e /** * @type {Cypress.PluginConfig} */ +// eslint-disable-next-line no-unused-vars module.exports = (on, config) => { // on is used to hook into various events Cypress emits // config is the resolved Cypress config diff --git a/packages/server/lib/scaffold.js b/packages/server/lib/scaffold.js index 1704ffa6d71b..60780a2cccfb 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 6a252454170f..224e1c37e487 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 e2f9cdba4989..ce7802c21e10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -237,6 +237,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.4.0", "@babel/generator@^7.4.4", "@babel/generator@^7.6.0", "@babel/generator@^7.9.0": version "7.11.6" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" @@ -255,6 +277,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/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.10": version "7.12.10" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d" @@ -448,6 +479,15 @@ "@babel/traverse" "^7.12.5" "@babel/types" "^7.12.5" +"@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/highlight@^7.0.0", "@babel/highlight@^7.10.4", "@babel/highlight@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" @@ -467,6 +507,16 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== +"@babel/parser@^7.10.4": + version "7.13.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99" + integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw== + +"@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/plugin-proposal-async-generator-functions@^7.10.4", "@babel/plugin-proposal-async-generator-functions@^7.12.1", "@babel/plugin-proposal-async-generator-functions@^7.2.0", "@babel/plugin-proposal-async-generator-functions@^7.8.3": version "7.12.12" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz#04b8f24fd4532008ab4e79f788468fd5a8476566" @@ -1185,7 +1235,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-typescript@^7.10.4", "@babel/plugin-transform-typescript@^7.12.1", "@babel/plugin-transform-typescript@^7.9.0": +"@babel/plugin-transform-typescript@^7.10.4", "@babel/plugin-transform-typescript@^7.12.1", "@babel/plugin-transform-typescript@^7.2.0", "@babel/plugin-transform-typescript@^7.9.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== @@ -1711,6 +1761,15 @@ "@babel/parser" "^7.12.7" "@babel/types" "^7.12.7" +"@babel/template@^7.5.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.6.0", "@babel/traverse@^7.7.0", "@babel/traverse@^7.9.0": version "7.11.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" @@ -1757,9 +1816,9 @@ 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" @@ -1798,6 +1857,15 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@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" + "@bahmutov/all-paths@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@bahmutov/all-paths/-/all-paths-1.0.2.tgz#9ae0dcdf9022dd6e5e14d7fda3479e6a330d035b" @@ -5244,6 +5312,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.2" resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8" @@ -5769,6 +5848,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.2.tgz#264b44c5a28dfa80198fc2f7b6d3c8a054b9491f" integrity sha512-onlIwbaeqvZyniGPfdw/TEhKIh79pz66L1q06WUQqJLnAb6wbjvOtepLYTGHTqzdXgBYIE3ZdmqHDGsRsbBz7A== +"@types/node@9.6.49": + version "9.6.49" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.49.tgz#ab4df6e505db088882c8ce5417ae0bc8cbb7a8a6" + integrity sha512-YY0Okyn4QXC4ugJI+Kng5iWjK8A6eIHiQVaGIhJkyn0YL6Iqo0E0tBC8BuhvYcBK87vykBijM5FtMnCqaa5anA== + "@types/node@^12.0.12": version "12.19.12" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.12.tgz#04793c2afa4ce833a9972e4c476432e30f9df47b" @@ -5784,6 +5868,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" @@ -7220,6 +7311,13 @@ ansi-gray@^0.1.1: dependencies: ansi-wrap "0.1.0" +ansi-green@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-green/-/ansi-green-0.1.1.tgz#8a5d9a979e458d57c40e33580b37390b8e10d0f7" + integrity sha1-il2al55FjVfEDjNYCzc5C44Q0Pc= + dependencies: + ansi-wrap "0.1.0" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -7875,6 +7973,11 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async-array-reduce@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/async-array-reduce/-/async-array-reduce-0.2.1.tgz#c8be010a2b5cd00dea96c81116034693dfdd82d1" + integrity sha1-yL4BCitc0A3qlsgRFgNGk9/dgtE= + async-done@^1.2.0, async-done@^1.2.2: version "1.3.2" resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.3.2.tgz#5e15aa729962a4b07414f528a88cdf18e0b290a2" @@ -9388,7 +9491,7 @@ bluebird@3.7.1: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de" integrity sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg== -bluebird@3.7.2, bluebird@^3.1.1, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5, bluebird@^3.7.1, bluebird@^3.7.2: +bluebird@3.7.2, bluebird@^3.1.1, bluebird@^3.4.1, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5, bluebird@^3.7.1, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -10487,7 +10590,7 @@ chai@4.2.0, 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== @@ -11021,7 +11124,7 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-highlight@^2.1.4: +cli-highlight@2.1.10, cli-highlight@^2.1.4: version "2.1.10" resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.10.tgz#26a087da9209dce4fcb8cf5427dc97cd96ac173a" integrity sha512-CcPFD3JwdQ2oSzy+AMG6j3LRTkNjM82kzcSKzoVw6cLanDCJNlsLjeqVTOTfOfucnWv5F0rmBemVf1m9JiIasw== @@ -11033,7 +11136,7 @@ cli-highlight@^2.1.4: parse5-htmlparser2-tree-adapter "^6.0.0" yargs "^16.0.0" -cli-spinners@^2.0.0: +cli-spinners@^2.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== @@ -11238,6 +11341,11 @@ clone-response@1.0.2, clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +clone-stats@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" + integrity sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE= + clone-stats@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" @@ -11248,7 +11356,7 @@ clone@2.1.2, clone@^2.1.1, clone@^2.1.2: resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= -clone@^1.0.2: +clone@^1.0.0, clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= @@ -11494,6 +11602,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@6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" @@ -11984,6 +12097,26 @@ copy-webpack-plugin@^5.1.1: serialize-javascript "^4.0.0" webpack-log "^2.0.0" +copy@0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/copy/-/copy-0.3.2.tgz#870b871d02a599b3c6ef27bc5b3d4c4102261909" + integrity sha512-drDFuUZctIuvSuvL9dOF/v5GxrwB1Q8eMIRlYONC0lSMEq+L2xabXP3jme8cQFdDO8cgP8JsuYhQg7JtTwezmg== + dependencies: + async-each "^1.0.0" + bluebird "^3.4.1" + extend-shallow "^2.0.1" + file-contents "^0.3.1" + glob-parent "^2.0.0" + graceful-fs "^4.1.4" + has-glob "^0.1.1" + is-absolute "^0.2.5" + lazy-cache "^2.0.1" + log-ok "^0.1.1" + matched "^0.4.1" + mkdirp "^0.5.1" + resolve-dir "^0.1.0" + to-file "^0.2.0" + core-js-compat@^3.1.1, core-js-compat@^3.6.2, core-js-compat@^3.6.5, core-js-compat@^3.8.0: version "3.8.2" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.2.tgz#3717f51f6c3d2ebba8cbf27619b57160029d1d4c" @@ -15631,6 +15764,13 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +expand-tilde@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" + integrity sha1-C4HrqJflo9MdHD0QL48BRB5VlEk= + dependencies: + os-homedir "^1.0.1" + expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" @@ -15767,7 +15907,7 @@ extend-shallow@^1.1.2: dependencies: kind-of "^1.1.0" -extend-shallow@^2.0.1: +extend-shallow@^2.0.0, extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= @@ -16049,6 +16189,37 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" +file-contents@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/file-contents/-/file-contents-0.2.4.tgz#0506f7b8eff62afa45ae45da4df9e9d47df453cb" + integrity sha1-BQb3uO/2KvpFrkXaTfnp1H30U8s= + dependencies: + extend-shallow "^2.0.0" + file-stat "^0.1.0" + graceful-fs "^4.1.2" + is-buffer "^1.1.0" + is-utf8 "^0.2.0" + lazy-cache "^0.2.3" + through2 "^2.0.0" + +file-contents@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/file-contents/-/file-contents-0.3.2.tgz#a0939fed1b8cda1580266fc6b753a232fb46de53" + integrity sha1-oJOf7RuM2hWAJm/Gt1OiMvtG3lM= + dependencies: + define-property "^0.2.5" + extend-shallow "^2.0.1" + file-stat "^0.2.3" + fs-exists-sync "^0.1.0" + graceful-fs "^4.1.4" + is-buffer "^1.1.3" + isobject "^2.1.0" + lazy-cache "^2.0.1" + strip-bom-buffer "^0.1.1" + strip-bom-string "^0.1.2" + through2 "^2.0.1" + vinyl "^1.1.1" + file-entry-cache@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" @@ -16092,6 +16263,25 @@ file-loader@4.3.0, file-loader@^4.2.0, file-loader@~4.3.0: loader-utils "^1.2.3" schema-utils "^2.5.0" +file-stat@^0.1.0: + version "0.1.3" + resolved "https://registry.yarnpkg.com/file-stat/-/file-stat-0.1.3.tgz#d0f1961d7d10732928120a6e6955471c2a5b5411" + integrity sha1-0PGWHX0QcykoEgpuaVVHHCpbVBE= + dependencies: + graceful-fs "^4.1.2" + lazy-cache "^0.2.3" + through2 "^2.0.0" + +file-stat@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/file-stat/-/file-stat-0.2.3.tgz#469a7e927d6930079624cdb38109405456cb06a9" + integrity sha1-Rpp+kn1pMAeWJM2zgQlAVFbLBqk= + dependencies: + fs-exists-sync "^0.1.0" + graceful-fs "^4.1.4" + lazy-cache "^2.0.1" + through2 "^2.0.1" + file-type@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-9.0.0.tgz#a68d5ad07f486414dfb2c8866f73161946714a18" @@ -16767,6 +16957,11 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" + integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= + fs-extra@7.0.1, fs-extra@^7.0.0, fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" @@ -17484,6 +17679,14 @@ global-modules@2.0.0: dependencies: global-prefix "^3.0.0" +global-modules@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" + integrity sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0= + dependencies: + global-prefix "^0.1.4" + is-windows "^0.2.0" + global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -17493,6 +17696,16 @@ global-modules@^1.0.0: is-windows "^1.0.1" resolve-dir "^1.0.0" +global-prefix@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f" + integrity sha1-jTvGuNo8qBEqFg2NSW/wRiv+948= + dependencies: + homedir-polyfill "^1.0.0" + ini "^1.3.4" + is-windows "^0.2.0" + which "^1.2.12" + global-prefix@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" @@ -18102,6 +18315,13 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-glob@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/has-glob/-/has-glob-0.1.1.tgz#a261c4c2a6c667e0c77b700a7f297c39ef3aa589" + integrity sha1-omHEwqbGZ+DHe3AKfyl8Oe86pYk= + dependencies: + is-glob "^2.0.1" + has-only@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/has-only/-/has-only-1.1.1.tgz#0ed2b101e73a2226254421464c65e381affcce66" @@ -18305,7 +18525,7 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" -homedir-polyfill@^1.0.1: +homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== @@ -19157,6 +19377,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" @@ -19176,25 +19415,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.1" resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.2.1.tgz#d5e33185181a4e1f33b15f7bf100ee91890d5cb3" @@ -19313,6 +19533,14 @@ is-absolute-url@^3.0.3: resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== +is-absolute@^0.2.5: + version "0.2.6" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.2.6.tgz#20de69f3db942ef2d87b9c2da36f172235b1b5eb" + integrity sha1-IN5p89uULvLYe5wto28XIjWxtes= + dependencies: + is-relative "^0.2.1" + is-windows "^0.2.0" + is-absolute@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" @@ -19373,7 +19601,7 @@ is-boolean-object@^1.0.1: dependencies: call-bind "^1.0.0" -is-buffer@^1.0.2, is-buffer@^1.1.0, is-buffer@^1.1.5, is-buffer@~1.1.6: +is-buffer@^1.0.2, is-buffer@^1.1.0, is-buffer@^1.1.3, is-buffer@^1.1.5, is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -19615,6 +19843,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-lower-case@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393" @@ -19941,6 +20174,11 @@ is-utf8@^0.2.0, is-utf8@^0.2.1: resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= +is-valid-glob@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-0.3.0.tgz#d4b55c69f51886f9b65c70d6c2622d37e29f48fe" + integrity sha1-1LVcafUYhvm2XHDWwmItN+KfSP4= + is-valid-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" @@ -19956,6 +20194,11 @@ is-window@^1.0.2: resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0= +is-windows@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" + integrity sha1-3hqm1j6indJIc3tp8f+LgALSEIw= + is-windows@^1.0.0, is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -20019,7 +20262,7 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -isobject@^2.0.0: +isobject@^2.0.0, isobject@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= @@ -21593,6 +21836,13 @@ lazy-cache@^1.0.3: resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= +lazy-cache@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" + integrity sha1-uRkKT5EzVGlIQIWfio9whNiCImQ= + dependencies: + set-getter "^0.1.0" + lazy-compile-webpack-plugin@0.1.11: version "0.1.11" resolved "https://registry.yarnpkg.com/lazy-compile-webpack-plugin/-/lazy-compile-webpack-plugin-0.1.11.tgz#be3b9487ccc731a606dc55bcfcd80000c72e4237" @@ -22440,6 +22690,14 @@ lodash@4.17.4: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" integrity sha1-eCA6TRwyiuHYbcpkYONptX9AVa4= +log-ok@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/log-ok/-/log-ok-0.1.1.tgz#bea3dd36acd0b8a7240d78736b5b97c65444a334" + integrity sha1-vqPdNqzQuKckDXhza1uXxlREozQ= + dependencies: + ansi-green "^0.1.1" + success-symbol "^0.1.0" + log-symbols@2.2.0, log-symbols@^2.1.0, log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" @@ -22821,6 +23079,21 @@ matchdep@^2.0.0: resolve "^1.4.0" stack-trace "0.0.10" +matched@^0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/matched/-/matched-0.4.4.tgz#56d7b7eb18033f0cf9bc52eb2090fac7dc1e89fa" + integrity sha1-Vte36xgDPwz5vFLrIJD6x9weifo= + dependencies: + arr-union "^3.1.0" + async-array-reduce "^0.2.0" + extend-shallow "^2.0.1" + fs-exists-sync "^0.1.0" + glob "^7.0.5" + has-glob "^0.1.1" + is-valid-glob "^0.3.0" + lazy-cache "^2.0.1" + resolve-dir "^0.1.0" + matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" @@ -25380,6 +25653,20 @@ optionator@^0.9.1: type-check "^0.4.0" 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" + ora@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" @@ -25416,7 +25703,7 @@ os-browserify@^0.3.0, os-browserify@~0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= -os-homedir@^1.0.0: +os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= @@ -29375,6 +29662,11 @@ repl.history@0.1.4: resolved "https://registry.yarnpkg.com/repl.history/-/repl.history-0.1.4.tgz#80367171f3781d6e4299c71758c253097f5d5832" integrity sha1-gDZxcfN4HW5CmccXWMJTCX9dWDI= +replace-ext@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" + integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ= + replace-ext@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" @@ -29539,6 +29831,14 @@ resolve-dependency-path@^2.0.0: resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-2.0.0.tgz#11700e340717b865d216c66cabeb4a2a3c696736" integrity sha512-DIgu+0Dv+6v2XwRaNWnumKu7GPufBBOr5I1gRPJHkvghrfCGOooJODFvgFimX/KRxk9j0whD2MnKHzM1jYvk9w== +resolve-dir@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" + integrity sha1-shklmlYC+sXFxJatiUpujMQwJh4= + dependencies: + expand-tilde "^1.2.2" + global-modules "^0.2.3" + resolve-dir@^1.0.0, resolve-dir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" @@ -30504,6 +30804,13 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-getter@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" + integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y= + dependencies: + to-object-path "^0.3.0" + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -32027,6 +32334,19 @@ strip-ansi@~0.1.0: resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" integrity sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE= +strip-bom-buffer@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/strip-bom-buffer/-/strip-bom-buffer-0.1.1.tgz#ca3ddc4919c13f9fddf30b1dff100a9835248b4d" + integrity sha1-yj3cSRnBP5/d8wsd/xAKmDUki00= + dependencies: + is-buffer "^1.1.0" + is-utf8 "^0.2.0" + +strip-bom-string@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-0.1.2.tgz#9c6e720a313ba9836589518405ccfb88a5f41b9c" + integrity sha1-nG5yCjE7qYNliVGEBcz7iKX0G5w= + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -32206,6 +32526,11 @@ subarg@^1.0.0: dependencies: minimist "^1.1.0" +success-symbol@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" + integrity sha1-JAIuSG878c3KCUKDt2nEctO3KJc= + sumchecker@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42" @@ -32796,7 +33121,7 @@ through2-filter@^3.0.0: through2 "~2.0.0" xtend "~4.0.0" -through2@2.0.5, through2@^2.0.0, through2@^2.0.2, through2@^2.0.3, through2@~2.0.0: +through2@2.0.5, through2@^2.0.0, through2@^2.0.1, through2@^2.0.2, through2@^2.0.3, through2@~2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -32985,6 +33310,20 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= +to-file@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/to-file/-/to-file-0.2.0.tgz#236c6c088065e570defbd15cf4b4e565be46ea93" + integrity sha1-I2xsCIBl5XDe+9Fc9LTlZb5G6pM= + dependencies: + define-property "^0.2.5" + extend-shallow "^2.0.1" + file-contents "^0.2.4" + glob-parent "^2.0.0" + is-valid-glob "^0.3.0" + isobject "^2.1.0" + lazy-cache "^2.0.1" + vinyl "^1.1.1" + to-iso-string@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" @@ -34576,6 +34915,15 @@ vinyl-sourcemap@^1.1.0: remove-bom-buffer "^3.0.0" vinyl "^2.0.0" +vinyl@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" + integrity sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ= + dependencies: + clone "^1.0.0" + clone-stats "^0.0.1" + replace-ext "0.0.1" + vinyl@^2.0.0, vinyl@^2.1.0, vinyl@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" @@ -35455,7 +35803,7 @@ which-typed-array@^1.1.2: has-symbols "^1.0.1" is-typed-array "^1.1.3" -which@1.3.1, which@^1.1.1, which@^1.2.14, which@^1.2.4, which@^1.2.8, which@^1.2.9, which@^1.3.0, which@^1.3.1: +which@1.3.1, which@^1.1.1, which@^1.2.12, which@^1.2.14, which@^1.2.4, which@^1.2.8, which@^1.2.9, which@^1.3.0, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==