import { addDependenciesToPackageJson, formatFiles, generateFiles, GeneratorCallback, joinPathFragments, offsetFromRoot, parseTargetString, readProjectConfiguration, runTasksInSerial, toJS, Tree, updateJson, updateProjectConfiguration, } from '@nx/devkit'; import { getRelativePathToRootTsConfig } from '@nx/js'; import { Linter } from '@nx/eslint'; import { join } from 'path'; import { addLinterToCyProject } from '../../utils/add-linter'; import { addDefaultE2EConfig } from '../../utils/config'; import { installedCypressVersion } from '../../utils/cypress-version'; import { viteVersion } from '../../utils/versions'; import cypressInitGenerator from '../init/init'; import { addBaseCypressSetup } from '../base-setup/base-setup'; export interface CypressE2EConfigSchema { project: string; baseUrl?: string; directory?: string; js?: boolean; skipFormat?: boolean; setParserOptionsProject?: boolean; skipPackageJson?: boolean; bundler?: 'webpack' | 'vite' | 'none'; devServerTarget?: string; linter?: Linter; port?: number | 'cypress-auto'; jsx?: boolean; } type NormalizedSchema = ReturnType; export async function configurationGenerator( tree: Tree, options: CypressE2EConfigSchema ) { const opts = normalizeOptions(tree, options); const tasks: GeneratorCallback[] = []; if (!installedCypressVersion()) { tasks.push(await cypressInitGenerator(tree, opts)); } if (opts.bundler === 'vite') { tasks.push(addDependenciesToPackageJson(tree, {}, { vite: viteVersion })); } await addFiles(tree, opts); addTarget(tree, opts); const linterTask = await addLinterToCyProject(tree, { ...opts, cypressDir: opts.directory, }); tasks.push(linterTask); if (!opts.skipFormat) { await formatFiles(tree); } return runTasksInSerial(...tasks); } function normalizeOptions(tree: Tree, options: CypressE2EConfigSchema) { const projectConfig = readProjectConfiguration(tree, options.project); if (projectConfig?.targets?.e2e) { throw new Error(`Project ${options.project} already has an e2e target. Rename or remove the existing e2e target.`); } if ( !options.baseUrl && !options.devServerTarget && !projectConfig.targets.serve ) { throw new Error(`The project ${options.project} does not have a 'serve' target. In this case you need to provide a devServerTarget,':[:]', or a baseUrl option`); } options.directory ??= 'src'; return { ...options, bundler: options.bundler ?? 'webpack', rootProject: projectConfig.root === '.', linter: options.linter ?? Linter.EsLint, devServerTarget: options.devServerTarget ?? (projectConfig.targets.serve ? `${options.project}:serve` : undefined), }; } async function addFiles(tree: Tree, options: NormalizedSchema) { const projectConfig = readProjectConfiguration(tree, options.project); const cyVersion = installedCypressVersion(); const filesToUse = cyVersion && cyVersion < 10 ? 'v9' : 'v10'; const hasTsConfig = tree.exists( joinPathFragments(projectConfig.root, 'tsconfig.json') ); const offsetFromProjectRoot = options.directory .split('/') .map((_) => '..') .join('/'); const fileOpts = { ...options, dir: options.directory ?? 'src', ext: options.js ? 'js' : 'ts', offsetFromRoot: offsetFromRoot(projectConfig.root), offsetFromProjectRoot, projectRoot: projectConfig.root, tsConfigPath: hasTsConfig ? `${offsetFromProjectRoot}/tsconfig.json` : getRelativePathToRootTsConfig(tree, projectConfig.root), tmpl: '', }; generateFiles( tree, join(__dirname, 'files', filesToUse), projectConfig.root, fileOpts ); if (filesToUse === 'v10') { addBaseCypressSetup(tree, { project: options.project, directory: options.directory, jsx: options.jsx, }); const cyFile = joinPathFragments(projectConfig.root, 'cypress.config.ts'); const updatedCyConfig = await addDefaultE2EConfig( tree.read(cyFile, 'utf-8'), { directory: options.directory, bundler: options.bundler, } ); tree.write(cyFile, updatedCyConfig); } if ( cyVersion && cyVersion < 7 && tree.exists( joinPathFragments(projectConfig.root, 'src', 'plugins', 'index.js') ) ) { updateJson(tree, join(projectConfig.root, 'cypress.json'), (json) => { json.pluginsFile = './src/plugins/index'; return json; }); } else if (cyVersion < 10) { const pluginPath = join(projectConfig.root, 'src/plugins/index.js'); if (tree.exists(pluginPath)) { tree.delete(pluginPath); } } if (options.js) { toJS(tree); } } function addTarget(tree: Tree, opts: NormalizedSchema) { const projectConfig = readProjectConfiguration(tree, opts.project); const cyVersion = installedCypressVersion(); projectConfig.targets.e2e = { executor: '@nx/cypress:cypress', options: { cypressConfig: joinPathFragments( projectConfig.root, cyVersion && cyVersion < 10 ? 'cypress.json' : 'cypress.config.ts' ), testingType: 'e2e', }, }; if (opts.baseUrl) { projectConfig.targets.e2e.options = { ...projectConfig.targets.e2e.options, baseUrl: opts.baseUrl, }; } else if (opts.devServerTarget) { const parsedTarget = parseTargetString(opts.devServerTarget); projectConfig.targets.e2e.options = { ...projectConfig.targets.e2e.options, devServerTarget: opts.devServerTarget, port: opts.port, }; const devServerProjectConfig = readProjectConfiguration( tree, parsedTarget.project ); // Add production e2e target if serve target is found if ( parsedTarget.configuration !== 'production' && devServerProjectConfig.targets?.[parsedTarget.target]?.configurations?.[ 'production' ] ) { projectConfig.targets.e2e.configurations ??= {}; projectConfig.targets.e2e.configurations['production'] = { devServerTarget: `${parsedTarget.project}:${parsedTarget.target}:production`, }; } // Add ci/static e2e target if serve target is found if (devServerProjectConfig.targets?.['serve-static']) { projectConfig.targets.e2e.configurations ??= {}; projectConfig.targets.e2e.configurations.ci = { devServerTarget: `${parsedTarget.project}:serve-static`, }; } } else { throw new Error('Either baseUrl or devServerTarget must be provided'); } updateProjectConfiguration(tree, opts.project, projectConfig); } export default configurationGenerator;