@nx/react
Version:
227 lines (226 loc) • 10.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.nxComponentTestingPreset = nxComponentTestingPreset;
const cypress_preset_1 = require("@nx/cypress/plugins/cypress-preset");
const devkit_1 = require("@nx/devkit");
const ct_helpers_1 = require("@nx/cypress/src/utils/ct-helpers");
const fs_1 = require("fs");
const path_1 = require("path");
/**
* React nx preset for Cypress Component Testing
*
* This preset contains the base configuration
* for your component tests that nx recommends.
* including a devServer that supports nx workspaces.
* you can easily extend this within your cypress config via spreading the preset
* @example
* export default defineConfig({
* component: {
* ...nxComponentTestingPreset(__dirname)
* // add your own config here
* }
* })
*
* @param pathToConfig will be used for loading project options and to construct the output paths for videos and screenshots
* @param options override options
*/
function nxComponentTestingPreset(pathToConfig, options) {
const basePresetSettings = (0, cypress_preset_1.nxBaseCypressPreset)(pathToConfig, {
testingType: 'component',
});
if (global.NX_GRAPH_CREATION) {
// this is only used by plugins, so we don't need the component testing
// options, cast to any to avoid type errors
return basePresetSettings;
}
const normalizedProjectRootPath = ['.ts', '.js'].some((ext) => pathToConfig.endsWith(ext))
? pathToConfig
: (0, path_1.dirname)(pathToConfig);
if (options?.bundler === 'vite') {
return {
...basePresetSettings,
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
devServer: {
...{ framework: 'react', bundler: 'vite' },
viteConfig: async () => {
const viteConfigPath = findViteConfig(normalizedProjectRootPath);
const { mergeConfig, loadConfigFromFile, searchForWorkspaceRoot } = await Function('return import("vite")')();
const resolved = await loadConfigFromFile({
mode: 'watch',
command: 'serve',
}, viteConfigPath);
return mergeConfig(resolved.config, {
server: {
fs: {
allow: [
searchForWorkspaceRoot(normalizedProjectRootPath),
devkit_1.workspaceRoot,
(0, devkit_1.joinPathFragments)(devkit_1.workspaceRoot, 'node_modules/vite'),
],
},
},
});
},
},
};
}
let webpackConfig;
try {
const graph = (0, devkit_1.readCachedProjectGraph)();
const { targets: ctTargets, name: ctProjectName } = (0, ct_helpers_1.getProjectConfigByPath)(graph, pathToConfig);
const ctTargetName = options?.ctTargetName || 'component-test';
const ctConfigurationName = process.env.NX_CYPRESS_TARGET_CONFIGURATION;
const ctExecutorContext = (0, ct_helpers_1.createExecutorContext)(graph, ctTargets, ctProjectName, ctTargetName, ctConfigurationName);
let buildTarget = options?.buildTarget;
if (!buildTarget) {
const ctExecutorOptions = (0, devkit_1.readTargetOptions)({
project: ctProjectName,
target: ctTargetName,
configuration: ctConfigurationName,
}, ctExecutorContext);
buildTarget = ctExecutorOptions.devServerTarget;
}
if (!buildTarget) {
throw new Error(`Unable to find the 'devServerTarget' executor option in the '${ctTargetName}' target of the '${ctProjectName}' project`);
}
webpackConfig = buildTargetWebpack(ctExecutorContext, buildTarget, ctProjectName);
}
catch (e) {
if (e instanceof InvalidExecutorError) {
throw e;
}
devkit_1.logger.warn((0, devkit_1.stripIndents) `Unable to build a webpack config with the project graph.
Falling back to default webpack config.`);
devkit_1.logger.warn(e);
const { buildBaseWebpackConfig } = require('./webpack-fallback');
webpackConfig = buildBaseWebpackConfig({
tsConfigPath: findTsConfig(normalizedProjectRootPath),
compiler: options?.compiler || 'babel',
});
}
return {
...basePresetSettings,
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
devServer: {
// cypress uses string union type,
// need to use const to prevent typing to string
// but don't want to use as const on webpackConfig
// so it is still user modifiable
...{ framework: 'react', bundler: 'webpack' },
webpackConfig,
},
};
}
/**
* apply the schema.json defaults from the @nx/web:webpack executor to the target options
*/
function withSchemaDefaults(target, context) {
const options = (0, devkit_1.readTargetOptions)(target, context);
options.compiler ??= 'babel';
options.deleteOutputPath ??= true;
options.vendorChunk ??= true;
options.commonChunk ??= true;
options.runtimeChunk ??= true;
options.sourceMap ??= true;
options.assets ??= [];
options.scripts ??= [];
options.styles ??= [];
options.budgets ??= [];
options.namedChunks ??= true;
options.outputHashing ??= 'none';
options.extractCss ??= true;
options.memoryLimit ??= 2048;
options.maxWorkers ??= 2;
options.fileReplacements ??= [];
options.buildLibsFromSource ??= true;
return options;
}
function buildTargetWebpack(ctx, buildTarget, componentTestingProjectName) {
const graph = ctx.projectGraph;
const parsed = (0, devkit_1.parseTargetString)(buildTarget, graph);
const buildableProjectConfig = graph.nodes[parsed.project]?.data;
const ctProjectConfig = graph.nodes[componentTestingProjectName]?.data;
if (!buildableProjectConfig || !ctProjectConfig) {
throw new Error((0, devkit_1.stripIndents) `Unable to load project configs from graph.
Using build target '${buildTarget}'
Has build config? ${!!buildableProjectConfig}
Has component config? ${!!ctProjectConfig}
`);
}
if (buildableProjectConfig.targets[parsed.target].executor !==
'@nx/webpack:webpack' &&
buildableProjectConfig.targets[parsed.target].executor !==
'@nx/rspack:rspack') {
throw new InvalidExecutorError(`The '${parsed.target}' target of the '${parsed.project}' project is not using the '@nx/webpack:webpack' or '@nx/rspack:rspack' executor. ` +
`Please make sure to use '@nx/webpack:webpack' or '@nx/rspack:rspack' executor in that target to use Cypress Component Testing.`);
}
const context = (0, ct_helpers_1.createExecutorContext)(graph, buildableProjectConfig.targets, parsed.project, parsed.target, parsed.target);
const { normalizeOptions, } = require('@nx/webpack/src/executors/webpack/lib/normalize-options');
const { resolveUserDefinedWebpackConfig, } = require('@nx/webpack/src/utils/webpack/resolve-user-defined-webpack-config');
const { composePluginsSync } = require('@nx/webpack/src/utils/config');
const { withNx } = require('@nx/webpack/src/utils/with-nx');
const { withWeb } = require('@nx/webpack/src/utils/with-web');
const options = normalizeOptions(withSchemaDefaults(parsed, context), devkit_1.workspaceRoot, buildableProjectConfig.root, buildableProjectConfig.sourceRoot);
let customWebpack;
if (options.webpackConfig) {
customWebpack = resolveUserDefinedWebpackConfig(options.webpackConfig, options.tsConfig.startsWith(context.root)
? options.tsConfig
: (0, path_1.join)(context.root, options.tsConfig));
}
return async () => {
customWebpack = await customWebpack;
// TODO(v22): Component testing need to be agnostic of the underlying executor. With Crystal, we're not using `@nx/webpack:webpack` by default.
// We need to decouple CT from the build target of the app, we just care about bundler config (e.g. webpack.config.js).
// The generated setup should support both Webpack and Vite as documented here: https://docs.cypress.io/guides/component-testing/react/overview
// Related issue: https://github.com/nrwl/nx/issues/21546
const configure = composePluginsSync(withNx(), withWeb());
const defaultWebpack = configure({}, {
options: {
...options,
// cypress will generate its own index.html from component-index.html
generateIndexHtml: false,
// causes issues with buildable libraries with ENOENT: no such file or directory, scandir error
extractLicenses: false,
root: devkit_1.workspaceRoot,
projectRoot: ctProjectConfig.root,
sourceRoot: ctProjectConfig.sourceRoot,
},
context,
});
if (customWebpack) {
return await customWebpack(defaultWebpack, {
options,
context,
configuration: parsed.configuration,
});
}
return defaultWebpack;
};
}
function findViteConfig(projectRootFullPath) {
const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'];
for (const ext of allowsExt) {
if ((0, fs_1.existsSync)((0, path_1.join)(projectRootFullPath, `vite.config.${ext}`))) {
return (0, path_1.join)(projectRootFullPath, `vite.config.${ext}`);
}
}
}
function findTsConfig(projectRoot) {
const potentialConfigs = [
'cypress/tsconfig.json',
'cypress/tsconfig.cy.json',
'tsconfig.cy.json',
];
for (const config of potentialConfigs) {
if ((0, fs_1.existsSync)((0, path_1.join)(projectRoot, config))) {
return config;
}
}
}
class InvalidExecutorError extends Error {
constructor(message) {
super(message);
this.message = message;
this.name = 'InvalidExecutorError';
}
}