UNPKG

@nx/react

Version:

The React plugin for Nx contains executors and generators for managing React applications and libraries within an Nx workspace. It provides: - Integration with libraries such as Jest, Vitest, Playwright, Cypress, and Storybook. - Generators for applica

227 lines (226 loc) • 10.3 kB
"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'; } }