chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
import {
|
||||
nxBaseCypressPreset,
|
||||
NxComponentTestingOptions,
|
||||
} from '@nx/cypress/plugins/cypress-preset';
|
||||
import {
|
||||
createExecutorContext,
|
||||
type CypressExecutorOptions,
|
||||
getProjectConfigByPath,
|
||||
} from '@nx/cypress/internal';
|
||||
import {
|
||||
ExecutorContext,
|
||||
joinPathFragments,
|
||||
logger,
|
||||
parseTargetString,
|
||||
readCachedProjectGraph,
|
||||
readTargetOptions,
|
||||
stripIndents,
|
||||
Target,
|
||||
workspaceRoot,
|
||||
} from '@nx/devkit';
|
||||
|
||||
import { getProjectSourceRoot } from '@nx/js/internal';
|
||||
import { existsSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
type ViteDevServer = {
|
||||
framework: 'react';
|
||||
bundler: 'vite';
|
||||
viteConfig?: any;
|
||||
};
|
||||
|
||||
type WebpackDevServer = {
|
||||
framework: 'react';
|
||||
bundler: 'webpack';
|
||||
webpackConfig?: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function nxComponentTestingPreset(
|
||||
pathToConfig: string,
|
||||
options?: NxComponentTestingOptions
|
||||
): {
|
||||
specPattern: string;
|
||||
devServer: ViteDevServer | WebpackDevServer;
|
||||
videosFolder: string;
|
||||
screenshotsFolder: string;
|
||||
chromeWebSecurity: boolean;
|
||||
} {
|
||||
const basePresetSettings = 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 as any;
|
||||
}
|
||||
|
||||
const normalizedProjectRootPath = ['.ts', '.js'].some((ext) =>
|
||||
pathToConfig.endsWith(ext)
|
||||
)
|
||||
? pathToConfig
|
||||
: dirname(pathToConfig);
|
||||
|
||||
if (options?.bundler === 'vite') {
|
||||
return {
|
||||
...basePresetSettings,
|
||||
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
|
||||
devServer: {
|
||||
...({ framework: 'react', bundler: 'vite' } as const),
|
||||
viteConfig: async () => {
|
||||
const viteConfigPath = findViteConfig(normalizedProjectRootPath);
|
||||
|
||||
// TODO(jack): Remove this cast when @nx/react switches to
|
||||
// moduleResolution: "nodenext". Vite 8 ships ESM-only type
|
||||
// declarations (.d.mts) not resolvable under moduleResolution: "node".
|
||||
const { mergeConfig, loadConfigFromFile, searchForWorkspaceRoot } =
|
||||
await (Function('return import("vite")')() as Promise<any>);
|
||||
|
||||
const resolved = await loadConfigFromFile(
|
||||
{
|
||||
mode: 'watch',
|
||||
command: 'serve',
|
||||
},
|
||||
viteConfigPath
|
||||
);
|
||||
return mergeConfig(resolved.config, {
|
||||
server: {
|
||||
fs: {
|
||||
allow: [
|
||||
searchForWorkspaceRoot(normalizedProjectRootPath),
|
||||
workspaceRoot,
|
||||
joinPathFragments(workspaceRoot, 'node_modules/vite'),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let webpackConfig: any = null;
|
||||
try {
|
||||
const graph = readCachedProjectGraph();
|
||||
const { targets: ctTargets, name: ctProjectName } = getProjectConfigByPath(
|
||||
graph,
|
||||
pathToConfig
|
||||
);
|
||||
const ctTargetName = options?.ctTargetName || 'component-test';
|
||||
const ctConfigurationName = process.env.NX_CYPRESS_TARGET_CONFIGURATION;
|
||||
|
||||
const ctExecutorContext = createExecutorContext(
|
||||
graph,
|
||||
ctTargets,
|
||||
ctProjectName,
|
||||
ctTargetName,
|
||||
ctConfigurationName
|
||||
);
|
||||
|
||||
let buildTarget: string = options?.buildTarget;
|
||||
if (!buildTarget) {
|
||||
const ctExecutorOptions = readTargetOptions<CypressExecutorOptions>(
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
stripIndents`Unable to build a webpack config with the project graph.
|
||||
Falling back to default webpack config.`
|
||||
);
|
||||
logger.warn(e);
|
||||
}
|
||||
|
||||
// Fallback config
|
||||
if (!webpackConfig) {
|
||||
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' } as const),
|
||||
webpackConfig,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* apply the schema.json defaults from the @nx/web:webpack executor to the target options
|
||||
*/
|
||||
function withSchemaDefaults(target: Target, context: ExecutorContext) {
|
||||
const options = 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: ExecutorContext,
|
||||
buildTarget: string,
|
||||
componentTestingProjectName: string
|
||||
) {
|
||||
const graph = ctx.projectGraph;
|
||||
const parsed = parseTargetString(buildTarget, graph);
|
||||
|
||||
const buildableProjectConfig = graph.nodes[parsed.project]?.data;
|
||||
const ctProjectConfig = graph.nodes[componentTestingProjectName]?.data;
|
||||
|
||||
if (!buildableProjectConfig || !ctProjectConfig) {
|
||||
throw new Error(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 = createExecutorContext(
|
||||
graph,
|
||||
buildableProjectConfig.targets,
|
||||
parsed.project,
|
||||
parsed.target,
|
||||
parsed.target
|
||||
);
|
||||
|
||||
const { resolveUserDefinedWebpackConfig } = require('@nx/webpack/internal');
|
||||
const {
|
||||
normalizeOptions,
|
||||
composePluginsSync,
|
||||
withNx,
|
||||
withWeb,
|
||||
} = require('@nx/webpack');
|
||||
|
||||
const options = normalizeOptions(
|
||||
withSchemaDefaults(parsed, context),
|
||||
workspaceRoot,
|
||||
buildableProjectConfig.root!,
|
||||
getProjectSourceRoot(buildableProjectConfig)
|
||||
);
|
||||
|
||||
let customWebpack: any;
|
||||
|
||||
if (options.webpackConfig) {
|
||||
customWebpack = resolveUserDefinedWebpackConfig(
|
||||
options.webpackConfig,
|
||||
options.tsConfig.startsWith(context.root)
|
||||
? options.tsConfig
|
||||
: join(context.root, options.tsConfig)
|
||||
);
|
||||
}
|
||||
|
||||
return async () => {
|
||||
customWebpack = await customWebpack;
|
||||
// For legacy `composePlugins(...)` setup, we need change some options to make Cypress CT work properly.
|
||||
if (
|
||||
customWebpack &&
|
||||
require('@nx/webpack').isNxWebpackComposablePlugin(customWebpack) // using inline since @nx/webpack may not be installed when using vite so top-level import would error
|
||||
) {
|
||||
return await customWebpack(
|
||||
{},
|
||||
{
|
||||
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: workspaceRoot,
|
||||
projectRoot: ctProjectConfig.root,
|
||||
sourceRoot: getProjectSourceRoot(ctProjectConfig),
|
||||
},
|
||||
context,
|
||||
configuration: parsed.configuration,
|
||||
}
|
||||
);
|
||||
} else if (
|
||||
typeof customWebpack === 'object' ||
|
||||
typeof customWebpack === 'function'
|
||||
) {
|
||||
// If this is a standard webpack config object or function, just return. it
|
||||
return customWebpack;
|
||||
}
|
||||
|
||||
return null; // return null to use fallback config
|
||||
};
|
||||
}
|
||||
|
||||
function findViteConfig(projectRootFullPath: string): string {
|
||||
const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'];
|
||||
|
||||
for (const ext of allowsExt) {
|
||||
if (existsSync(join(projectRootFullPath, `vite.config.${ext}`))) {
|
||||
return join(projectRootFullPath, `vite.config.${ext}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findTsConfig(projectRoot: string) {
|
||||
const potentialConfigs = [
|
||||
'cypress/tsconfig.json',
|
||||
'cypress/tsconfig.cy.json',
|
||||
'tsconfig.cy.json',
|
||||
];
|
||||
|
||||
for (const config of potentialConfigs) {
|
||||
if (existsSync(join(projectRoot, config))) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidExecutorError extends Error {
|
||||
constructor(public message: string) {
|
||||
super(message);
|
||||
this.name = 'InvalidExecutorError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
|
||||
import { Configuration } from 'webpack';
|
||||
import { getCSSModuleLocalIdent } from '@nx/webpack';
|
||||
import { resolvePathsBaseUrl } from '@nx/js';
|
||||
|
||||
export function buildBaseWebpackConfig({
|
||||
tsConfigPath = 'tsconfig.cy.json',
|
||||
compiler = 'babel',
|
||||
}: {
|
||||
tsConfigPath: string;
|
||||
compiler: 'swc' | 'babel';
|
||||
}): Configuration {
|
||||
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
|
||||
const config: Configuration = {
|
||||
target: 'web',
|
||||
resolve: {
|
||||
extensions,
|
||||
plugins: [
|
||||
new TsconfigPathsPlugin({
|
||||
configFile: tsConfigPath,
|
||||
baseUrl: resolvePathsBaseUrl(tsConfigPath),
|
||||
extensions,
|
||||
}) as never,
|
||||
],
|
||||
},
|
||||
mode: 'development',
|
||||
devtool: false,
|
||||
output: {
|
||||
publicPath: '/',
|
||||
chunkFilename: '[name].bundle.js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(bmp|png|jpe?g|gif|webp|avif)$/,
|
||||
type: 'asset',
|
||||
parser: {
|
||||
dataUrlCondition: {
|
||||
maxSize: 10_000, // 10 kB
|
||||
},
|
||||
},
|
||||
},
|
||||
CSS_MODULES_LOADER,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
if (compiler === 'swc') {
|
||||
config.module.rules.push({
|
||||
test: /\.([jt])sx?$/,
|
||||
loader: require.resolve('swc-loader'),
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
decorators: true,
|
||||
tsx: true,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'automatic',
|
||||
},
|
||||
},
|
||||
loose: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (compiler === 'babel') {
|
||||
config.module.rules.push({
|
||||
test: /\.(js|jsx|mjs|ts|tsx)$/,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
presets: [`@nx/react/babel`],
|
||||
rootMode: 'upward',
|
||||
babelrc: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
const loaderModulesOptions = {
|
||||
modules: {
|
||||
mode: 'local',
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
importLoaders: 1,
|
||||
};
|
||||
|
||||
const commonLoaders = [
|
||||
{
|
||||
loader: require.resolve('style-loader'),
|
||||
},
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: loaderModulesOptions,
|
||||
},
|
||||
];
|
||||
|
||||
const CSS_MODULES_LOADER = {
|
||||
test: /\.css$|\.scss$|\.sass$|\.less$/,
|
||||
oneOf: [
|
||||
{
|
||||
test: /\.module\.css$/,
|
||||
use: commonLoaders,
|
||||
},
|
||||
{
|
||||
test: /\.module\.(scss|sass)$/,
|
||||
use: [
|
||||
...commonLoaders,
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
sassOptions: {
|
||||
fiber: false,
|
||||
precision: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.module\.less$/,
|
||||
use: [
|
||||
...commonLoaders,
|
||||
{
|
||||
loader: require.resolve('less-loader'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user