@nx/playwright
Version:
387 lines (386 loc) • 16.1 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.createNodes = exports.createNodesV2 = void 0;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const devkit_1 = require("@nx/devkit");
const get_named_inputs_1 = require("@nx/devkit/src/utils/get-named-inputs");
const calculate_hash_for_create_nodes_1 = require("@nx/devkit/src/utils/calculate-hash-for-create-nodes");
const workspace_context_1 = require("nx/src/utils/workspace-context");
const minimatch_1 = require("minimatch");
const cache_directory_1 = require("nx/src/utils/cache-directory");
const js_1 = require("@nx/js");
const config_utils_1 = require("@nx/devkit/src/utils/config-utils");
const file_hasher_1 = require("nx/src/hasher/file-hasher");
const pmc = (0, devkit_1.getPackageManagerCommand)();
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 playwrightConfigGlob = '**/playwright.config.{js,ts,cjs,cts,mjs,mts}';
exports.createNodesV2 = [
playwrightConfigGlob,
async (configFilePaths, options, context) => {
const optionsHash = (0, file_hasher_1.hashObject)(options);
const cachePath = (0, node_path_1.join)(cache_directory_1.workspaceDataDirectory, `playwright-${optionsHash}.hash`);
const targetsCache = readTargetsCache(cachePath);
try {
return await (0, devkit_1.createNodesFromFiles)((configFile, options, context) => createNodesInternal(configFile, options, context, targetsCache), configFilePaths, 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 = [
playwrightConfigGlob,
async (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) {
const projectRoot = (0, node_path_1.dirname)(configFilePath);
// Do not create a project if package.json and project.json isn't there.
const siblingFiles = (0, node_fs_1.readdirSync)((0, node_path_1.join)(context.workspaceRoot, projectRoot));
if (!siblingFiles.includes('package.json') &&
!siblingFiles.includes('project.json')) {
return {};
}
const normalizedOptions = normalizeOptions(options);
const hash = await (0, calculate_hash_for_create_nodes_1.calculateHashForCreateNodes)(projectRoot, normalizedOptions, context, [(0, js_1.getLockFileName)((0, devkit_1.detectPackageManager)(context.workspaceRoot))]);
targetsCache[hash] ??= await buildPlaywrightTargets(configFilePath, projectRoot, normalizedOptions, context);
const { targets, metadata } = targetsCache[hash];
return {
projects: {
[projectRoot]: {
root: projectRoot,
targets,
metadata,
},
},
};
}
async function buildPlaywrightTargets(configFilePath, projectRoot, options, context) {
// Playwright forbids importing the `@playwright/test` module twice. This would affect running the tests,
// but we're just reading the config so let's delete the variable they are using to detect this.
// See: https://github.com/microsoft/playwright/pull/11218/files
delete process['__pw_initiator__'];
const playwrightConfig = await (0, config_utils_1.loadConfigFile)((0, node_path_1.join)(context.workspaceRoot, configFilePath));
const namedInputs = (0, get_named_inputs_1.getNamedInputs)(projectRoot, context);
const targets = {};
let metadata;
const testOutput = getTestOutput(playwrightConfig);
const reporterOutputs = getReporterOutputs(playwrightConfig);
const webserverCommandTasks = getWebserverCommandTasks(playwrightConfig);
const baseTargetConfig = {
command: 'playwright test',
options: {
cwd: '{projectRoot}',
},
metadata: {
technologies: ['playwright'],
description: 'Runs Playwright Tests',
help: {
command: `${pmc.exec} playwright test --help`,
example: {
options: {
workers: 1,
},
},
},
},
};
if (webserverCommandTasks.length) {
baseTargetConfig.dependsOn = getDependsOn(webserverCommandTasks);
}
else {
baseTargetConfig.parallelism = false;
}
targets[options.targetName] = {
...baseTargetConfig,
cache: true,
inputs: [
...('production' in namedInputs
? ['default', '^production']
: ['default', '^default']),
{ externalDependencies: ['@playwright/test'] },
],
outputs: getTargetOutputs(testOutput, reporterOutputs, context.workspaceRoot, projectRoot),
};
if (options.ciTargetName) {
const ciBaseTargetConfig = {
...baseTargetConfig,
cache: true,
inputs: [
...('production' in namedInputs
? ['default', '^production']
: ['default', '^default']),
{ externalDependencies: ['@playwright/test'] },
],
outputs: getTargetOutputs(testOutput, reporterOutputs, context.workspaceRoot, projectRoot),
};
const groupName = 'E2E (CI)';
metadata = { targetGroups: { [groupName]: [] } };
const ciTargetGroup = metadata.targetGroups[groupName];
const testDir = playwrightConfig.testDir
? (0, devkit_1.joinPathFragments)(projectRoot, playwrightConfig.testDir)
: projectRoot;
// Playwright defaults to the following pattern.
playwrightConfig.testMatch ??= '**/*.@(spec|test).?(c|m)[jt]s?(x)';
const dependsOn = [];
const testFiles = await getAllTestFiles({
context,
path: testDir,
config: playwrightConfig,
});
for (const testFile of testFiles) {
const outputSubfolder = (0, node_path_1.relative)(projectRoot, testFile)
.replace(/[\/\\]/g, '-')
.replace(/\./g, '-');
const relativeSpecFilePath = (0, devkit_1.normalizePath)((0, node_path_1.relative)(projectRoot, testFile));
if (relativeSpecFilePath.includes('../')) {
throw new Error('@nx/playwright/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,
testFile,
testFiles,
context,
config: playwrightConfig,
}, null, 2)}`);
}
const targetName = `${options.ciTargetName}--${relativeSpecFilePath}`;
ciTargetGroup.push(targetName);
targets[targetName] = {
...ciBaseTargetConfig,
options: {
...ciBaseTargetConfig.options,
env: getOutputEnvVars(reporterOutputs, outputSubfolder),
},
outputs: getTargetOutputs(testOutput, reporterOutputs, context.workspaceRoot, projectRoot, outputSubfolder),
command: `${baseTargetConfig.command} ${relativeSpecFilePath} --output=${(0, node_path_1.join)(testOutput, outputSubfolder)}`,
metadata: {
technologies: ['playwright'],
description: `Runs Playwright Tests in ${relativeSpecFilePath} in CI`,
help: {
command: `${pmc.exec} playwright test --help`,
example: {
options: {
workers: 1,
},
},
},
},
};
dependsOn.push({
target: targetName,
projects: 'self',
params: 'forward',
});
}
targets[options.ciTargetName] ??= {};
targets[options.ciTargetName] = {
executor: 'nx:noop',
cache: ciBaseTargetConfig.cache,
inputs: ciBaseTargetConfig.inputs,
outputs: ciBaseTargetConfig.outputs,
dependsOn,
metadata: {
technologies: ['playwright'],
description: 'Runs Playwright Tests in CI',
nonAtomizedTarget: options.targetName,
help: {
command: `${pmc.exec} playwright test --help`,
example: {
options: {
workers: 1,
},
},
},
},
};
if (!webserverCommandTasks.length) {
targets[options.ciTargetName].parallelism = false;
}
ciTargetGroup.push(options.ciTargetName);
}
return { targets, metadata };
}
async function getAllTestFiles(opts) {
const files = await (0, workspace_context_1.getFilesInDirectoryUsingContext)(opts.context.workspaceRoot, opts.path);
const matcher = createMatcher(opts.config.testMatch);
const ignoredMatcher = opts.config.testIgnore
? createMatcher(opts.config.testIgnore)
: () => false;
return files.filter((file) => matcher(file) && !ignoredMatcher(file));
}
function createMatcher(pattern) {
if (Array.isArray(pattern)) {
const matchers = pattern.map((p) => createMatcher(p));
return (path) => matchers.some((m) => m(path));
}
else if (pattern instanceof RegExp) {
return (path) => pattern.test(path);
}
else {
return (path) => {
try {
return (0, minimatch_1.minimatch)(path, pattern);
}
catch (e) {
throw new Error(`Error matching ${path} with ${pattern}: ${e.message}`);
}
};
}
}
function normalizeOptions(options) {
return {
...options,
targetName: options?.targetName ?? 'e2e',
ciTargetName: options?.ciTargetName ?? 'e2e-ci',
};
}
function getTestOutput(playwrightConfig) {
const { outputDir } = playwrightConfig;
if (outputDir) {
return outputDir;
}
else {
return './test-results';
}
}
function getReporterOutputs(playwrightConfig) {
const outputs = [];
const { reporter } = playwrightConfig;
if (reporter) {
const DEFAULT_REPORTER_OUTPUT = 'playwright-report';
if (reporter === 'html') {
outputs.push([reporter, DEFAULT_REPORTER_OUTPUT]);
}
else if (reporter === 'json') {
outputs.push([reporter, DEFAULT_REPORTER_OUTPUT]);
}
else if (Array.isArray(reporter)) {
for (const r of reporter) {
const [reporter, opts] = r;
// There are a few different ways to specify an output file or directory
// depending on the reporter. This is a best effort to find the output.
if (opts?.outputFile) {
outputs.push([reporter, opts.outputFile]);
}
else if (opts?.outputDir) {
outputs.push([reporter, opts.outputDir]);
}
else if (opts?.outputFolder) {
outputs.push([reporter, opts.outputFolder]);
}
else {
outputs.push([reporter, DEFAULT_REPORTER_OUTPUT]);
}
}
}
}
return outputs;
}
function getTargetOutputs(testOutput, reporterOutputs, workspaceRoot, projectRoot, subFolder) {
const outputs = new Set();
outputs.add(normalizeOutput(addSubfolderToOutput(testOutput, subFolder), workspaceRoot, projectRoot));
for (const [, output] of reporterOutputs) {
outputs.add(normalizeOutput(addSubfolderToOutput(output, subFolder), workspaceRoot, projectRoot));
}
return Array.from(outputs);
}
function addSubfolderToOutput(output, subfolder) {
if (!subfolder)
return output;
const parts = (0, node_path_1.parse)(output);
if (parts.ext !== '') {
return (0, node_path_1.join)(parts.dir, subfolder, parts.base);
}
return (0, node_path_1.join)(output, subfolder);
}
function getWebserverCommandTasks(playwrightConfig) {
if (!playwrightConfig.webServer) {
return [];
}
const tasks = [];
const webServer = Array.isArray(playwrightConfig.webServer)
? playwrightConfig.webServer
: [playwrightConfig.webServer];
for (const server of webServer) {
if (!server.reuseExistingServer) {
continue;
}
const task = parseTaskFromCommand(server.command);
if (task) {
tasks.push(task);
}
}
return tasks;
}
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;
}
function getDependsOn(tasks) {
const projectsPerTask = new Map();
for (const { project, target } of tasks) {
if (!projectsPerTask.has(target)) {
projectsPerTask.set(target, []);
}
projectsPerTask.get(target).push(project);
}
return Array.from(projectsPerTask.entries()).map(([target, projects]) => ({
projects,
target,
}));
}
function normalizeOutput(path, workspaceRoot, projectRoot) {
const fullProjectRoot = (0, node_path_1.resolve)(workspaceRoot, projectRoot);
const fullPath = (0, node_path_1.resolve)(fullProjectRoot, path);
const pathRelativeToProjectRoot = (0, devkit_1.normalizePath)((0, node_path_1.relative)(fullProjectRoot, fullPath));
if (pathRelativeToProjectRoot.startsWith('..')) {
return (0, devkit_1.joinPathFragments)('{workspaceRoot}', (0, node_path_1.relative)(workspaceRoot, fullPath));
}
return (0, devkit_1.joinPathFragments)('{projectRoot}', pathRelativeToProjectRoot);
}
function getOutputEnvVars(reporterOutputs, outputSubfolder) {
const env = {};
for (let [reporter, output] of reporterOutputs) {
if (outputSubfolder) {
const isFile = (0, node_path_1.parse)(output).ext !== '';
const envVarName = `PLAYWRIGHT_${reporter.toUpperCase()}_OUTPUT_${isFile ? 'FILE' : 'DIR'}`;
env[envVarName] = addSubfolderToOutput(output, outputSubfolder);
// Also set PLAYWRIGHT_HTML_REPORT for Playwright prior to 1.45.0.
// HTML prior to this version did not follow the pattern of "PLAYWRIGHT_<REPORTER>_OUTPUT_<FILE|DIR>".
if (reporter === 'html') {
env['PLAYWRIGHT_HTML_REPORT'] = env[envVarName];
}
}
}
return env;
}
;