feat(testing): cypress vite (#13474)

This commit is contained in:
Katerina Skroumpelou 2022-12-15 06:28:53 +02:00 committed by GitHub
parent 92d33f9539
commit 1ef01f8ccc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 727 additions and 103 deletions

View File

@ -111,10 +111,23 @@
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`."
},
"rootProject": {
"description": "Create a application at the root of the workspace",
"type": "boolean",
"default": false,
"hidden": true
},
"bundler": {
"description": "The Cypress bundler to use.",
"type": "string",
"enum": ["vite", "webpack", "none"],
"x-prompt": "Which Cypress bundler do you want to use?",
"default": "webpack"
}
},
"required": ["name"],
"examplesFile": "Adding Cypress to an existing application requires two options. The name of the e2e app to create and what project that e2e app is for.\n\n```bash\nnx g cypress-project --name=my-app-e2e --project=my-app\n```\n\nWhen providing `--project` option, the generator will look for the `serve` target in that given project. This allows the [cypress executor](/packages/cypress/executors/cypress) to spin up the project and start the cypress runner.\n\nIf you prefer to not have the project served automatically, you can provide a `--base-url` argument in place of `--project`\n\n```bash\nnx g cypress-project --name=my-app-e2e --base-url=http://localhost:1234\n```\n\n{% callout type=\"note\" title=\"What about API Projects?\" %}\nYou can also run the `cypress-project` generator against API projects like a [Nest API](/packages/nest/generators/application#@nrwl/nest:application).\nIf there is a URL to visit then you can test it with Cypress!\n{% /callout %}\n",
"examplesFile": "Adding Cypress to an existing application requires two options. The name of the e2e app to create and what project that e2e app is for.\n\n```bash\nnx g cypress-project --name=my-app-e2e --project=my-app\n```\n\nWhen providing `--project` option, the generator will look for the `serve` target in that given project. This allows the [cypress executor](/packages/cypress/executors/cypress) to spin up the project and start the cypress runner.\n\nIf you prefer to not have the project served automatically, you can provide a `--base-url` argument in place of `--project`\n\n```bash\nnx g cypress-project --name=my-app-e2e --base-url=http://localhost:1234\n```\n\n{% callout type=\"note\" title=\"What about API Projects?\" %}\nYou can also run the `cypress-project` generator against API projects like a [Nest API](/packages/nest/generators/application#@nrwl/nest:application).\nIf there is a URL to visit then you can test it with Cypress!\n{% /callout %}\n\n## Using Cypress with Vite.js\n\nNow, you can generate your Cypress project with Vite.js as the bundler:\n\n```bash\nnx g cypress-project --name=my-app-e2e --project=my-app --bundler=vite\n```\n\nThis generator will pass the `bundler` information (`bundler: 'vite'`) to our `nxE2EPreset`, in your project's `cypress.config.ts` file (eg. `my-app-e2e/cypress.config.ts`).\n\n### Customizing the Vite.js configuration\n\nThe `nxE2EPreset` will then use the `bundler` information to generate the correct settings for your Cypress project to use Vite.js. In the background, the way this works is that it's using a custom Vite preprocessor for your files, that's called on the `file:preprocessor` event. If you want to customize this behaviour, you can do so like this in your project's `cypress.config.ts` file:\n\n```ts\nimport { defineConfig } from 'cypress';\nimport { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';\n\nconst config = nxE2EPreset(__filename, { bundler: 'vite' });\nexport default defineConfig({\n e2e: {\n ...config,\n setupNodeEvents(on, config): {\n config.setupNodeEvents(on);\n // Your settings here\n }\n },\n});\n```\n",
"presets": []
},
"description": "Add a Cypress E2E Project.",

File diff suppressed because one or more lines are too long

View File

@ -114,6 +114,7 @@
},
"bundler": {
"description": "The bundler to use.",
"type": "string",
"enum": ["vite", "webpack"],
"x-prompt": "Which bundler do you want to use?",
"default": "webpack"
@ -196,7 +197,10 @@
},
"bundler": {
"description": "The Storybook builder to use.",
"enum": ["vite", "webpack"]
"type": "string",
"enum": ["vite", "webpack"],
"x-prompt": "Which Storybook builder do you want to use?",
"default": "webpack"
}
},
"required": ["name"],

View File

