import { addDependenciesToPackageJson, addProjectConfiguration, convertNxGenerator, extractLayoutDirectory, formatFiles, generateFiles, GeneratorCallback, getProjects, getWorkspaceLayout, joinPathFragments, logger, names, offsetFromRoot, ProjectConfiguration, readProjectConfiguration, runTasksInSerial, stripIndents, toJS, Tree, updateJson, } from '@nrwl/devkit'; import { Linter, lintProjectGenerator } from '@nrwl/linter'; import { getRelativePathToRootTsConfig } from '@nrwl/js'; import { globalJavaScriptOverrides, globalTypeScriptOverrides, } from '@nrwl/linter/src/generators/init/global-eslint-config'; import { join } from 'path'; import { installedCypressVersion } from '../../utils/cypress-version'; import { filePathPrefix } from '../../utils/project-name'; import { cypressVersion, eslintPluginCypressVersion, viteVersion, } from '../../utils/versions'; import { cypressInitGenerator } from '../init/init'; // app import { Schema } from './schema'; export interface CypressProjectSchema extends Schema { projectName: string; projectRoot: string; } function createFiles(tree: Tree, options: CypressProjectSchema) { // if not installed or >v10 use v10 folder // else use v9 folder const cypressVersion = installedCypressVersion(); const cypressFiles = cypressVersion && cypressVersion < 10 ? 'v9-and-under' : 'v10-and-after'; generateFiles( tree, join(__dirname, './files', cypressFiles), options.projectRoot, { tmpl: '', ...options, project: options.project || 'Project', ext: options.js ? 'js' : 'ts', offsetFromRoot: offsetFromRoot(options.projectRoot), rootTsConfigPath: getRelativePathToRootTsConfig( tree, options.projectRoot ), bundler: options.bundler, } ); if (cypressVersion && cypressVersion < 7) { updateJson(tree, join(options.projectRoot, 'cypress.json'), (json) => { json.pluginsFile = './src/plugins/index'; return json; }); } else if (cypressVersion < 10) { const pluginPath = join(options.projectRoot, 'src/plugins/index.js'); if (tree.exists(pluginPath)) { tree.delete(pluginPath); } } if (options.js) { toJS(tree); } } function addProject(tree: Tree, options: CypressProjectSchema) { let e2eProjectConfig: ProjectConfiguration; const detectedCypressVersion = installedCypressVersion() ?? cypressVersion; const cypressConfig = detectedCypressVersion < 10 ? 'cypress.json' : 'cypress.config.ts'; if (options.baseUrl) { e2eProjectConfig = { root: options.projectRoot, sourceRoot: joinPathFragments(options.projectRoot, 'src'), projectType: 'application', targets: { e2e: { executor: '@nrwl/cypress:cypress', options: { cypressConfig: joinPathFragments( options.projectRoot, cypressConfig ), baseUrl: options.baseUrl, testingType: 'e2e', }, }, }, tags: [], implicitDependencies: options.project ? [options.project] : undefined, }; } else if (options.project) { const project = readProjectConfiguration(tree, options.project); if (!project.targets) { logger.warn(stripIndents` NOTE: Project, "${options.project}", does not have any targets defined and a baseUrl was not provided. Nx will use "${options.project}:serve" as the devServerTarget. But you may need to define this target within the project, "${options.project}". `); } const devServerTarget = project.targets?.serve && project.targets?.serve?.defaultConfiguration ? `${options.project}:serve:${project.targets.serve.defaultConfiguration}` : `${options.project}:serve`; e2eProjectConfig = { root: options.projectRoot, sourceRoot: joinPathFragments(options.projectRoot, 'src'), projectType: 'application', targets: { e2e: { executor: '@nrwl/cypress:cypress', options: { cypressConfig: joinPathFragments( options.projectRoot, cypressConfig ), devServerTarget, testingType: 'e2e', }, configurations: { production: { devServerTarget: `${options.project}:serve:production`, }, }, }, }, tags: [], implicitDependencies: options.project ? [options.project] : undefined, }; } else { throw new Error(`Either project or baseUrl should be specified.`); } if (detectedCypressVersion < 7) { e2eProjectConfig.targets.e2e.options.tsConfig = joinPathFragments( options.projectRoot, 'tsconfig.json' ); } addProjectConfiguration(tree, options.projectName, e2eProjectConfig); } export async function addLinter(host: Tree, options: CypressProjectSchema) { if (options.linter === Linter.None) { return () => {}; } const installTask = await lintProjectGenerator(host, { project: options.projectName, linter: options.linter, skipFormat: true, tsConfigPaths: [joinPathFragments(options.projectRoot, 'tsconfig.json')], eslintFilePatterns: [ `${options.projectRoot}/**/*.${options.js ? 'js' : '{js,ts}'}`, ], setParserOptionsProject: options.setParserOptionsProject, skipPackageJson: options.skipPackageJson, rootProject: options.rootProject, }); if (!options.linter || options.linter !== Linter.EsLint) { return installTask; } const installTask2 = !options.skipPackageJson ? addDependenciesToPackageJson( host, {}, { 'eslint-plugin-cypress': eslintPluginCypressVersion } ) : () => {}; updateJson(host, join(options.projectRoot, '.eslintrc.json'), (json) => { if (options.rootProject) { json.plugins = ['@nrwl/nx']; json.extends = ['plugin:cypress/recommended']; } else { json.extends = ['plugin:cypress/recommended', ...json.extends]; } json.overrides = [ ...(options.rootProject ? [globalTypeScriptOverrides, globalJavaScriptOverrides] : []), /** * In order to ensure maximum efficiency when typescript-eslint generates TypeScript Programs * behind the scenes during lint runs, we need to make sure the project is configured to use its * own specific tsconfigs, and not fall back to the ones in the root of the workspace. */ { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], /** * NOTE: We no longer set parserOptions.project by default when creating new projects. * * We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore * do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project, * typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple * parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much * less memory intensive. * * In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set * parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you * and provide feedback to the user. */ parserOptions: !options.setParserOptionsProject ? undefined : { project: `${options.projectRoot}/tsconfig.*?.json`, }, /** * Having an empty rules object present makes it more obvious to the user where they would * extend things from if they needed to */ rules: {}, }, ]; if (installedCypressVersion() < 7) { /** * We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests. * That however leads to issues with the CommonJS Cypress plugin file. */ json.overrides.push({ files: ['src/plugins/index.js'], rules: { '@typescript-eslint/no-var-requires': 'off', 'no-undef': 'off', }, }); } return json; }); return runTasksInSerial(installTask, installTask2); } export async function cypressProjectGenerator(host: Tree, schema: Schema) { const options = normalizeOptions(host, schema); const tasks: GeneratorCallback[] = []; const cypressVersion = installedCypressVersion(); // if there is an installed cypress version, then we don't call // init since we want to keep the existing version that is installed if (!cypressVersion) { tasks.push(await cypressInitGenerator(host, options)); } if (schema.bundler === 'vite') { tasks.push( addDependenciesToPackageJson( host, {}, { vite: viteVersion, } ) ); } createFiles(host, options); addProject(host, options); const installTask = await addLinter(host, options); tasks.push(installTask); if (!options.skipFormat) { await formatFiles(host); } return runTasksInSerial(...tasks); } function normalizeOptions(host: Tree, options: Schema): CypressProjectSchema { const { layoutDirectory, projectDirectory } = extractLayoutDirectory( options.directory ); const appsDir = layoutDirectory ?? getWorkspaceLayout(host).appsDir; let projectName: string; let projectRoot: string; let maybeRootProject: ProjectConfiguration; let isRootProject = false; const projects = getProjects(host); // nx will set the project option for generators when ran within a project. // since the root project will always be set for standlone projects we can just check it here. if (options.project) { maybeRootProject = projects.get(options.project); } if ( maybeRootProject?.root === '.' || // should still check to see if we are in a standalone based workspace (!maybeRootProject && Array.from(projects.values()).some((config) => config.root === '.')) ) { projectName = options.name; projectRoot = options.name; isRootProject = true; } else { projectName = filePathPrefix( projectDirectory ? `${projectDirectory}-${options.name}` : options.name ); projectRoot = projectDirectory ? joinPathFragments( appsDir, names(projectDirectory).fileName, options.name ) : joinPathFragments(appsDir, options.name); } options.linter = options.linter || Linter.EsLint; options.bundler = options.bundler || 'webpack'; return { ...options, // other generators depend on the rootProject flag down stream rootProject: isRootProject, projectName, projectRoot, }; } export default cypressProjectGenerator; export const cypressProjectSchematic = convertNxGenerator( cypressProjectGenerator );