From 1ef01f8cccd756bf88bf1063275bcec6c0a211dc Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Thu, 15 Dec 2022 06:28:53 +0200 Subject: [PATCH] feat(testing): cypress vite (#13474) --- docs/generated/packages/cypress.json | 15 +- docs/generated/packages/react.json | 8 +- docs/generated/packages/storybook.json | 6 +- packages/angular/plugins/component-testing.ts | 8 +- .../__snapshots__/e2e.migrator.spec.ts.snap | 38 +++-- .../migrators/projects/e2e.migrator.spec.ts | 17 +-- .../cypress/docs/cypress-project-examples.md | 30 ++++ packages/cypress/plugins/cypress-preset.ts | 23 ++- .../cypress-project.spec.ts.snap | 19 ++- .../cypress-project/cypress-project.spec.ts | 16 +++ .../cypress-project/cypress-project.ts | 18 ++- .../v10-and-after/cypress.config.ts__tmpl__ | 8 +- .../generators/cypress-project/schema.d.ts | 1 + .../generators/cypress-project/schema.json | 13 ++ .../cypress/src/plugins/preprocessor-vite.ts | 89 ++++++++++++ packages/cypress/src/utils/versions.ts | 1 + ...ypress-component-configuration-examples.md | 29 +++- .../react/plugins/component-testing/index.ts | 49 +++++-- .../component-testing/webpack-fallback.ts | 2 +- ...press-component-configuration.spec.ts.snap | 44 ++++++ .../cypress-component-configuration.spec.ts | 136 ++++++++++++++++-- .../cypress-component-configuration.ts | 6 +- .../files/cypress.config.ts__tpl__ | 6 +- .../lib/add-files.ts | 38 ++++- .../lib/update-configs.ts | 10 +- .../schema.d.ts | 1 + .../schema.json | 6 + .../src/generators/configuration/schema.json | 5 +- .../storybook/src/generators/init/schema.json | 1 + packages/vite/index.ts | 1 + .../update-vite-tsconfig-paths.ts | 23 +-- .../vite/src/utils/generator-util.test.ts | 111 ++++++++++++++ packages/vite/src/utils/generator-utils.ts | 35 +++++ packages/vite/src/utils/options-utils.ts | 15 +- scripts/depcheck/missing.ts | 2 +- 35 files changed, 727 insertions(+), 103 deletions(-) create mode 100644 packages/cypress/src/plugins/preprocessor-vite.ts create mode 100644 packages/vite/src/utils/generator-util.test.ts diff --git a/docs/generated/packages/cypress.json b/docs/generated/packages/cypress.json index 2642273f59c6b..d24a0bf7e7d60 100644 --- a/docs/generated/packages/cypress.json +++ b/docs/generated/packages/cypress.json @@ -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.", diff --git a/docs/generated/packages/react.json b/docs/generated/packages/react.json index b4f5e63d5c6a0..5f066eaebc135 100644 --- a/docs/generated/packages/react.json +++ b/docs/generated/packages/react.json @@ -1333,10 +1333,16 @@ "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"], - "examplesFile": "{% callout type=\"caution\" title=\"Can I use component testing?\" %}\nReact component testing with Nx requires **Cypress version 10.7.0** and up.\n\nYou can migrate with to v10 via the [migrate-to-cypress-10 generator](/packages/cypress/generators/migrate-to-cypress-10).\n\nThis generator is for Cypress based component testing.\n\nIf you want to test components via Storybook with Cypress, then check out the [storybook-configuration generator docs](/packages/react/generators/storybook-configuration)\n{% /callout %}\n\nThis generator is designed to get your React project up and running with Cypress Component Testing.\n\n```shell\nnx g @nrwl/react:cypress-component-project --project=my-cool-react-project\n```\n\nRunning this generator, adds the required files to the specified project with a preconfigured `cypress.config.ts` designed for Nx workspaces.\n\n```ts {% fileName=\"cypress.config.ts\" %}\nimport { defineConfig } from 'cypress';\nimport { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';\n\nexport default defineConfig({\n component: nxComponentTestingPreset(__filename),\n});\n```\n\nHere is an example on how to add custom options to the configuration\n\n```ts {% fileName=\"cypress.config.ts\" %}\nimport { defineConfig } from 'cypress';\nimport { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';\n\nexport default defineConfig({\n component: {\n ...nxComponentTestingPreset(__filename),\n // extra options here\n },\n});\n```\n\n## Specifying a Build Target\n\nComponent 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.\n\nFor React projects, the build target needs to be using the `@nrwl/webpack:webpack` executor.\nThe generator will throw an error if a build target can't be found and suggest passing one in manually.\n\nLetting Nx infer the build target by default\n\n```shell\nnx g @nrwl/react:cypress-component-project --project=my-cool-react-project\n```\n\nManually specifying the build target\n\n```shell\nnx g @nrwl/react:cypress-component-project --project=my-cool-react-project --build-target:some-react-app:build --generate-tests\n```\n\n{% callout type=\"note\" title=\"Build Target with Configuration\" %}\nIf you're wanting to use a build target with a specific configuration. i.e. `my-app:build:production`,\nthen manually providing `--build-target=my-app:build:production` is the best way to do that.\n{% /callout %}\n\n## Auto Generating Tests\n\nYou can optionally use the `--generate-tests` flag to generate a test file for each component in your project.\n\n```shell\nnx g @nrwl/react:cypress-component-project --project=my-cool-react-project --generate-tests\n```\n\n## Running Component Tests\n\nA new `component-test` target will be added to the specified project to run your component tests.\n\n```shell\nnx g component-test my-cool-react-project\n```\n\nHere is an example of the project configuration that is generated. The `--build-target` option is added as the `devServerTarget` which can be changed as needed.\n\n```json {% fileName=\"project.json\" %}\n{\n \"targets\" {\n \"component-test\": {\n \"executor\": \"@nrwl/cypress:cypress\",\n \"options\": {\n \"cypressConfig\": \"/cypress.config.ts\",\n \"testingType\": \"component\",\n \"devServerTarget\": \"some-react-app:build\",\n \"skipServe\": true\n }\n }\n }\n}\n```\n\nNx also supports [Angular component testing](/packages/angular/generators/cypress-component-configuration).\n", + "examplesFile": "{% callout type=\"caution\" title=\"Can I use component testing?\" %}\nReact component testing with Nx requires **Cypress version 10.7.0** and up.\n\nYou can migrate with to v10 via the [migrate-to-cypress-10 generator](/packages/cypress/generators/migrate-to-cypress-10).\n\nThis generator is for Cypress based component testing.\n\nIf you want to test components via Storybook with Cypress, then check out the [storybook-configuration generator docs](/packages/react/generators/storybook-configuration)\n{% /callout %}\n\nThis generator is designed to get your React project up and running with Cypress Component Testing.\n\n```shell\nnx g @nrwl/react:cypress-component-project --project=my-cool-react-project\n```\n\nRunning this generator, adds the required files to the specified project with a preconfigured `cypress.config.ts` designed for Nx workspaces.\n\nThe following file will be added to projects where the Component Testing build target is using `webpack` for bundling:\n\n```ts {% fileName=\"cypress.config.ts\" %}\nimport { defineConfig } from 'cypress';\nimport { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';\n\nexport default defineConfig({\n component: nxComponentTestingPreset(__filename, {\n bundler: 'webpack',\n }),\n});\n```\n\nThe following file will be added to projects where the Component Testing build target is using `vite` for bundling:\n\n```ts {% fileName=\"cypress.config.ts\" %}\nimport { defineConfig } from 'cypress';\nimport { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';\n\nexport default defineConfig({\n component: nxComponentTestingPreset(__filename, {\n bundler: 'vite',\n }),\n});\n```\n\nHere is an example on how to add custom options to the configuration\n\n```ts {% fileName=\"cypress.config.ts\" %}\nimport { defineConfig } from 'cypress';\nimport { nxComponentTestingPreset } from '@nrwl/react/plugins/component-testing';\n\nexport default defineConfig({\n component: {\n ...nxComponentTestingPreset(__filename, {\n bundler: 'webpack',\n }),\n // extra options here\n },\n});\n```\n\n## The `bundler` option\n\nComponent 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.\n\nYou 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.\n\n## Specifying a Build Target\n\nComponent 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.\n\nFor React projects, the build target needs to be using the `@nrwl/webpack:webpack` executor.\nThe generator will throw an error if a build target can't be found and suggest passing one in manually.\n\nLetting Nx infer the build target by default\n\n```shell\nnx g @nrwl/react:cypress-component-project --project=my-cool-react-project\n```\n\nManually specifying the build target\n\n```shell\nnx g @nrwl/react:cypress-component-project --project=my-cool-react-project --build-target:some-react-app:build --generate-tests\n```\n\n{% callout type=\"note\" title=\"Build Target with Configuration\" %}\nIf you're wanting to use a build target with a specific configuration. i.e. `my-app:build:production`,\nthen manually providing `--build-target=my-app:build:production` is the best way to do that.\n{% /callout %}\n\n## Auto Generating Tests\n\nYou can optionally use the `--generate-tests` flag to generate a test file for each component in your project.\n\n```shell\nnx g @nrwl/react:cypress-component-project --project=my-cool-react-project --generate-tests\n```\n\n## Running Component Tests\n\nA new `component-test` target will be added to the specified project to run your component tests.\n\n```shell\nnx g component-test my-cool-react-project\n```\n\nHere is an example of the project configuration that is generated. The `--build-target` option is added as the `devServerTarget` which can be changed as needed.\n\n```json {% fileName=\"project.json\" %}\n{\n \"targets\" {\n \"component-test\": {\n \"executor\": \"@nrwl/cypress:cypress\",\n \"options\": {\n \"cypressConfig\": \"/cypress.config.ts\",\n \"testingType\": \"component\",\n \"devServerTarget\": \"some-react-app:build\",\n \"skipServe\": true\n }\n }\n }\n}\n```\n\nNx also supports [Angular component testing](/packages/angular/generators/cypress-component-configuration).\n", "presets": [] }, "description": "Setup Cypress component testing for a React project", diff --git a/docs/generated/packages/storybook.json b/docs/generated/packages/storybook.json index 53f93998f4fa6..beab76b8ed309 100644 --- a/docs/generated/packages/storybook.json +++ b/docs/generated/packages/storybook.json @@ -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"], diff --git a/packages/angular/plugins/component-testing.ts b/packages/angular/plugins/component-testing.ts index 9fc28faf89520..c294935156f81 100644 --- a/packages/angular/plugins/component-testing.ts +++ b/packages/angular/plugins/component-testing.ts @@ -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, diff --git a/packages/angular/src/generators/ng-add/migrators/projects/__snapshots__/e2e.migrator.spec.ts.snap b/packages/angular/src/generators/ng-add/migrators/projects/__snapshots__/e2e.migrator.spec.ts.snap index 3e13315f47883..97c514c3b320c 100644 --- a/packages/angular/src/generators/ng-add/migrators/projects/__snapshots__/e2e.migrator.spec.ts.snap +++ b/packages/angular/src/generators/ng-add/migrators/projects/__snapshots__/e2e.migrator.spec.ts.snap @@ -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({ - e2e: {...nxE2EPreset(__dirname), - baseUrl: 'http://localhost:4200' - }, -});" + 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({ - e2e: {...nxE2EPreset(__dirname), - baseUrl: 'http://localhost:4200' - }, -});" + 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`] = ` diff --git a/packages/angular/src/generators/ng-add/migrators/projects/e2e.migrator.spec.ts b/packages/angular/src/generators/ng-add/migrators/projects/e2e.migrator.spec.ts index ab95f6ddf6c86..1b2430b2b7c10 100644 --- a/packages/angular/src/generators/ng-add/migrators/projects/e2e.migrator.spec.ts +++ b/packages/angular/src/generators/ng-add/migrators/projects/e2e.migrator.spec.ts @@ -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({ - e2e: { - baseUrl: 'http://localhost:4200' - }, -});` + export default defineConfig({ + e2e: { + baseUrl: 'http://localhost:4200' + }, + });` ); const project = addProject('app1', { root, diff --git a/packages/cypress/docs/cypress-project-examples.md b/packages/cypress/docs/cypress-project-examples.md index 3eee4b9890863..f35fff802c9b4 100644 --- a/packages/cypress/docs/cypress-project-examples.md +++ b/packages/cypress/docs/cypress-project-examples.md @@ -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 + } + }, +}); +``` diff --git a/packages/cypress/plugins/cypress-preset.ts b/packages/cypress/plugins/cypress-preset.ts index 8c85f8ba4756c..2e969f7928095 100644 --- a/packages/cypress/plugins/cypress-preset.ts +++ b/packages/cypress/plugins/cypress-preset.ts @@ -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; } diff --git a/packages/cypress/src/generators/cypress-project/__snapshots__/cypress-project.spec.ts.snap b/packages/cypress/src/generators/cypress-project/__snapshots__/cypress-project.spec.ts.snap index 96d46aa11dd44..42eca726b6e31 100644 --- a/packages/cypress/src/generators/cypress-project/__snapshots__/cypress-project.spec.ts.snap +++ b/packages/cypress/src/generators/cypress-project/__snapshots__/cypress-project.spec.ts.snap @@ -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`] = ` diff --git a/packages/cypress/src/generators/cypress-project/cypress-project.spec.ts b/packages/cypress/src/generators/cypress-project/cypress-project.spec.ts index 89892dcc0e95d..0bc6c4786bc96 100644 --- a/packages/cypress/src/generators/cypress-project/cypress-project.spec.ts +++ b/packages/cypress/src/generators/cypress-project/cypress-project.spec.ts @@ -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, { diff --git a/packages/cypress/src/generators/cypress-project/cypress-project.ts b/packages/cypress/src/generators/cypress-project/cypress-project.ts index 243d547ce1b11..81cba62bd5dfb 100644 --- a/packages/cypress/src/generators/cypress-project/cypress-project.ts +++ b/packages/cypress/src/generators/cypress-project/cypress-project.ts @@ -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 diff --git a/packages/cypress/src/generators/cypress-project/files/v10-and-after/cypress.config.ts__tmpl__ b/packages/cypress/src/generators/cypress-project/files/v10-and-after/cypress.config.ts__tmpl__ index ec426d33f5c31..cc3910ec5ca2a 100644 --- a/packages/cypress/src/generators/cypress-project/files/v10-and-after/cypress.config.ts__tmpl__ +++ b/packages/cypress/src/generators/cypress-project/files/v10-and-after/cypress.config.ts__tmpl__ @@ -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' + } + <% } %>) +}); \ No newline at end of file diff --git a/packages/cypress/src/generators/cypress-project/schema.d.ts b/packages/cypress/src/generators/cypress-project/schema.d.ts index 83038d479e3bb..b991f6cad0973 100644 --- a/packages/cypress/src/generators/cypress-project/schema.d.ts +++ b/packages/cypress/src/generators/cypress-project/schema.d.ts @@ -12,4 +12,5 @@ export interface Schema { standaloneConfig?: boolean; skipPackageJson?: boolean; rootProject?: boolean; + bundler?: 'webpack' | 'vite' | 'none'; } diff --git a/packages/cypress/src/generators/cypress-project/schema.json b/packages/cypress/src/generators/cypress-project/schema.json index 18ede4e4bd3d5..0d8340fe7d9a2 100644 --- a/packages/cypress/src/generators/cypress-project/schema.json +++ b/packages/cypress/src/generators/cypress-project/schema.json @@ -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"], diff --git a/packages/cypress/src/plugins/preprocessor-vite.ts b/packages/cypress/src/plugins/preprocessor-vite.ts new file mode 100644 index 0000000000000..978a1d97dde8b --- /dev/null +++ b/packages/cypress/src/plugins/preprocessor-vite.ts @@ -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 | Promise; + +/** + * 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; diff --git a/packages/cypress/src/utils/versions.ts b/packages/cypress/src/utils/versions.ts index 4a1a296adf9e2..5cf5807512991 100644 --- a/packages/cypress/src/utils/versions.ts +++ b/packages/cypress/src/utils/versions.ts @@ -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'; diff --git a/packages/react/docs/cypress-component-configuration-examples.md b/packages/react/docs/cypress-component-configuration-examples.md index d8a446c463c28..cd66f019b35c3 100644 --- a/packages/react/docs/cypress-component-configuration-examples.md +++ b/packages/react/docs/cypress-component-configuration-examples.md @@ -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. diff --git a/packages/react/plugins/component-testing/index.ts b/packages/react/plugins/component-testing/index.ts index 16db8483d397d..c595aa9c36d54 100644 --- a/packages/react/plugins/component-testing/index.ts +++ b/packages/react/plugins/component-testing/index.ts @@ -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(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, diff --git a/packages/react/plugins/component-testing/webpack-fallback.ts b/packages/react/plugins/component-testing/webpack-fallback.ts index 69e0c84ec3d67..397314e16f08b 100644 --- a/packages/react/plugins/component-testing/webpack-fallback.ts +++ b/packages/react/plugins/component-testing/webpack-fallback.ts @@ -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', diff --git a/packages/react/src/generators/cypress-component-configuration/__snapshots__/cypress-component-configuration.spec.ts.snap b/packages/react/src/generators/cypress-component-configuration/__snapshots__/cypress-component-configuration.spec.ts.snap index eabe96260b165..6eaccf7587c4b 100644 --- a/packages/react/src/generators/cypress-component-configuration/__snapshots__/cypress-component-configuration.spec.ts.snap +++ b/packages/react/src/generators/cypress-component-configuration/__snapshots__/cypress-component-configuration.spec.ts.snap @@ -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' diff --git a/packages/react/src/generators/cypress-component-configuration/cypress-component-configuration.spec.ts b/packages/react/src/generators/cypress-component-configuration/cypress-component-configuration.spec.ts index e903b5b02f3fd..40acf9e08f3ce 100644 --- a/packages/react/src/generators/cypress-component-configuration/cypress-component-configuration.spec.ts +++ b/packages/react/src/generators/cypress-component-configuration/cypress-component-configuration.spec.ts @@ -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, { diff --git a/packages/react/src/generators/cypress-component-configuration/cypress-component-configuration.ts b/packages/react/src/generators/cypress-component-configuration/cypress-component-configuration.ts index 3ac5523961ef0..1265d86ca15a6 100644 --- a/packages/react/src/generators/cypress-component-configuration/cypress-component-configuration.ts +++ b/packages/react/src/generators/cypress-component-configuration/cypress-component-configuration.ts @@ -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); } diff --git a/packages/react/src/generators/cypress-component-configuration/files/cypress.config.ts__tpl__ b/packages/react/src/generators/cypress-component-configuration/files/cypress.config.ts__tpl__ index 6fa4db1385684..22b4b44221a71 100644 --- a/packages/react/src/generators/cypress-component-configuration/files/cypress.config.ts__tpl__ +++ b/packages/react/src/generators/cypress-component-configuration/files/cypress.config.ts__tpl__ @@ -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, +}); \ No newline at end of file diff --git a/packages/react/src/generators/cypress-component-configuration/lib/add-files.ts b/packages/react/src/generators/cypress-component-configuration/lib/add-files.ts index 81b2c0ef62b2c..323fee1a1a3f4 100644 --- a/packages/react/src/generators/cypress-component-configuration/lib/add-files.ts +++ b/packages/react/src/generators/cypress-component-configuration/lib/add-files.ts @@ -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; diff --git a/packages/react/src/generators/cypress-component-configuration/lib/update-configs.ts b/packages/react/src/generators/cypress-component-configuration/lib/update-configs.ts index 57d83ba4531eb..ecb91734fa8e0 100644 --- a/packages/react/src/generators/cypress-component-configuration/lib/update-configs.ts +++ b/packages/react/src/generators/cypress-component-configuration/lib/update-configs.ts @@ -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 { 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) { diff --git a/packages/react/src/generators/cypress-component-configuration/schema.d.ts b/packages/react/src/generators/cypress-component-configuration/schema.d.ts index bc2c652495c82..4b75757bf3ad5 100644 --- a/packages/react/src/generators/cypress-component-configuration/schema.d.ts +++ b/packages/react/src/generators/cypress-component-configuration/schema.d.ts @@ -3,4 +3,5 @@ export interface CypressComponentConfigurationSchema { generateTests: boolean; skipFormat?: boolean; buildTarget?: string; + bundler?: 'webpack' | 'vite'; } diff --git a/packages/react/src/generators/cypress-component-configuration/schema.json b/packages/react/src/generators/cypress-component-configuration/schema.json index 2764831b769ba..b06cb6ba66d02 100644 --- a/packages/react/src/generators/cypress-component-configuration/schema.json +++ b/packages/react/src/generators/cypress-component-configuration/schema.json @@ -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"], diff --git a/packages/storybook/src/generators/configuration/schema.json b/packages/storybook/src/generators/configuration/schema.json index 09b91a04ca8e2..1665670e86973 100644 --- a/packages/storybook/src/generators/configuration/schema.json +++ b/packages/storybook/src/generators/configuration/schema.json @@ -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"], diff --git a/packages/storybook/src/generators/init/schema.json b/packages/storybook/src/generators/init/schema.json index 403de94854434..aa4492d2b529f 100644 --- a/packages/storybook/src/generators/init/schema.json +++ b/packages/storybook/src/generators/init/schema.json @@ -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" diff --git a/packages/vite/index.ts b/packages/vite/index.ts index 2c2e3587f17f1..9e95a6fe2be3e 100644 --- a/packages/vite/index.ts +++ b/packages/vite/index.ts @@ -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'; diff --git a/packages/vite/src/migrations/update-15-3-1/update-vite-tsconfig-paths.ts b/packages/vite/src/migrations/update-15-3-1/update-vite-tsconfig-paths.ts index a7308609cbe1a..890c63e11dcf4 100644 --- a/packages/vite/src/migrations/update-15-3-1/update-vite-tsconfig-paths.ts +++ b/packages/vite/src/migrations/update-15-3-1/update-vite-tsconfig-paths.ts @@ -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; -} diff --git a/packages/vite/src/utils/generator-util.test.ts b/packages/vite/src/utils/generator-util.test.ts new file mode 100644 index 0000000000000..82ba55b15c32f --- /dev/null +++ b/packages/vite/src/utils/generator-util.test.ts @@ -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, + }); + }); + }); +}); diff --git a/packages/vite/src/utils/generator-utils.ts b/packages/vite/src/utils/generator-utils.ts index 46adee9081d13..c6a7c67871b58 100644 --- a/packages/vite/src/utils/generator-utils.ts +++ b/packages/vite/src/utils/generator-utils.ts @@ -462,3 +462,38 @@ ${options.includeVitest ? '/// ' : ''} 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); +} diff --git a/packages/vite/src/utils/options-utils.ts b/packages/vite/src/utils/options-utils.ts index 2357a36d4cafc..4b0810d53e38d 100644 --- a/packages/vite/src/utils/options-utils.ts +++ b/packages/vite/src/utils/options-utils.ts @@ -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`)) diff --git a/scripts/depcheck/missing.ts b/scripts/depcheck/missing.ts index 9db6da11adcae..10e79615d9388 100644 --- a/scripts/depcheck/missing.ts +++ b/scripts/depcheck/missing.ts @@ -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: [