UNPKG

@nx/cypress

Version:

The Nx Plugin for Cypress contains executors and generators allowing your workspace to use the powerful Cypress integration testing capabilities.

391 lines (390 loc) • 16.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createNodes = exports.createNodesV2 = void 0; const devkit_1 = require("@nx/devkit"); const path_1 = require("path"); const js_1 = require("@nx/js"); const get_named_inputs_1 = require("@nx/devkit/src/utils/get-named-inputs"); const fs_1 = require("fs"); const calculate_hash_for_create_nodes_1 = require("@nx/devkit/src/utils/calculate-hash-for-create-nodes"); const cache_directory_1 = require("nx/src/utils/cache-directory"); const constants_1 = require("../utils/constants"); const config_utils_1 = require("@nx/devkit/src/utils/config-utils"); const devkit_internals_1 = require("nx/src/devkit-internals"); const workspace_context_1 = require("nx/src/utils/workspace-context"); function readTargetsCache(cachePath) { try { return process.env.NX_CACHE_PROJECT_GRAPH !== 'false' ? (0, devkit_1.readJsonFile)(cachePath) : {}; } catch { return {}; } } function writeTargetsToCache(cachePath, results) { (0, devkit_1.writeJsonFile)(cachePath, results); } const cypressConfigGlob = '**/cypress.config.{js,ts,mjs,cjs}'; const pmc = (0, devkit_1.getPackageManagerCommand)(); exports.createNodesV2 = [ cypressConfigGlob, async (configFiles, options, context) => { const optionsHash = (0, devkit_internals_1.hashObject)(options); const cachePath = (0, path_1.join)(cache_directory_1.workspaceDataDirectory, `cypress-${optionsHash}.hash`); const targetsCache = readTargetsCache(cachePath); try { return await (0, devkit_1.createNodesFromFiles)((configFile, options, context) => createNodesInternal(configFile, options, context, targetsCache), configFiles, options, context); } finally { writeTargetsToCache(cachePath, targetsCache); } }, ]; /** * @deprecated This is replaced with {@link createNodesV2}. Update your plugin to export its own `createNodesV2` function that wraps this one instead. * This function will change to the v2 function in Nx 20. */ exports.createNodes = [ cypressConfigGlob, (configFile, options, context) => { devkit_1.logger.warn('`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'); return createNodesInternal(configFile, options, context, {}); }, ]; async function createNodesInternal(configFilePath, options, context, targetsCache) { options = normalizeOptions(options); const projectRoot = (0, path_1.dirname)(configFilePath); // Do not create a project if package.json and project.json isn't there. const siblingFiles = (0, fs_1.readdirSync)((0, path_1.join)(context.workspaceRoot, projectRoot)); if (!siblingFiles.includes('package.json') && !siblingFiles.includes('project.json')) { return {}; } const hash = await (0, calculate_hash_for_create_nodes_1.calculateHashForCreateNodes)(projectRoot, options, context, [(0, js_1.getLockFileName)((0, devkit_1.detectPackageManager)(context.workspaceRoot))]); targetsCache[hash] ??= await buildCypressTargets(configFilePath, projectRoot, options, context); const { targets, metadata } = targetsCache[hash]; const project = { projectType: 'application', targets, metadata, }; return { projects: { [projectRoot]: project, }, }; } function getTargetOutputs(outputs, subfolder) { return outputs.map((output) => subfolder ? (0, path_1.join)(output, subfolder) : output); } function getTargetConfig(cypressConfig, outputSubfolder, ciBaseUrl) { const config = {}; if (ciBaseUrl) { config['baseUrl'] = ciBaseUrl; } const { screenshotsFolder, videosFolder, e2e, component } = cypressConfig; if (videosFolder) { config['videosFolder'] = (0, path_1.join)(videosFolder, outputSubfolder); } if (screenshotsFolder) { config['screenshotsFolder'] = (0, path_1.join)(screenshotsFolder, outputSubfolder); } if (e2e) { config['e2e'] = {}; if (e2e.videosFolder) { config['e2e']['videosFolder'] = (0, path_1.join)(e2e.videosFolder, outputSubfolder); } if (e2e.screenshotsFolder) { config['e2e']['screenshotsFolder'] = (0, path_1.join)(e2e.screenshotsFolder, outputSubfolder); } } if (component) { config['component'] = {}; if (component.videosFolder) { config['component']['videosFolder'] = (0, path_1.join)(component.videosFolder, outputSubfolder); } if (component.screenshotsFolder) { config['component']['screenshotsFolder'] = (0, path_1.join)(component.screenshotsFolder, outputSubfolder); } } // Stringify twice to escape the quotes. return JSON.stringify(JSON.stringify(config)); } function getOutputs(projectRoot, cypressConfig, testingType) { function getOutput(path) { if (path.startsWith('..')) { return (0, devkit_1.joinPathFragments)('{workspaceRoot}', projectRoot, path); } else { return (0, devkit_1.joinPathFragments)('{projectRoot}', path); } } const { screenshotsFolder, videosFolder, e2e, component } = cypressConfig; const outputs = []; if (videosFolder) { outputs.push(getOutput(videosFolder)); } if (screenshotsFolder) { outputs.push(getOutput(screenshotsFolder)); } switch (testingType) { case 'e2e': { if (e2e.videosFolder) { outputs.push(getOutput(e2e.videosFolder)); } if (e2e.screenshotsFolder) { outputs.push(getOutput(e2e.screenshotsFolder)); } break; } case 'component': { if (component.videosFolder) { outputs.push(getOutput(component.videosFolder)); } if (component.screenshotsFolder) { outputs.push(getOutput(component.screenshotsFolder)); } break; } } return outputs; } async function buildCypressTargets(configFilePath, projectRoot, options, context) { const cypressConfig = await (0, config_utils_1.loadConfigFile)((0, path_1.join)(context.workspaceRoot, configFilePath)); const pluginPresetOptions = { ...cypressConfig.e2e?.[constants_1.NX_PLUGIN_OPTIONS], ...cypressConfig.env, ...cypressConfig.e2e?.env, }; const webServerCommands = pluginPresetOptions?.webServerCommands; const shouldReuseExistingServer = pluginPresetOptions?.reuseExistingServer ?? true; const namedInputs = (0, get_named_inputs_1.getNamedInputs)(projectRoot, context); const targets = {}; let metadata; const tsNodeCompilerOptions = JSON.stringify({ customConditions: null }); if ('e2e' in cypressConfig) { targets[options.targetName] = { command: `cypress run`, options: { cwd: projectRoot, env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions }, }, cache: true, inputs: getInputs(namedInputs), outputs: getOutputs(projectRoot, cypressConfig, 'e2e'), metadata: { technologies: ['cypress'], description: 'Runs Cypress Tests', help: { command: `${pmc.exec} cypress run --help`, example: { args: ['--dev', '--headed'], }, }, }, }; if (webServerCommands?.default) { const webServerCommandTask = shouldReuseExistingServer ? parseTaskFromCommand(webServerCommands.default) : null; if (webServerCommandTask) { targets[options.targetName].dependsOn = [ { projects: [webServerCommandTask.project], target: webServerCommandTask.target, }, ]; } else { targets[options.targetName].parallelism = false; } delete webServerCommands.default; } else { targets[options.targetName].parallelism = false; } if (Object.keys(webServerCommands ?? {}).length > 0) { targets[options.targetName].configurations ??= {}; for (const [configuration, webServerCommand] of Object.entries(webServerCommands ?? {})) { targets[options.targetName].configurations[configuration] = { command: `cypress run --env webServerCommand="${webServerCommand}"`, }; } } const ciWebServerCommand = pluginPresetOptions?.ciWebServerCommand; if (ciWebServerCommand) { const specPatterns = Array.isArray(cypressConfig.e2e.specPattern) ? cypressConfig.e2e.specPattern.map((p) => (0, path_1.join)(projectRoot, p)) : [(0, path_1.join)(projectRoot, cypressConfig.e2e.specPattern)]; const excludeSpecPatterns = !cypressConfig.e2e .excludeSpecPattern ? cypressConfig.e2e.excludeSpecPattern : Array.isArray(cypressConfig.e2e.excludeSpecPattern) ? cypressConfig.e2e.excludeSpecPattern.map((p) => (0, path_1.join)(projectRoot, p)) : [(0, path_1.join)(projectRoot, cypressConfig.e2e.excludeSpecPattern)]; const specFiles = await (0, workspace_context_1.globWithWorkspaceContext)(context.workspaceRoot, specPatterns, excludeSpecPatterns); const ciBaseUrl = pluginPresetOptions?.ciBaseUrl; const dependsOn = []; const outputs = getOutputs(projectRoot, cypressConfig, 'e2e'); const inputs = getInputs(namedInputs); const groupName = 'E2E (CI)'; metadata = { targetGroups: { [groupName]: [] } }; const ciTargetGroup = metadata.targetGroups[groupName]; const ciWebServerCommandTask = shouldReuseExistingServer ? parseTaskFromCommand(ciWebServerCommand) : null; for (const file of specFiles) { const relativeSpecFilePath = (0, devkit_1.normalizePath)((0, path_1.relative)(projectRoot, file)); if (relativeSpecFilePath.includes('../')) { throw new Error('@nx/cypress/plugin attempted to run tests outside of the project root. This is not supported and should not happen. Please open an issue at https://github.com/nrwl/nx/issues/new/choose with the following information:\n\n' + `\n\n${JSON.stringify({ projectRoot, relativeSpecFilePath, specFiles, context, excludeSpecPatterns, specPatterns, }, null, 2)}`); } const targetName = options.ciTargetName + '--' + relativeSpecFilePath; const outputSubfolder = relativeSpecFilePath .replace(/[\/\\]/g, '-') .replace(/\./g, '-'); ciTargetGroup.push(targetName); targets[targetName] = { outputs: getTargetOutputs(outputs, outputSubfolder), inputs, cache: true, command: `cypress run --env webServerCommand="${ciWebServerCommand}" --spec ${relativeSpecFilePath} --config=${getTargetConfig(cypressConfig, outputSubfolder, ciBaseUrl)}`, options: { cwd: projectRoot, env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions }, }, metadata: { technologies: ['cypress'], description: `Runs Cypress Tests in ${relativeSpecFilePath} in CI`, help: { command: `${pmc.exec} cypress run --help`, example: { args: ['--dev', '--headed'], }, }, }, }; dependsOn.push({ target: targetName, projects: 'self', params: 'forward', }); if (ciWebServerCommandTask) { targets[targetName].dependsOn = [ { target: ciWebServerCommandTask.target, projects: [ciWebServerCommandTask.project], }, ]; } else { targets[targetName].parallelism = false; } } targets[options.ciTargetName] = { executor: 'nx:noop', cache: true, inputs, outputs, dependsOn, metadata: { technologies: ['cypress'], description: 'Runs Cypress Tests in CI', nonAtomizedTarget: options.targetName, help: { command: `${pmc.exec} cypress run --help`, example: { args: ['--dev', '--headed'], }, }, }, }; if (!ciWebServerCommandTask) { targets[options.ciTargetName].parallelism = false; } ciTargetGroup.push(options.ciTargetName); } } if ('component' in cypressConfig) { // This will not override the e2e target if it is the same targets[options.componentTestingTargetName] ??= { command: `cypress run --component`, options: { cwd: projectRoot, env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions }, }, cache: true, inputs: getInputs(namedInputs), outputs: getOutputs(projectRoot, cypressConfig, 'component'), metadata: { technologies: ['cypress'], description: 'Runs Cypress Component Tests', help: { command: `${pmc.exec} cypress run --help`, example: { args: ['--dev', '--headed'], }, }, }, }; } targets[options.openTargetName] = { command: `cypress open`, options: { cwd: projectRoot, env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions }, }, metadata: { technologies: ['cypress'], description: 'Opens Cypress', help: { command: `${pmc.exec} cypress open --help`, example: { args: ['--dev', '--e2e'], }, }, }, }; return { targets, metadata }; } function normalizeOptions(options) { options ??= {}; options.targetName ??= 'e2e'; options.openTargetName ??= 'open-cypress'; options.componentTestingTargetName ??= 'component-test'; options.ciTargetName ??= 'e2e-ci'; return options; } function getInputs(namedInputs) { return [ ...('production' in namedInputs ? ['default', '^production'] : ['default', '^default']), { externalDependencies: ['cypress'], }, ]; } function parseTaskFromCommand(command) { const nxRunRegex = /^(?:(?:npx|yarn|bun|pnpm|pnpm exec|pnpx) )?nx run (\S+:\S+)$/; const infixRegex = /^(?:(?:npx|yarn|bun|pnpm|pnpm exec|pnpx) )?nx (\S+ \S+)$/; const nxRunMatch = command.match(nxRunRegex); if (nxRunMatch) { const [project, target] = nxRunMatch[1].split(':'); return { project, target }; } const infixMatch = command.match(infixRegex); if (infixMatch) { const [target, project] = infixMatch[1].split(' '); return { project, target }; } return null; }