@nx/cypress
Version:
478 lines (477 loc) • 20.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createNodesV2 = exports.createNodes = void 0;
const devkit_1 = require("@nx/devkit");
const calculate_hash_for_create_nodes_1 = require("@nx/devkit/src/utils/calculate-hash-for-create-nodes");
const config_utils_1 = require("@nx/devkit/src/utils/config-utils");
const get_named_inputs_1 = require("@nx/devkit/src/utils/get-named-inputs");
const js_1 = require("@nx/js");
const fs_1 = require("fs");
const devkit_internals_1 = require("nx/src/devkit-internals");
const cache_directory_1 = require("nx/src/utils/cache-directory");
const workspace_context_1 = require("nx/src/utils/workspace-context");
const path_1 = require("path");
const constants_1 = require("../utils/constants");
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 defaultPatterns = {
e2e: {
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
excludeSpecPattern: '*.hot-update.js',
},
component: {
specPattern: '**/*.cy.{js,jsx,ts,tsx}',
excludeSpecPattern: ['/snapshots/*', '/image_snapshots/*'],
},
};
const pmc = (0, devkit_1.getPackageManagerCommand)();
exports.createNodes = [
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);
}
},
];
exports.createNodesV2 = exports.createNodes;
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 { specFiles, specPatterns, excludeSpecPatterns } = await getSpecFilesAndPatternsForTestType(cypressConfig, 'e2e', context.workspaceRoot, projectRoot);
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',
options: '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) {
const inputs = getInputs(namedInputs);
const outputs = getOutputs(projectRoot, cypressConfig, 'component');
// 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,
outputs,
metadata: {
technologies: ['cypress'],
description: 'Runs Cypress Component Tests',
help: {
command: `${pmc.exec} cypress run --help`,
example: {
args: ['--dev', '--headed'],
},
},
},
};
if (options.ciComponentTestingTargetName) {
const { specFiles, specPatterns, excludeSpecPatterns } = await getSpecFilesAndPatternsForTestType(cypressConfig, 'component', context.workspaceRoot, projectRoot);
const dependsOn = [];
const groupName = 'Component Testing (CI)';
metadata ??= {};
metadata.targetGroups ??= {};
metadata.targetGroups[groupName] ??= [];
const ctCiTargetGroup = metadata.targetGroups[groupName];
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.ciComponentTestingTargetName + '--' + relativeSpecFilePath;
const outputSubfolder = relativeSpecFilePath
.replace(/[\/\\]/g, '-')
.replace(/\./g, '-');
ctCiTargetGroup.push(targetName);
targets[targetName] = {
outputs: getTargetOutputs(outputs, outputSubfolder),
inputs,
cache: true,
command: `cypress run --component --spec ${relativeSpecFilePath} --config=${getTargetConfig(cypressConfig, outputSubfolder)}`,
options: {
cwd: projectRoot,
env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions },
},
// Cypress handles starting the server, there's no separate server
// target we can use as continuous, so we need to disable parallelism
// to avoid port conflicts
parallelism: false,
metadata: {
technologies: ['cypress'],
description: `Runs Cypress Component Tests for ${relativeSpecFilePath} in CI`,
help: {
command: `${pmc.exec} cypress run --help`,
example: {
args: ['--dev', '--headed'],
},
},
},
};
dependsOn.push({
target: targetName,
projects: 'self',
params: 'forward',
options: 'forward',
});
}
targets[options.ciComponentTestingTargetName] = {
executor: 'nx:noop',
cache: true,
inputs,
outputs,
dependsOn,
metadata: {
technologies: ['cypress'],
description: 'Runs Cypress Component Tests in CI',
nonAtomizedTarget: options.componentTestingTargetName,
help: {
command: `${pmc.exec} cypress run --help`,
example: {
args: ['--dev', '--headed'],
},
},
},
};
ctCiTargetGroup.push(options.ciComponentTestingTargetName);
}
}
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';
// must be explicitly provided to opt-in to atomized component testing
options.ciComponentTestingTargetName;
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;
}
async function getSpecFilesAndPatternsForTestType(cypressConfig, testType, workspaceRoot, projectRoot) {
const specPattern = cypressConfig[testType].specPattern ??
defaultPatterns[testType].specPattern;
const specPatterns = Array.isArray(specPattern)
? specPattern.map((p) => (0, path_1.join)(projectRoot, p))
: [(0, path_1.join)(projectRoot, specPattern)];
const excludeSpecPattern = cypressConfig[testType].excludeSpecPattern ??
defaultPatterns[testType].excludeSpecPattern;
const excludeSpecPatterns = Array.isArray(excludeSpecPattern)
? excludeSpecPattern.map((p) => (0, path_1.join)(projectRoot, p))
: [(0, path_1.join)(projectRoot, excludeSpecPattern)];
const specFiles = await (0, workspace_context_1.globWithWorkspaceContext)(workspaceRoot, specPatterns, excludeSpecPatterns);
return { specFiles, specPatterns, excludeSpecPatterns };
}