@ -85,7 +85,7 @@ ${e.stack ? e.stack : e}`
const offset = offsetFromRoot(normalizedFromWorkspaceRootPath);
const buildContext = createExecutorContext(
graph,
graph.nodes[buildTarget.project].data.targets,
graph.nodes[buildTarget.project]?.data.targets,
buildTarget.project,
buildTarget.target,
buildTarget.configuration
@ -117,7 +117,7 @@ ${e.stack ? e.stack : e}`
function getBuildableTarget(ctContext: ExecutorContext) {
const targets =
ctContext.projectGraph.nodes[ctContext.projectName].data?.targets;
ctContext.projectGraph.nodes[ctContext.projectName]?.data?.targets;
const targetConfig = targets?.[ctContext.targetName];
if (!targetConfig) {
@ -232,7 +232,7 @@ function normalizeBuildTargetOptions(
buildOptions.stylePreprocessorOptions = { includePaths: [] };
}
const { root, sourceRoot } =
buildContext.projectGraph.nodes[buildContext.projectName].data;
buildContext.projectGraph.nodes[buildContext.projectName]?.data;
return {
root: joinPathFragments(offset, root),
sourceRoot: joinPathFragments(offset, sourceRoot),
@ -280,7 +280,7 @@ function withSchemaDefaults(options: any): BrowserBuilderSchema {
function getTempStylesForTailwind(ctExecutorContext: ExecutorContext) {
const ctProjectConfig = ctExecutorContext.projectGraph.nodes[
ctExecutorContext.projectName
].data as ProjectConfiguration;
]?.data as ProjectConfiguration;
// angular only supports `tailwind.config.{js,cjs}`
const ctProjectTailwindConfig = join(
ctExecutorContext.root,

View File

@ -13,6 +13,15 @@ export default defineConfig({
});"
`;
exports[`e2e migrator cypress with project root at "" cypress version >=10 should create a cypress.config.ts file when it does not exist 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname)
});"
`;
exports[`e2e migrator cypress with project root at "" cypress version >=10 should keep paths in the e2e config when they differ from the nx preset defaults 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
@ -41,11 +50,11 @@ exports[`e2e migrator cypress with project root at "" cypress version >=10 shoul
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
export default defineConfig({
e2e: {...nxE2EPreset(__dirname),
baseUrl: 'http://localhost:4200'
},
});"
});"
`;
exports[`e2e migrator cypress with project root at "" cypress version >=10 should update paths in the config 1`] = `
@ -79,6 +88,15 @@ export default defineConfig({
});"
`;
exports[`e2e migrator cypress with project root at "projects/app1" cypress version >=10 should create a cypress.config.ts file when it does not exist 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname)
});"
`;
exports[`e2e migrator cypress with project root at "projects/app1" cypress version >=10 should keep paths in the e2e config when they differ from the nx preset defaults 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
@ -107,11 +125,11 @@ exports[`e2e migrator cypress with project root at "projects/app1" cypress versi
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
export default defineConfig({
e2e: {...nxE2EPreset(__dirname),
baseUrl: 'http://localhost:4200'
},
});"
});"
`;
exports[`e2e migrator cypress with project root at "projects/app1" cypress version >=10 should update paths in the config 1`] = `

View File

@ -840,13 +840,8 @@ describe('e2e migrator', () => {
'apps/app1-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toBe(`import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname)
});
`);
expect(cypressConfig).toMatchSnapshot();
});
it('should update e2e config with the nx preset', async () => {
@ -856,11 +851,11 @@ export default defineConfig({
joinPathFragments(root, 'cypress.config.ts'),
`import { defineConfig } from 'cypress';
export default defineConfig({
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:4200'
},
});`
});`
);
const project = addProject('app1', {
root,

View File

@ -16,3 +16,33 @@ nx g cypress-project --name=my-app-e2e --base-url=http://localhost:1234
You can also run the `cypress-project` generator against API projects like a [Nest API](/packages/nest/generators/application#@nrwl/nest:application).
If there is a URL to visit then you can test it with Cypress!
{% /callout %}
## Using Cypress with Vite.js
Now, you can generate your Cypress project with Vite.js as the bundler:
```bash
nx g cypress-project --name=my-app-e2e --project=my-app --bundler=vite
```
This generator will pass the `bundler` information (`bundler: 'vite'`) to our `nxE2EPreset`, in your project's `cypress.config.ts` file (eg. `my-app-e2e/cypress.config.ts`).
### Customizing the Vite.js configuration
The `nxE2EPreset` will then use the `bundler` information to generate the correct settings for your Cypress project to use Vite.js. In the background, the way this works is that it's using a custom Vite preprocessor for your files, that's called on the `file:preprocessor` event. If you want to customize this behaviour, you can do so like this in your project's `cypress.config.ts` file:
```ts
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
const config = nxE2EPreset(__filename, { bundler: 'vite' });
export default defineConfig({
e2e: {
...config,
setupNodeEvents(on, config): {
config.setupNodeEvents(on);
// Your settings here
}
},
});
```

View File

@ -2,6 +2,8 @@ import { workspaceRoot } from '@nrwl/devkit';
import { dirname, join, relative } from 'path';
import { lstatSync } from 'fs';
import vitePreprocessor from '../src/plugins/preprocessor-vite';
interface BaseCypressPreset {
videosFolder: string;
screenshotsFolder: string;
@ -15,8 +17,10 @@ export interface NxComponentTestingOptions {
* this is only when customized away from the default value of `component-test`
* @example 'component-test'
*/
ctTargetName: string;
ctTargetName?: string;
bundler?: 'vite' | 'webpack';
}
export function nxBaseCypressPreset(pathToConfig: string): BaseCypressPreset {
// prevent from placing path outside the root of the workspace
// if they pass in a file or directory
@ -58,12 +62,25 @@ export function nxBaseCypressPreset(pathToConfig: string): BaseCypressPreset {
*
* @param pathToConfig will be used to construct the output paths for videos and screenshots
*/
export function nxE2EPreset(pathToConfig: string) {
return {
export function nxE2EPreset(
pathToConfig: string,
options?: { bundler?: string }
) {
const baseConfig = {
...nxBaseCypressPreset(pathToConfig),
fileServerFolder: '.',
supportFile: 'src/support/e2e.ts',
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
fixturesFolder: 'src/fixtures',
};
if (options?.bundler === 'vite') {
return {
...baseConfig,
setupNodeEvents(on) {
on('file:preprocessor', vitePreprocessor());
},
};
}
return baseConfig;
}

View File

@ -193,14 +193,26 @@ Object {
}
`;
exports[`Cypress Project > v10 for bundler:vite should pass the bundler info to nxE2EPreset in \`cypress.config.ts\` 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname,
{
bundler: 'vite'
}
)
});"
`;
exports[`Cypress Project > v10 nested should set right path names in \`cypress.config.ts\` 1`] = `
"import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname)
});
"
});"
`;
exports[`Cypress Project > v10 nested should set right path names in \`tsconfig.e2e.json\` 1`] = `
@ -243,8 +255,7 @@ import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname)
});
"
});"
`;
exports[`Cypress Project > v10 should set right path names in \`tsconfig.e2e.json\` 1`] = `

View File

@ -189,6 +189,22 @@ describe('Cypress Project', () => {
expect(tsConfig.extends).toBe('../../tsconfig.json');
});
describe('for bundler:vite', () => {
it('should pass the bundler info to nxE2EPreset in `cypress.config.ts`', async () => {
await cypressProjectGenerator(tree, {
...defaultOptions,
name: 'my-app-e2e',
project: 'my-app',
bundler: 'vite',
});
const cypressConfig = tree.read(
'apps/my-app-e2e/cypress.config.ts',
'utf-8'
);
expect(cypressConfig).toMatchSnapshot();
});
});
describe('nested', () => {
it('should set right path names in `cypress.config.ts`', async () => {
await cypressProjectGenerator(tree, {

View File

@ -5,6 +5,7 @@ import {
extractLayoutDirectory,
formatFiles,
generateFiles,
getProjects,
getWorkspaceLayout,
joinPathFragments,
logger,
@ -16,7 +17,6 @@ import {
toJS,
Tree,
updateJson,
getProjects,
} from '@nrwl/devkit';
import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
@ -32,6 +32,7 @@ import { filePathPrefix } from '../../utils/project-name';
import {
cypressVersion,
eslintPluginCypressVersion,
viteVersion,
} from '../../utils/versions';
import { cypressInitGenerator } from '../init/init';
// app
@ -63,6 +64,7 @@ function createFiles(tree: Tree, options: CypressProjectSchema) {
tree,
options.projectRoot
),
bundler: options.bundler,
}
);
@ -271,6 +273,19 @@ export async function cypressProjectGenerator(host: Tree, schema: Schema) {
if (!cypressVersion) {
tasks.push(cypressInitGenerator(host, options));
}
if (schema.bundler === 'vite') {
tasks.push(
addDependenciesToPackageJson(
host,
{},
{
vite: viteVersion,
}
)
);
}
createFiles(host, options);
addProject(host, options);
const installTask = await addLinter(host, options);
@ -320,6 +335,7 @@ function normalizeOptions(host: Tree, options: Schema): CypressProjectSchema {
}
options.linter = options.linter || Linter.EsLint;
options.bundler = options.bundler || 'webpack';
return {
...options,
// other generators depend on the rootProject flag down stream

View File

@ -2,5 +2,9 @@ import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: nxE2EPreset(__dirname)
e2e: nxE2EPreset(__dirname<% if (bundler === 'vite'){ %>,
{
bundler: 'vite'
}
<% } %>)
});

View File

@ -12,4 +12,5 @@ export interface Schema {
standaloneConfig?: boolean;
skipPackageJson?: boolean;
rootProject?: boolean;
bundler?: 'webpack' | 'vite' | 'none';
}

View File

@ -59,6 +59,19 @@
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`."
},
"rootProject": {
"description": "Create a application at the root of the workspace",
"type": "boolean",
"default": false,
"hidden": true
},
"bundler": {
"description": "The Cypress bundler to use.",
"type": "string",
"enum": ["vite", "webpack", "none"],
"x-prompt": "Which Cypress bundler do you want to use?",
"default": "webpack"
}
},
"required": ["name"],

View File

@ -0,0 +1,89 @@
// Adapted from: https://github.com/mammadataei/cypress-vite
import * as path from 'path';
import type { RollupOutput, RollupWatcher, WatcherOptions } from 'rollup';
type CypressPreprocessor = (
file: Record<string, any>
) => string | Promise<string>;
/**
* Cypress preprocessor for running e2e tests using vite.
*
* @param {string} userConfigPath
* @example
* setupNodeEvents(on) {
* on(
* 'file:preprocessor',
* vitePreprocessor(path.resolve(__dirname, './vite.config.ts')),
* )
* },
*/
function vitePreprocessor(userConfigPath?: string): CypressPreprocessor {
return async (file) => {
const { outputPath, filePath, shouldWatch } = file;
const fileName = path.basename(outputPath);
const filenameWithoutExtension = path.basename(
outputPath,
path.extname(outputPath)
);
const defaultConfig = {
logLevel: 'silent',
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
build: {
emptyOutDir: false,
minify: false,
outDir: path.dirname(outputPath),
sourcemap: true,
write: true,
watch: getWatcherConfig(shouldWatch),
lib: {
entry: filePath,
fileName: () => fileName,
formats: ['umd'],
name: filenameWithoutExtension,
},
},
};
const { build } = require('vite');
const watcher = await build({
configFile: userConfigPath,
...defaultConfig,
});
if (shouldWatch && isWatcher(watcher)) {
watcher.on('event', (event) => {
if (event.code === 'END') {
file.emit('rerun');
}
if (event.code === 'ERROR') {
console.error(event);
}
});
file.on('close', () => {
watcher.close();
});
}
return outputPath;
};
}
function getWatcherConfig(shouldWatch: boolean): WatcherOptions | null {
return shouldWatch ? {} : null;
}
type BuildResult = RollupWatcher | RollupOutput | RollupOutput[];
function isWatcher(watcher: BuildResult): watcher is RollupWatcher {
return (watcher as RollupWatcher).on !== undefined;
}
export default vitePreprocessor;

View File

@ -4,3 +4,4 @@ export const typesNodeVersion = '16.11.7';
export const cypressVersion = '^11.0.0';
export const cypressWebpackVersion = '^2.0.0';
export const webpackHttpPluginVersion = '^5.5.0';
export const viteVersion = '^4.0.1';

View File

@ -16,12 +16,29 @@ nx g @nrwl/react:cypress-component-project --project=my-cool-react-project
Running this generator, adds the required files to the specified project with a preconfigured `cypress.config.ts` designed for Nx workspaces.
The following file will be added to projects where the Component Testing build target is using `webpack` for bundling:
```ts {% fileName="cypress.config.ts" %}
import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename),
component: nxComponentTestingPreset(__filename, {
bundler: 'webpack',
}),
});
```
The following file will be added to projects where the Component Testing build target is using `vite` for bundling:
```ts {% fileName="cypress.config.ts" %}
import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename, {
bundler: 'vite',
}),
});
```
@ -33,12 +50,20 @@ import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing'
export default defineConfig({
component: {
...nxComponentTestingPreset(__filename),
...nxComponentTestingPreset(__filename, {
bundler: 'webpack',
}),
// extra options here
},
});
```
## The `bundler` option
Component testing supports two different bundlers: `webpack` and `vite`. The Nx generator will pick up the bundler used in the specified project's build target. If the build target is using `@nrwl/webpack:webpack`, then the generator will use `webpack` as the bundler. If the build target is using `@nrwl/vite:build`, then the generator will use `vite` as the bundler.
You can manually set the bundler by passing `--bundler=webpack` or `--bundler=vite` to the generator, but that is not needed since the generator will pick up the correct bundler for you. However, if you want to use a different bundler than the one that is used in the build target, then you can manually set it using that flag.
## Specifying a Build Target
Component testing requires a _build target_ to correctly run the component test dev server. This option can be manually specified with `--build-target=some-react-app:build`, but Nx will infer this usage from the [project graph](/concepts/mental-model#the-project-graph) if one isn't provided.

View File

@ -14,11 +14,6 @@ import {
Target,
workspaceRoot,
} from '@nrwl/devkit';
import type { WebpackExecutorOptions } from '@nrwl/webpack/src/executors/webpack/schema';
import { normalizeOptions } from '@nrwl/webpack/src/executors/webpack/lib/normalize-options';
import { getWebpackConfig } from '@nrwl/webpack/src/executors/webpack/lib/get-webpack-config';
import { resolveCustomWebpackConfig } from '@nrwl/webpack/src/utils/webpack/custom-webpack';
import { buildBaseWebpackConfig } from './webpack-fallback';
import {
createExecutorContext,
getProjectConfigByPath,
@ -45,7 +40,29 @@ import {
export function nxComponentTestingPreset(
pathToConfig: string,
options?: NxComponentTestingOptions
) {
): {
specPattern: string;
devServer: {
framework?: 'react';
bundler?: 'vite' | 'webpack';
viteConfig?: any;
webpackConfig?: any;
};
videosFolder: string;
screenshotsFolder: string;
video: boolean;
chromeWebSecurity: boolean;
} {
if (options.bundler === 'vite') {
return {
...nxBaseCypressPreset(pathToConfig),
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
devServer: {
...({ framework: 'react', bundler: 'vite' } as const),
},
};
}
let webpackConfig;
try {
const graph = readCachedProjectGraph();
@ -88,11 +105,14 @@ export function nxComponentTestingPreset(
Falling back to default webpack config.`
);
logger.warn(e);
const { buildBaseWebpackConfig } = require('./webpack-fallback');
webpackConfig = buildBaseWebpackConfig({
tsConfigPath: 'cypress/tsconfig.cy.json',
compiler: 'babel',
});
}
return {
...nxBaseCypressPreset(pathToConfig),
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
@ -109,11 +129,8 @@ export function nxComponentTestingPreset(
/**
* apply the schema.json defaults from the @nrwl/web:webpack executor to the target options
*/
function withSchemaDefaults(
target: Target,
context: ExecutorContext
): WebpackExecutorOptions {
const options = readTargetOptions<WebpackExecutorOptions>(target, context);
function withSchemaDefaults(target: Target, context: ExecutorContext) {
const options = readTargetOptions(target, context);
options.compiler ??= 'babel';
options.deleteOutputPath ??= true;
@ -161,6 +178,16 @@ function buildTargetWebpack(
parsed.target
);
const {
normalizeOptions,
} = require('@nrwl/webpack/src/executors/webpack/lib/normalize-options');
const {
resolveCustomWebpackConfig,
} = require('@nrwl/webpack/src/utils/webpack/custom-webpack');
const {
getWebpackConfig,
} = require('@nrwl/webpack/src/executors/webpack/lib/get-webpack-config');
const options = normalizeOptions(
withSchemaDefaults(parsed, context),
workspaceRoot,

View File

@ -1,6 +1,6 @@
import { getCSSModuleLocalIdent } from '@nrwl/webpack/src/executors/webpack/lib/get-webpack-config';
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import { Configuration } from 'webpack';
import { getCSSModuleLocalIdent } from '@nrwl/webpack/src/executors/webpack/lib/get-webpack-config';
export function buildBaseWebpackConfig({
tsConfigPath = 'tsconfig.cy.json',

View File

@ -1,5 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`React:CypressComponentTestConfiguration should generate cypress component test config with --build-target 1`] = `
"import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename, {
bundler: 'vite'
}) as any,
});"
`;
exports[`React:CypressComponentTestConfiguration should generate cypress component test config with project graph 1`] = `
"import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename, {
bundler: 'vite'
}) as any,
});"
`;
exports[`React:CypressComponentTestConfiguration should generate cypress component test config with webpack 1`] = `
"import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename, {
bundler: 'webpack'
}) as any,
});"
`;
exports[`React:CypressComponentTestConfiguration should generate cypress config with vite 1`] = `
"import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename, {
bundler: 'vite'
}) as any,
});"
`;
exports[`React:CypressComponentTestConfiguration should generate tests for existing js components 1`] = `
"import * as React from 'react'
import SomeCmp from './some-cmp'

View File

@ -30,6 +30,63 @@ describe('React:CypressComponentTestConfiguration', () => {
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should generate cypress config with vite', async () => {
mockedAssertCypressVersion.mockReturnValue();
await applicationGenerator(tree, {
e2eTestRunner: 'none',
linter: Linter.EsLint,
skipFormat: true,
style: 'scss',
unitTestRunner: 'none',
name: 'my-app',
bundler: 'vite',
});
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
projectGraph = {
nodes: {
'my-app': {
name: 'my-app',
type: 'app',
data: {
...readProjectConfiguration(tree, 'my-app'),
},
},
'some-lib': {
name: 'some-lib',
type: 'lib',
data: {
...readProjectConfiguration(tree, 'some-lib'),
},
},
},
dependencies: {
'my-app': [
{ type: DependencyType.static, source: 'my-app', target: 'some-lib' },
],
},
};
await cypressComponentConfigGenerator(tree, {
project: 'some-lib',
generateTests: false,
buildTarget: 'my-app:build',
});
const config = tree.read('libs/some-lib/cypress.config.ts', 'utf-8');
expect(config).toMatchSnapshot();
});
it('should generate cypress component test config with --build-target', async () => {
mockedAssertCypressVersion.mockReturnValue();
@ -83,12 +140,7 @@ describe('React:CypressComponentTestConfiguration', () => {
});
const config = tree.read('libs/some-lib/cypress.config.ts', 'utf-8');
expect(config).toContain(
"import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing"
);
expect(config).toContain(
'component: nxComponentTestingPreset(__filename),'
);
expect(config).toMatchSnapshot();
expect(
readProjectConfiguration(tree, 'some-lib').targets['component-test']
@ -154,12 +206,7 @@ describe('React:CypressComponentTestConfiguration', () => {
});
const config = tree.read('libs/some-lib/cypress.config.ts', 'utf-8');
expect(config).toContain(
"import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing"
);
expect(config).toContain(
'component: nxComponentTestingPreset(__filename),'
);
expect(config).toMatchSnapshot();
expect(
readProjectConfiguration(tree, 'some-lib').targets['component-test']
@ -174,6 +221,71 @@ describe('React:CypressComponentTestConfiguration', () => {
});
});
it('should generate cypress component test config with webpack', async () => {
mockedAssertCypressVersion.mockReturnValue();
await applicationGenerator(tree, {
e2eTestRunner: 'none',
linter: Linter.EsLint,
skipFormat: true,
style: 'scss',
unitTestRunner: 'none',
name: 'my-app',
bundler: 'webpack',
});
await libraryGenerator(tree, {
linter: Linter.EsLint,
name: 'some-lib',
skipFormat: true,
skipTsConfig: false,
style: 'scss',
unitTestRunner: 'none',
component: true,
});
projectGraph = {
nodes: {
'my-app': {
name: 'my-app',
type: 'app',
data: {
...readProjectConfiguration(tree, 'my-app'),
},
},
'some-lib': {
name: 'some-lib',
type: 'lib',
data: {
...readProjectConfiguration(tree, 'some-lib'),
},
},
},
dependencies: {
'my-app': [
{ type: DependencyType.static, source: 'my-app', target: 'some-lib' },
],
},
};
await cypressComponentConfigGenerator(tree, {
project: 'some-lib',
generateTests: false,
});
const config = tree.read('libs/some-lib/cypress.config.ts', 'utf-8');
expect(config).toMatchSnapshot();
expect(
readProjectConfiguration(tree, 'some-lib').targets['component-test']
).toEqual({
executor: '@nrwl/cypress:cypress',
options: {
cypressConfig: 'libs/some-lib/cypress.config.ts',
devServerTarget: 'my-app:build',
skipServe: true,
testingType: 'component',
},
});
});
it('should generate tests for existing tsx components', async () => {
mockedAssertCypressVersion.mockReturnValue();
await applicationGenerator(tree, {

View File

@ -6,7 +6,7 @@ import {
} from '@nrwl/devkit';
import { nxVersion } from '../../utils/versions';
import { addFiles } from './lib/add-files';
import { updateProjectConfig } from './lib/update-configs';
import { FoundTarget, updateProjectConfig } from './lib/update-configs';
import { CypressComponentConfigurationSchema } from './schema.d';
/**
@ -26,8 +26,8 @@ export async function cypressComponentConfigGenerator(
skipFormat: true,
});
await updateProjectConfig(tree, options);
await addFiles(tree, projectConfig, options);
const found: FoundTarget = await updateProjectConfig(tree, options);
await addFiles(tree, projectConfig, options, found);
if (options.skipFormat) {
await formatFiles(tree);
}

View File

@ -2,5 +2,7 @@ import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename),
component: nxComponentTestingPreset(__filename, {
bundler: '<%= bundler %>'
}) as any,
});

View File

@ -1,14 +1,20 @@
import {
ensurePackage,
generateFiles,
joinPathFragments,
logger,
parseTargetString,
ProjectConfiguration,
readProjectConfiguration,
Tree,
visitNotIgnoredFiles,
} from '@nrwl/devkit';
import { nxVersion } from 'nx/src/utils/versions';
import * as ts from 'typescript';
import { getComponentNode } from '../../../utils/ast-utils';
import { componentTestGenerator } from '../../component-test/component-test';
import { CypressComponentConfigurationSchema } from '../schema';
import { FoundTarget } from './update-configs';
const allowedFileExt = new RegExp(/\.[jt]sx?/g);
const isSpecFile = new RegExp(/(spec|test)\./g);
@ -16,7 +22,8 @@ const isSpecFile = new RegExp(/(spec|test)\./g);
export async function addFiles(
tree: Tree,
projectConfig: ProjectConfiguration,
options: CypressComponentConfigurationSchema
options: CypressComponentConfigurationSchema,
found: FoundTarget
) {
const cypressConfigPath = joinPathFragments(
projectConfig.root,
@ -26,15 +33,32 @@ export async function addFiles(
tree.delete(cypressConfigPath);
}
const actualBundler = getBundler(found, tree);
if (options.bundler && options.bundler !== actualBundler) {
logger.warn(
`You have specified ${options.bundler} as the bundler but this project is configured to use ${actualBundler}.
This may cause errors. If you are seeing errors, try removing the --bundler option.`
);
}
generateFiles(
tree,
joinPathFragments(__dirname, '..', 'files'),
projectConfig.root,
{
tpl: '',
bundler: options.bundler ?? actualBundler,
}
);
if (
options.bundler === 'webpack' ||
(!options.bundler && actualBundler === 'webpack')
) {
await ensurePackage(tree, '@nrwl/webpack', nxVersion);
}
if (options.generateTests) {
const filePaths = [];
visitNotIgnoredFiles(tree, projectConfig.sourceRoot, (filePath) => {
@ -52,6 +76,18 @@ export async function addFiles(
}
}
function getBundler(found: FoundTarget, tree: Tree): 'vite' | 'webpack' {
if (found.target && found.config?.executor) {
return found.config.executor === '@nrwl/vite:build' ? 'vite' : 'webpack';
}
const { target, project } = parseTargetString(found.target);
const projectConfig = readProjectConfiguration(tree, project);
return projectConfig?.targets?.[target]?.executor === '@nrwl/vite:build'
? 'vite'
: 'webpack';
}
function isComponent(tree: Tree, filePath: string): boolean {
if (isSpecFile.test(filePath) || !allowedFileExt.test(filePath)) {
return false;

View File

@ -1,14 +1,20 @@
import {
readProjectConfiguration,
TargetConfiguration,
Tree,
updateProjectConfiguration,
} from '@nrwl/devkit';
import { CypressComponentConfigurationSchema } from '../schema';
export interface FoundTarget {
config?: TargetConfiguration;
target: string;
}
export async function updateProjectConfig(
tree: Tree,
options: CypressComponentConfigurationSchema
) {
): Promise<FoundTarget> {
const { findBuildConfig } = await import(
'@nrwl/cypress/src/utils/find-target-options'
);
@ -30,6 +36,8 @@ export async function updateProjectConfig(
skipServe: true,
};
updateProjectConfiguration(tree, options.project, projectConfig);
return found;
}
function assetValidConfig(config: unknown) {

View File

@ -3,4 +3,5 @@ export interface CypressComponentConfigurationSchema {
generateTests: boolean;
skipFormat?: boolean;
buildTarget?: string;
bundler?: 'webpack' | 'vite';
}

View File

@ -37,6 +37,12 @@
"type": "boolean",
"description": "Skip formatting files",
"default": false
},
"bundler": {
"description": "The bundler to use for Cypress Component Testing.",
"type": "string",
"enum": ["vite", "webpack"],
"hidden": true
}
},
"required": ["project"],

View File

@ -67,7 +67,10 @@
},
"bundler": {
"description": "The Storybook builder to use.",
"enum": ["vite", "webpack"]
"type": "string",
"enum": ["vite", "webpack"],
"x-prompt": "Which Storybook builder do you want to use?",
"default": "webpack"
}
},
"required": ["name"],

View File

@ -22,6 +22,7 @@
},
"bundler": {
"description": "The bundler to use.",
"type": "string",
"enum": ["vite", "webpack"],
"x-prompt": "Which bundler do you want to use?",
"default": "webpack"

View File

@ -1,3 +1,4 @@
export * from './src/utils/versions';
export * from './src/utils/generator-utils';
export { viteConfigurationGenerator } from './src/generators/configuration/configuration';
export { vitestGenerator } from './src/generators/vitest/vitest-generator';

View File

@ -2,15 +2,14 @@ import {
applyChangesToString,
ChangeType,
formatFiles,
joinPathFragments,
readProjectConfiguration,
Tree,
workspaceRoot,
} from '@nrwl/devkit';
import { forEachExecutorOptions } from '@nrwl/workspace/src/utilities/executor-options-utils';
import { findNodes } from 'nx/src/utils/typescript';
import ts = require('typescript');
import { normalizeViteConfigFilePathWithTree } from '../../utils/generator-utils';
export async function removeProjectsFromViteTsConfigPaths(tree: Tree) {
findAllProjectsWithViteConfig(tree);
@ -22,11 +21,10 @@ export default removeProjectsFromViteTsConfigPaths;
function findAllProjectsWithViteConfig(tree: Tree): void {
forEachExecutorOptions(tree, '@nrwl/vite:build', (options, project) => {
const projectConfiguration = readProjectConfiguration(tree, project);
const viteConfig = normalizeConfigFilePathWithTree(
const viteConfig = normalizeViteConfigFilePathWithTree(
tree,
projectConfiguration.root,
options?.['configFile'],
workspaceRoot
options?.['configFile']
);
if (viteConfig) {
const file = getTsSourceFile(tree, viteConfig);
@ -85,18 +83,3 @@ export function getTsSourceFile(host: Tree, path: string): ts.SourceFile {
return source;
}
function normalizeConfigFilePathWithTree(
tree: Tree,
projectRoot: string,
configFile?: string,
workspaceRoot?: string
): string {
return configFile
? joinPathFragments(`${workspaceRoot}/${configFile}`)
: tree.exists(joinPathFragments(`${projectRoot}/vite.config.ts`))
? joinPathFragments(`${projectRoot}/vite.config.ts`)
: tree.exists(joinPathFragments(`${projectRoot}/vite.config.js`))
? joinPathFragments(`${projectRoot}/vite.config.js`)
: undefined;
}

View File

@ -0,0 +1,111 @@
import {
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nrwl/devkit';
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
import {
findExistingTargets,
getViteConfigPathForProject,
} from './generator-utils';
import { mockReactAppGenerator, mockViteReactAppGenerator } from './test-utils';
describe('generator utils', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyV1Workspace();
});
describe('getViteConfigPathForProject', () => {
beforeEach(() => {
mockViteReactAppGenerator(tree);
});
it('should return correct path for vite.config file if no configFile is set', () => {
const viteConfigPath = getViteConfigPathForProject(
tree,
'my-test-react-vite-app'
);
expect(viteConfigPath).toEqual(
'apps/my-test-react-vite-app/vite.config.ts'
);
});
it('should return correct path for vite.config file if custom configFile is set', () => {
const projectConfig = readProjectConfiguration(
tree,
'my-test-react-vite-app'
);
updateProjectConfiguration(tree, 'my-test-react-vite-app', {
...projectConfig,
targets: {
...projectConfig.targets,
build: {
...projectConfig.targets.build,
options: {
...projectConfig.targets.build.options,
configFile: 'apps/my-test-react-vite-app/vite.config.custom.ts',
},
},
},
});
tree.write(`apps/my-test-react-vite-app/vite.config.custom.ts`, '');
const viteConfigPath = getViteConfigPathForProject(
tree,
'my-test-react-vite-app'
);
expect(viteConfigPath).toEqual(
'apps/my-test-react-vite-app/vite.config.custom.ts'
);
});
it('should return correct path for vite.config file given a target name', () => {
const projectConfig = readProjectConfiguration(
tree,
'my-test-react-vite-app'
);
updateProjectConfiguration(tree, 'my-test-react-vite-app', {
...projectConfig,
targets: {
...projectConfig.targets,
'other-build': {
...projectConfig.targets.build,
options: {
...projectConfig.targets.build.options,
configFile: 'apps/my-test-react-vite-app/vite.other.custom.ts',
},
},
},
});
tree.write(`apps/my-test-react-vite-app/vite.other.custom.ts`, '');
const viteConfigPath = getViteConfigPathForProject(
tree,
'my-test-react-vite-app',
'other-build'
);
expect(viteConfigPath).toEqual(
'apps/my-test-react-vite-app/vite.other.custom.ts'
);
});
});
describe('findExistingTargets', () => {
beforeEach(() => {
mockReactAppGenerator(tree);
});
it('should return the correct targets', () => {
const { targets } = readProjectConfiguration(tree, 'my-test-react-app');
const existingTargets = findExistingTargets(targets);
expect(existingTargets).toMatchObject({
buildTarget: 'build',
serveTarget: 'serve',
testTarget: 'test',
unsuppored: undefined,
});
});
});
});

View File

@ -462,3 +462,38 @@ ${options.includeVitest ? '/// <reference types="vitest" />' : ''}
tree.write(viteConfigPath, viteConfigContent);
}
export function normalizeViteConfigFilePathWithTree(
tree: Tree,
projectRoot: string,
configFile?: string
): string {
return configFile && tree.exists(configFile)
? configFile
: tree.exists(joinPathFragments(`${projectRoot}/vite.config.ts`))
? joinPathFragments(`${projectRoot}/vite.config.ts`)
: tree.exists(joinPathFragments(`${projectRoot}/vite.config.js`))
? joinPathFragments(`${projectRoot}/vite.config.js`)
: undefined;
}
export function getViteConfigPathForProject(
tree: Tree,
projectName: string,
target?: string
) {
let viteConfigPath: string | undefined;
const { targets, root } = readProjectConfiguration(tree, projectName);
if (target) {
viteConfigPath = targets[target]?.options?.configFile;
} else {
const buildTarget = Object.entries(targets).find(
([_targetName, targetConfig]) => {
return targetConfig.executor === '@nrwl/vite:build';
}
);
viteConfigPath = buildTarget?.[1]?.options?.configFile;
}
return normalizeViteConfigFilePathWithTree(tree, root, viteConfigPath);
}

View File

@ -32,11 +32,7 @@ export async function getBuildAndSharedConfig(
mode: options.mode ?? context.configurationName,
root: projectRoot,
base: options.base,
configFile: normalizeConfigFilePath(
projectRoot,
options.configFile,
context.root
),
configFile: normalizeViteConfigFilePath(projectRoot, options.configFile),
plugins: [replaceFiles(options.fileReplacements)],
build: getViteBuildOptions(
options as ViteDevServerExecutorOptions & ViteBuildExecutorOptions,
@ -45,13 +41,12 @@ export async function getBuildAndSharedConfig(
} as InlineConfig);
}
export function normalizeConfigFilePath(
export function normalizeViteConfigFilePath(
projectRoot: string,
configFile?: string,
workspaceRoot?: string
configFile?: string
): string {
return configFile
? joinPathFragments(`${workspaceRoot}/${configFile}`)
return configFile && existsSync(joinPathFragments(configFile))
? configFile
: existsSync(joinPathFragments(`${projectRoot}/vite.config.ts`))
? joinPathFragments(`${projectRoot}/vite.config.ts`)
: existsSync(joinPathFragments(`${projectRoot}/vite.config.js`))

View File

@ -53,7 +53,7 @@ const IGNORE_MATCHES_IN_PACKAGE = {
'tailwindcss',
],
cli: ['nx'],
cypress: ['cypress', '@angular-devkit/schematics', '@nrwl/cypress'],
cypress: ['cypress', '@angular-devkit/schematics', '@nrwl/cypress', 'vite'],
devkit: ['@angular-devkit/architect', 'rxjs', 'webpack'],
'eslint-plugin-nx': ['@angular-eslint/eslint-plugin'],
jest: [