diff --git a/e2e/react/src/playwright.test.ts b/e2e/react/src/playwright.test.ts
index 4b15d606213d0..9bd7894c96664 100644
--- a/e2e/react/src/playwright.test.ts
+++ b/e2e/react/src/playwright.test.ts
@@ -18,7 +18,7 @@ describe('React Playwright e2e tests', () => {
packages: ['@nx/react'],
});
runCLI(
- `generate @nx/react:app ${appName} --e2eTestRunner=playwright --projectNameAndRootFormat=as-provided --no-interactive`
+ `generate @nx/react:app ${appName} --e2eTestRunner=playwright --bundler=vite --projectNameAndRootFormat=as-provided --no-interactive`
);
});
diff --git a/e2e/webpack/src/__snapshots__/webpack.legacy.test.ts.snap b/e2e/webpack/src/__snapshots__/webpack.legacy.test.ts.snap
index 9852a63a9406b..31219273146df 100644
--- a/e2e/webpack/src/__snapshots__/webpack.legacy.test.ts.snap
+++ b/e2e/webpack/src/__snapshots__/webpack.legacy.test.ts.snap
@@ -88,6 +88,14 @@ exports[`Webpack Plugin (legacy) ConvertConfigToWebpackPlugin, should convert wi
"lint": {
"executor": "@nx/eslint:lint"
},
+ "serve-static": {
+ "executor": "@nx/web:file-server",
+ "dependsOn": ["build"],
+ "options": {
+ "buildTarget": "app3224373:build",
+ "spa": true
+ }
+ },
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
diff --git a/e2e/webpack/src/webpack.legacy.test.ts b/e2e/webpack/src/webpack.legacy.test.ts
index 080ea696740c4..1c9a8c020a861 100644
--- a/e2e/webpack/src/webpack.legacy.test.ts
+++ b/e2e/webpack/src/webpack.legacy.test.ts
@@ -113,17 +113,17 @@ describe('Webpack Plugin (legacy)', () => {
updateFile(
`${appName}/src/main.ts`,
`
- document.querySelector('proj-root').innerHTML = '
Welcome
';
+ document.querySelector('proj-root')!.innerHTML = 'Welcome
';
`
);
updateFile(
`${appName}/webpack.config.js`,
`
const { join } = require('path');
- const {NxWebpackPlugin} = require('@nx/webpack');
+ const {NxAppWebpackPlugin} = require('@nx/webpack/app-plugin');
module.exports = {
output: {
- path: join(__dirname, '../dist/app9524918'),
+ path: join(__dirname, '../dist/${appName}'),
},
plugins: [
new NxAppWebpackPlugin({
diff --git a/packages/cypress/migrations.json b/packages/cypress/migrations.json
index 5bcd7d1698559..b1df577eb07dc 100644
--- a/packages/cypress/migrations.json
+++ b/packages/cypress/migrations.json
@@ -29,6 +29,12 @@
"version": "18.1.0-beta.3",
"description": "Update to Cypress ^13.6.6 if the workspace is using Cypress v13 to ensure workspaces don't use v13.6.5 which has an issue when verifying Cypress.",
"implementation": "./src/migrations/update-18-1-0/update-cypress-version-13-6-6"
+ },
+ "update-19-6-0-update-ci-webserver-for-vite": {
+ "cli": "nx",
+ "version": "19.6.0-beta.0",
+ "description": "Update ciWebServerCommand to use previewTargetName if Vite is detected for the application.",
+ "implementation": "./src/migrations/update-19-6-0/update-ci-webserver-for-vite"
}
},
"packageJsonUpdates": {
diff --git a/packages/cypress/plugins/cypress-preset.ts b/packages/cypress/plugins/cypress-preset.ts
index 95879c1730571..ea763eaa48152 100644
--- a/packages/cypress/plugins/cypress-preset.ts
+++ b/packages/cypress/plugins/cypress-preset.ts
@@ -131,6 +131,7 @@ export function nxE2EPreset(
webServerCommand: options?.webServerCommands?.default,
webServerCommands: options?.webServerCommands,
ciWebServerCommand: options?.ciWebServerCommand,
+ ciBaseUrl: options?.ciBaseUrl,
},
async setupNodeEvents(on, config) {
@@ -268,6 +269,11 @@ export type NxCypressE2EPresetOptions = {
*/
ciWebServerCommand?: string;
+ /**
+ * The url of the web server for ciWebServerCommand
+ */
+ ciBaseUrl?: string;
+
/**
* Configures how the web server command is started and monitored.
*/
diff --git a/packages/cypress/src/generators/configuration/configuration.ts b/packages/cypress/src/generators/configuration/configuration.ts
index 8635ee5f9664c..d569a3f94941f 100644
--- a/packages/cypress/src/generators/configuration/configuration.ts
+++ b/packages/cypress/src/generators/configuration/configuration.ts
@@ -47,6 +47,7 @@ export interface CypressE2EConfigSchema {
webServerCommands?: Record;
ciWebServerCommand?: string;
+ ciBaseUrl?: string;
addPlugin?: boolean;
}
@@ -218,10 +219,12 @@ async function addFiles(
let webServerCommands: Record;
let ciWebServerCommand: string;
+ let ciBaseUrl: string;
if (hasPlugin && options.webServerCommands && options.ciWebServerCommand) {
webServerCommands = options.webServerCommands;
ciWebServerCommand = options.ciWebServerCommand;
+ ciBaseUrl = options.ciBaseUrl;
} else if (hasPlugin && options.devServerTarget) {
webServerCommands = {};
@@ -253,6 +256,7 @@ async function addFiles(
bundler: options.bundler === 'vite' ? 'vite' : undefined,
webServerCommands,
ciWebServerCommand: ciWebServerCommand,
+ ciBaseUrl,
},
options.baseUrl
);
diff --git a/packages/cypress/src/migrations/update-19-6-0/update-ci-webserver-for-vite.spec.ts b/packages/cypress/src/migrations/update-19-6-0/update-ci-webserver-for-vite.spec.ts
new file mode 100644
index 0000000000000..05dd4aaa0d817
--- /dev/null
+++ b/packages/cypress/src/migrations/update-19-6-0/update-ci-webserver-for-vite.spec.ts
@@ -0,0 +1,262 @@
+import updateCiWebserverForVite from './update-ci-webserver-for-vite';
+import {
+ type Tree,
+ type ProjectGraph,
+ readNxJson,
+ updateNxJson,
+} from '@nx/devkit';
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
+
+let projectGraph: ProjectGraph;
+jest.mock('@nx/devkit', () => ({
+ ...jest.requireActual('@nx/devkit'),
+ createProjectGraphAsync: jest.fn().mockImplementation(async () => {
+ return projectGraph;
+ }),
+}));
+
+describe('updateCiWebserverForVite', () => {
+ let tree: Tree;
+ let tempFs: TempFs;
+
+ beforeEach(() => {
+ tree = createTreeWithEmptyWorkspace();
+ tempFs = new TempFs('add-e2e-ci');
+ tree.root = tempFs.tempDir;
+ projectGraph = {
+ nodes: {},
+ dependencies: {},
+ externalNodes: {},
+ };
+ });
+
+ afterEach(() => {
+ tempFs.reset();
+ });
+
+ it('should do nothing if vite is not found for application', async () => {
+ // ARRANGE
+ const nxJson = readNxJson(tree);
+ nxJson.plugins = [
+ {
+ plugin: '@nx/cypress/plugin',
+ options: {
+ targetName: 'e2e',
+ ciTargetName: 'e2e-ci',
+ },
+ },
+ ];
+ updateNxJson(tree, nxJson);
+
+ addProject(tree, tempFs, {
+ buildTargetName: 'build',
+ ciTargetName: 'e2e-ci',
+ appName: 'app',
+ noVite: true,
+ });
+
+ // ACT
+ await updateCiWebserverForVite(tree);
+
+ // ASSERT
+ expect(tree.read('app-e2e/cypress.config.ts', 'utf-8'))
+ .toMatchInlineSnapshot(`
+ "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
+ import { defineConfig } from 'cypress';
+ export default defineConfig({
+ e2e: {
+ ...nxE2EPreset(__filename, {
+ cypressDir: 'src',
+ bundler: 'vite',
+ webServerCommands: {
+ default: 'nx run app:serve',
+ production: 'nx run app:preview',
+ },
+ ciWebServerCommand: 'nx run app:serve-static',
+ }),
+ baseUrl: 'http://localhost:4200',
+ },
+ });
+ "
+ `);
+ });
+
+ it('should update ciWebServerCommand to preview for vite app', async () => {
+ // ARRANGE
+ const nxJson = readNxJson(tree);
+ nxJson.plugins = [
+ {
+ plugin: '@nx/cypress/plugin',
+ options: {
+ targetName: 'e2e',
+ ciTargetName: 'e2e-ci',
+ },
+ },
+ {
+ plugin: '@nx/vite/plugin',
+ options: {
+ buildTargetName: 'build',
+ previewTargetName: 'preview',
+ },
+ },
+ ];
+ updateNxJson(tree, nxJson);
+
+ addProject(tree, tempFs);
+
+ // ACT
+ await updateCiWebserverForVite(tree);
+
+ // ASSERT
+ expect(tree.read('app-e2e/cypress.config.ts', 'utf-8'))
+ .toMatchInlineSnapshot(`
+ "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
+ import { defineConfig } from 'cypress';
+ export default defineConfig({
+ e2e: {
+ ...nxE2EPreset(__filename, {
+ cypressDir: 'src',
+ bundler: 'vite',
+ webServerCommands: {
+ default: 'nx run app:serve',
+ production: 'nx run app:preview',
+ },
+ ciWebServerCommand: 'nx run app:preview',
+ ciBaseUrl: 'http://localhost:4300',
+ }),
+ baseUrl: 'http://localhost:4200',
+ },
+ });
+ "
+ `);
+ });
+});
+
+function addProject(
+ tree: Tree,
+ tempFs: TempFs,
+ overrides: {
+ ciTargetName: string;
+ buildTargetName: string;
+ appName: string;
+ noCi?: boolean;
+ noVite?: boolean;
+ } = { ciTargetName: 'e2e-ci', buildTargetName: 'build', appName: 'app' }
+) {
+ const appProjectConfig = {
+ name: overrides.appName,
+ root: overrides.appName,
+ sourceRoot: `${overrides.appName}/src`,
+ projectType: 'application',
+ };
+ const viteConfig = `///
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
+export default defineConfig({
+ root: __dirname,
+ cacheDir: '../../node_modules/.vite/${overrides.appName}',
+ server: {
+ port: 4200,
+ host: 'localhost',
+ },
+ preview: {
+ port: 4300,
+ host: 'localhost',
+ },
+ plugins: [react(), nxViteTsPaths()],
+ // Uncomment this if you are using workers.
+ // worker: {
+ // plugins: [ nxViteTsPaths() ],
+ // },
+ build: {
+ outDir: '../../dist/${overrides.appName}',
+ emptyOutDir: true,
+ reportCompressedSize: true,
+ commonjsOptions: {
+ transformMixedEsModules: true,
+ },
+ },
+});`;
+
+ const e2eProjectConfig = {
+ name: `${overrides.appName}-e2e`,
+ root: `${overrides.appName}-e2e`,
+ sourceRoot: `${overrides.appName}-e2e/src`,
+ projectType: 'application',
+ };
+
+ const cypressConfig = `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
+import { defineConfig } from 'cypress';
+export default defineConfig({
+ e2e: {
+ ...nxE2EPreset(__filename, {
+ cypressDir: 'src',
+ bundler: 'vite',
+ webServerCommands: {
+ default: 'nx run ${overrides.appName}:serve',
+ production: 'nx run ${overrides.appName}:preview',
+ },
+ ${
+ !overrides.noCi
+ ? `ciWebServerCommand: 'nx run ${overrides.appName}:serve-static',`
+ : ''
+ }
+ }),
+ baseUrl: 'http://localhost:4200',
+ },
+});
+`;
+
+ if (!overrides.noVite) {
+ tree.write(`${overrides.appName}/vite.config.ts`, viteConfig);
+ }
+ tree.write(
+ `${overrides.appName}/project.json`,
+ JSON.stringify(appProjectConfig)
+ );
+ tree.write(`${overrides.appName}-e2e/cypress.config.ts`, cypressConfig);
+ tree.write(
+ `${overrides.appName}-e2e/project.json`,
+ JSON.stringify(e2eProjectConfig)
+ );
+ if (!overrides.noVite) {
+ tempFs.createFile(`${overrides.appName}/vite.config.ts`, viteConfig);
+ }
+ tempFs.createFilesSync({
+ [`${overrides.appName}/project.json`]: JSON.stringify(appProjectConfig),
+ [`${overrides.appName}-e2e/cypress.config.ts`]: cypressConfig,
+ [`${overrides.appName}-e2e/project.json`]: JSON.stringify(e2eProjectConfig),
+ });
+
+ projectGraph.nodes[overrides.appName] = {
+ name: overrides.appName,
+ type: 'app',
+ data: {
+ projectType: 'application',
+ root: overrides.appName,
+ targets: {
+ [overrides.buildTargetName]: {},
+ 'serve-static': {
+ options: {
+ buildTarget: overrides.buildTargetName,
+ },
+ },
+ },
+ },
+ };
+
+ projectGraph.nodes[`${overrides.appName}-e2e`] = {
+ name: `${overrides.appName}-e2e`,
+ type: 'app',
+ data: {
+ projectType: 'application',
+ root: `${overrides.appName}-e2e`,
+ targets: {
+ e2e: {},
+ [overrides.ciTargetName]: {},
+ },
+ },
+ };
+}
diff --git a/packages/cypress/src/migrations/update-19-6-0/update-ci-webserver-for-vite.ts b/packages/cypress/src/migrations/update-19-6-0/update-ci-webserver-for-vite.ts
new file mode 100644
index 0000000000000..86ace3649e6f9
--- /dev/null
+++ b/packages/cypress/src/migrations/update-19-6-0/update-ci-webserver-for-vite.ts
@@ -0,0 +1,165 @@
+import {
+ type Tree,
+ CreateNodesV2,
+ createProjectGraphAsync,
+ readNxJson,
+ parseTargetString,
+ joinPathFragments,
+ PluginConfiguration,
+ CreateNodes,
+ formatFiles,
+} from '@nx/devkit';
+import {
+ retrieveProjectConfigurations,
+ LoadedNxPlugin,
+ ProjectConfigurationsError,
+ findMatchingConfigFiles,
+} from 'nx/src/devkit-internals';
+import { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
+import { tsquery } from '@phenomnomnominal/tsquery';
+import { CypressPluginOptions } from '../../plugins/plugin';
+
+export default async function (tree: Tree) {
+ const pluginName = '@nx/cypress/plugin';
+ const graph = await createProjectGraphAsync();
+ const nxJson = readNxJson(tree);
+ const matchingPluginRegistrations = nxJson.plugins?.filter((p) =>
+ typeof p === 'string' ? p === pluginName : p.plugin === pluginName
+ );
+
+ const {
+ createNodesV2,
+ }: { createNodesV2: CreateNodesV2 } = await import(
+ pluginName
+ );
+
+ for (const plugin of matchingPluginRegistrations) {
+ let projectConfigs: ConfigurationResult;
+ try {
+ const loadedPlugin = new LoadedNxPlugin(
+ { createNodesV2, name: pluginName },
+ plugin
+ );
+ projectConfigs = await retrieveProjectConfigurations(
+ [loadedPlugin],
+ tree.root,
+ nxJson
+ );
+ } catch (e) {
+ if (e instanceof ProjectConfigurationsError) {
+ projectConfigs = e.partialProjectConfigurationsResult;
+ } else {
+ throw e;
+ }
+ }
+
+ for (const configFile of projectConfigs.matchingProjectFiles) {
+ const configFileContents = tree.read(configFile, 'utf-8');
+ if (!configFileContents.includes('ciWebServerCommand')) {
+ continue;
+ }
+
+ const ast = tsquery.ast(configFileContents);
+ const CI_WEBSERVER_COMMAND_SELECTOR =
+ 'ObjectLiteralExpression PropertyAssignment:has(Identifier[name=ciWebServerCommand]) > StringLiteral';
+ const nodes = tsquery(ast, CI_WEBSERVER_COMMAND_SELECTOR, {
+ visitAllChildren: true,
+ });
+ if (!nodes.length) {
+ continue;
+ }
+ const ciWebServerCommand = nodes[0].getText();
+ const NX_TARGET_REGEX = "(?<=nx run )[^']+";
+ const matches = ciWebServerCommand.match(NX_TARGET_REGEX);
+ if (!matches) {
+ continue;
+ }
+ const targetString = matches[0];
+ const { project, target, configuration } = parseTargetString(
+ targetString,
+ graph
+ );
+
+ const pathToViteConfig = [
+ joinPathFragments(graph.nodes[project].data.root, 'vite.config.ts'),
+ joinPathFragments(graph.nodes[project].data.root, 'vite.config.js'),
+ ].find((p) => tree.exists(p));
+
+ if (!pathToViteConfig) {
+ continue;
+ }
+
+ const viteConfigContents = tree.read(pathToViteConfig, 'utf-8');
+ if (!viteConfigContents.includes('preview:')) {
+ continue;
+ }
+
+ const matchingVitePlugin = await findPluginForConfigFile(
+ tree,
+ '@nx/vite/plugin',
+ pathToViteConfig
+ );
+ const previewTargetName = matchingVitePlugin
+ ? typeof matchingVitePlugin === 'string'
+ ? 'preview'
+ : (matchingVitePlugin.options as any)?.previewTargetName ?? 'preview'
+ : 'preview';
+
+ tree.write(
+ configFile,
+ `${configFileContents.slice(
+ 0,
+ nodes[0].getStart()
+ )}'nx run ${project}:${previewTargetName}',
+ ciBaseUrl: "http://localhost:4300"${configFileContents.slice(
+ nodes[0].getEnd()
+ )}`
+ );
+ }
+ }
+
+ await formatFiles(tree);
+}
+
+async function findPluginForConfigFile(
+ tree: Tree,
+ pluginName: string,
+ pathToConfigFile: string
+): Promise {
+ const nxJson = readNxJson(tree);
+ if (!nxJson.plugins) {
+ return;
+ }
+
+ const pluginRegistrations: PluginConfiguration[] = nxJson.plugins.filter(
+ (p) => (typeof p === 'string' ? p === pluginName : p.plugin === pluginName)
+ );
+
+ for (const plugin of pluginRegistrations) {
+ if (typeof plugin === 'string') {
+ return plugin;
+ }
+
+ if (!plugin.include && !plugin.exclude) {
+ return plugin;
+ }
+
+ if (plugin.include || plugin.exclude) {
+ const resolvedPlugin: {
+ createNodes?: CreateNodes;
+ createNodesV2?: CreateNodesV2;
+ } = await import(pluginName);
+ const pluginGlob =
+ resolvedPlugin.createNodesV2?.[0] ?? resolvedPlugin.createNodes?.[0];
+ const matchingConfigFile = findMatchingConfigFiles(
+ [pathToConfigFile],
+ pluginGlob,
+ plugin.include,
+ plugin.exclude
+ );
+ if (matchingConfigFile.length) {
+ return plugin;
+ }
+ }
+ }
+}
diff --git a/packages/cypress/src/plugins/plugin.ts b/packages/cypress/src/plugins/plugin.ts
index 15a094bbaf3b5..77cee8ef6cd4c 100644
--- a/packages/cypress/src/plugins/plugin.ts
+++ b/packages/cypress/src/plugins/plugin.ts
@@ -257,6 +257,8 @@ async function buildCypressTargets(
excludeSpecPatterns
);
+ const ciBaseUrl = pluginPresetOptions?.ciBaseUrl;
+
const dependsOn: TargetConfiguration['dependsOn'] = [];
const outputs = getOutputs(projectRoot, cypressConfig, 'e2e');
const inputs = getInputs(namedInputs);
@@ -273,7 +275,9 @@ async function buildCypressTargets(
outputs,
inputs,
cache: true,
- command: `cypress run --env webServerCommand="${ciWebServerCommand}" --spec ${relativeSpecFilePath}`,
+ command: `cypress run --env webServerCommand="${ciWebServerCommand}" --spec ${relativeSpecFilePath}${
+ ciBaseUrl ? ` --config='{"baseUrl": "${ciBaseUrl}"}'` : ''
+ }`,
options: {
cwd: projectRoot,
},
diff --git a/packages/nx/src/devkit-internals.ts b/packages/nx/src/devkit-internals.ts
index 340f0373b8e08..72be433b4c674 100644
--- a/packages/nx/src/devkit-internals.ts
+++ b/packages/nx/src/devkit-internals.ts
@@ -8,7 +8,10 @@ export { getExecutorInformation } from './command-line/run/executor-utils';
export { readNxJson as readNxJsonFromDisk } from './config/nx-json';
export { calculateDefaultProjectName } from './config/calculate-default-project-name';
export { retrieveProjectConfigurationsWithAngularProjects } from './project-graph/utils/retrieve-workspace-files';
-export { mergeTargetConfigurations } from './project-graph/utils/project-configuration-utils';
+export {
+ mergeTargetConfigurations,
+ findMatchingConfigFiles,
+} from './project-graph/utils/project-configuration-utils';
export { readProjectConfigurationsFromRootMap } from './project-graph/utils/project-configuration-utils';
export { splitTarget } from './utils/split-target';
export { combineOptionsForExecutor } from './utils/params';
diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.ts
index db54257ac33e6..4696220e5057e 100644
--- a/packages/nx/src/project-graph/utils/project-configuration-utils.ts
+++ b/packages/nx/src/project-graph/utils/project-configuration-utils.ts
@@ -505,12 +505,12 @@ function mergeCreateNodesResults(
return { projectRootMap, externalNodes, rootMap, configurationSourceMaps };
}
-function findMatchingConfigFiles(
+export function findMatchingConfigFiles(
projectFiles: string[],
pattern: string,
include: string[],
exclude: string[]
-) {
+): string[] {
const matchingConfigFiles: string[] = [];
for (const file of projectFiles) {
diff --git a/packages/playwright/migrations.json b/packages/playwright/migrations.json
index 936cecd33b5ce..e53a5f3c69189 100644
--- a/packages/playwright/migrations.json
+++ b/packages/playwright/migrations.json
@@ -11,6 +11,12 @@
"version": "18.1.0-beta.3",
"description": "Remove invalid baseUrl option from @nx/playwright:playwright targets in project.json.",
"implementation": "./src/migrations/update-18-1-0/remove-baseUrl-from-project-json"
+ },
+ "19-6-0-use-serve-static-preview-for-command": {
+ "cli": "nx",
+ "version": "19.6.0-beta.0",
+ "description": "Use serve-static or preview for webServerCommand.",
+ "implementation": "./src/migrations/update-19-6-0/use-serve-static-preview-for-command"
}
}
}
diff --git a/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.spec.ts b/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.spec.ts
new file mode 100644
index 0000000000000..0122a55e20c47
--- /dev/null
+++ b/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.spec.ts
@@ -0,0 +1,234 @@
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import { ProjectGraph, type Tree } from '@nx/devkit';
+import useServeStaticPreviewForCommand from './use-serve-static-preview-for-command';
+import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
+
+let projectGraph: ProjectGraph;
+jest.mock('@nx/devkit', () => ({
+ ...jest.requireActual('@nx/devkit'),
+ createProjectGraphAsync: jest.fn().mockImplementation(async () => {
+ return projectGraph;
+ }),
+}));
+
+describe('useServeStaticPreviewForCommand', () => {
+ let tree: Tree;
+ let tempFs: TempFs;
+
+ beforeEach(() => {
+ tree = createTreeWithEmptyWorkspace();
+ tempFs = new TempFs('add-e2e-ci');
+ tree.root = tempFs.tempDir;
+ projectGraph = {
+ nodes: {},
+ dependencies: {},
+ externalNodes: {},
+ };
+ });
+
+ afterEach(() => {
+ tempFs.reset();
+ });
+
+ it('should update when it does not use serve-static for non-vite', async () => {
+ // ARRANGE
+ addProject(tree, tempFs, { noVite: true });
+
+ // ACT
+ await useServeStaticPreviewForCommand(tree);
+
+ // ASSERT
+ expect(tree.read('app-e2e/playwright.config.ts', 'utf-8'))
+ .toMatchInlineSnapshot(`
+ "import { defineConfig, devices } from '@playwright/test';
+ import { nxE2EPreset } from '@nx/playwright/preset';
+
+ import { workspaceRoot } from '@nx/devkit';
+
+ const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
+
+ export default defineConfig({
+ ...nxE2EPreset(__filename, { testDir: './src' }),
+ use: {
+ baseURL,
+ trace: 'on-first-retry',
+ },
+ webServer: {
+ command: 'npx nx run app:serve-static',
+ url: 'http://localhost:4200',
+ reuseExistingServer: !process.env.CI,
+ cwd: workspaceRoot,
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+ });
+ "
+ `);
+ });
+ it('should update when it does not use preview for vite', async () => {
+ // ARRANGE
+ addProject(tree, tempFs);
+
+ // ACT
+ await useServeStaticPreviewForCommand(tree);
+
+ // ASSERT
+ expect(tree.read('app-e2e/playwright.config.ts', 'utf-8'))
+ .toMatchInlineSnapshot(`
+ "import { defineConfig, devices } from '@playwright/test';
+ import { nxE2EPreset } from '@nx/playwright/preset';
+
+ import { workspaceRoot } from '@nx/devkit';
+
+ const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
+
+ export default defineConfig({
+ ...nxE2EPreset(__filename, { testDir: './src' }),
+ use: {
+ baseURL,
+ trace: 'on-first-retry',
+ },
+ webServer: {
+ command: 'npx nx run app:preview',
+ url: 'http://localhost:4300',
+ reuseExistingServer: !process.env.CI,
+ cwd: workspaceRoot,
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+ });
+ "
+ `);
+ });
+});
+
+const basePlaywrightConfig = (
+ appName: string
+) => `import { defineConfig, devices } from '@playwright/test';
+import { nxE2EPreset } from '@nx/playwright/preset';
+
+import { workspaceRoot } from '@nx/devkit';
+
+const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
+
+export default defineConfig({
+ ...nxE2EPreset(__filename, { testDir: './src' }),
+ use: {
+ baseURL,
+ trace: 'on-first-retry',
+ },
+ webServer: {
+ command: 'npx nx run ${appName}:serve',
+ url: 'http://localhost:4200',
+ reuseExistingServer: !process.env.CI,
+ cwd: workspaceRoot,
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+});`;
+
+const viteConfig = `///
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
+export default defineConfig({
+ root: __dirname,
+ cacheDir: '../../node_modules/.vite/app',
+ server: {
+ port: 4200,
+ host: 'localhost',
+ },
+ preview: {
+ port: 4300,
+ host: 'localhost',
+ },
+ plugins: [react(), nxViteTsPaths()],
+ // Uncomment this if you are using workers.
+ // worker: {
+ // plugins: [ nxViteTsPaths() ],
+ // },
+ build: {
+ outDir: '../../dist/app',
+ emptyOutDir: true,
+ reportCompressedSize: true,
+ commonjsOptions: {
+ transformMixedEsModules: true,
+ },
+ },
+});`;
+
+function addProject(
+ tree: Tree,
+ tempFs: TempFs,
+ overrides: {
+ noVite?: boolean;
+ } = {}
+) {
+ const appProjectConfig = {
+ name: 'app',
+ root: 'app',
+ sourceRoot: `${'app'}/src`,
+ projectType: 'application',
+ };
+
+ const e2eProjectConfig = {
+ name: `app-e2e`,
+ root: `app-e2e`,
+ sourceRoot: `app-e2e/src`,
+ projectType: 'application',
+ };
+
+ if (!overrides.noVite) {
+ tree.write(`app/vite.config.ts`, viteConfig);
+ } else {
+ tree.write(`app/webpack.config.ts`, ``);
+ }
+
+ tree.write(`app/project.json`, JSON.stringify(appProjectConfig));
+ tree.write(`app-e2e/playwright.config.ts`, basePlaywrightConfig('app'));
+ tree.write(`app-e2e/project.json`, JSON.stringify(e2eProjectConfig));
+ if (!overrides.noVite) {
+ tempFs.createFile(`app/vite.config.ts`, viteConfig);
+ } else {
+ tempFs.createFile(`app/webpack.config.ts`, ``);
+ }
+ tempFs.createFilesSync({
+ [`app/project.json`]: JSON.stringify(appProjectConfig),
+ [`app-e2e/playwright.config.ts`]: basePlaywrightConfig('app'),
+ [`app-e2e/project.json`]: JSON.stringify(e2eProjectConfig),
+ });
+
+ projectGraph.nodes['app'] = {
+ name: 'app',
+ type: 'app',
+ data: {
+ projectType: 'application',
+ root: 'app',
+ targets: {},
+ },
+ };
+
+ projectGraph.nodes[`app-e2e`] = {
+ name: `app-e2e`,
+ type: 'app',
+ data: {
+ projectType: 'application',
+ root: `app-e2e`,
+ targets: {
+ e2e: {},
+ },
+ },
+ };
+}
diff --git a/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts b/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts
new file mode 100644
index 0000000000000..c98bfff41a031
--- /dev/null
+++ b/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts
@@ -0,0 +1,142 @@
+import {
+ createProjectGraphAsync,
+ formatFiles,
+ getPackageManagerCommand,
+ joinPathFragments,
+ parseTargetString,
+ type Tree,
+ visitNotIgnoredFiles,
+} from '@nx/devkit';
+import { tsquery } from '@phenomnomnominal/tsquery';
+
+export default async function (tree: Tree) {
+ const graph = await createProjectGraphAsync();
+ visitNotIgnoredFiles(tree, '', (path) => {
+ if (!path.endsWith('playwright.config.ts')) {
+ return;
+ }
+
+ let playwrightConfigFileContents = tree.read(path, 'utf-8');
+
+ const WEBSERVER_COMMAND_SELECTOR =
+ 'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=command]) > StringLiteral';
+ let ast = tsquery.ast(playwrightConfigFileContents);
+ const nodes = tsquery(ast, WEBSERVER_COMMAND_SELECTOR, {
+ visitAllChildren: true,
+ });
+ if (!nodes.length) {
+ return;
+ }
+
+ const commandValueNode = nodes[0];
+ const command = commandValueNode.getText();
+ let project: string;
+ if (command.includes('nx run')) {
+ const NX_TARGET_REGEX = "(?<=nx run )[^']+";
+ const matches = command.match(NX_TARGET_REGEX);
+ if (!matches) {
+ return;
+ }
+ const targetString = matches[0];
+ const parsedTargetString = parseTargetString(targetString, graph);
+
+ if (
+ parsedTargetString.target === 'serve-static' ||
+ parsedTargetString.target === 'preview'
+ ) {
+ return;
+ }
+
+ project = parsedTargetString.project;
+ } else {
+ const NX_PROJECT_REGEX = "(?<=nx [^ ]+ )[^']+";
+ const matches = command.match(NX_PROJECT_REGEX);
+ if (!matches) {
+ return;
+ }
+ project = matches[0];
+ }
+
+ const pathToViteConfig = [
+ joinPathFragments(graph.nodes[project].data.root, 'vite.config.ts'),
+ joinPathFragments(graph.nodes[project].data.root, 'vite.config.js'),
+ ].find((p) => tree.exists(p));
+
+ if (!pathToViteConfig) {
+ const newCommand = `${
+ getPackageManagerCommand().exec
+ } nx run ${project}:serve-static`;
+ tree.write(
+ path,
+ `${playwrightConfigFileContents.slice(
+ 0,
+ commandValueNode.getStart()
+ )}"${newCommand}"${playwrightConfigFileContents.slice(
+ commandValueNode.getEnd()
+ )}`
+ );
+ } else {
+ const newCommand = `${
+ getPackageManagerCommand().exec
+ } nx run ${project}:preview`;
+ tree.write(
+ path,
+ `${playwrightConfigFileContents.slice(
+ 0,
+ commandValueNode.getStart()
+ )}"${newCommand}"${playwrightConfigFileContents.slice(
+ commandValueNode.getEnd()
+ )}`
+ );
+ playwrightConfigFileContents = tree.read(path, 'utf-8');
+ ast = tsquery.ast(playwrightConfigFileContents);
+
+ const BASE_URL_SELECTOR =
+ 'VariableDeclaration:has(Identifier[name=baseURL])';
+ const baseUrlNodes = tsquery(ast, BASE_URL_SELECTOR, {
+ visitAllChildren: true,
+ });
+ if (!baseUrlNodes.length) {
+ return;
+ }
+
+ const baseUrlNode = baseUrlNodes[0];
+ const newBaseUrlVariableDeclaration =
+ "baseURL = process.env['BASE_URL'] || 'http://localhost:4300';";
+ tree.write(
+ path,
+ `${playwrightConfigFileContents.slice(
+ 0,
+ baseUrlNode.getStart()
+ )}${newBaseUrlVariableDeclaration}${playwrightConfigFileContents.slice(
+ baseUrlNode.getEnd()
+ )}`
+ );
+
+ playwrightConfigFileContents = tree.read(path, 'utf-8');
+ ast = tsquery.ast(playwrightConfigFileContents);
+ const WEB_SERVER_URL_SELECTOR =
+ 'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=url]) > StringLiteral';
+ const webServerUrlNodes = tsquery(ast, WEB_SERVER_URL_SELECTOR, {
+ visitAllChildren: true,
+ });
+ if (!webServerUrlNodes.length) {
+ return;
+ }
+
+ const webServerUrlNode = webServerUrlNodes[0];
+ const newWebServerUrl = "'http://localhost:4300'";
+ tree.write(
+ path,
+ `${playwrightConfigFileContents.slice(
+ 0,
+ webServerUrlNode.getStart()
+ )}${newWebServerUrl}${playwrightConfigFileContents.slice(
+ webServerUrlNode.getEnd()
+ )}`
+ );
+ }
+ });
+
+ await formatFiles(tree);
+}
diff --git a/packages/react-native/src/generators/application/lib/add-e2e.ts b/packages/react-native/src/generators/application/lib/add-e2e.ts
index 6aff1fc81dd2f..c3e21a0f1b628 100644
--- a/packages/react-native/src/generators/application/lib/add-e2e.ts
+++ b/packages/react-native/src/generators/application/lib/add-e2e.ts
@@ -18,6 +18,8 @@ export async function addE2e(
styledModule: null,
hasStyles: false,
unitTestRunner: 'none',
+ e2eCiWebServerTarget: options.e2eWebServerTarget,
+ e2eCiBaseUrl: options.e2eWebServerAddress,
});
case 'playwright':
return addE2eReact(host, {
@@ -27,6 +29,8 @@ export async function addE2e(
styledModule: null,
hasStyles: false,
unitTestRunner: 'none',
+ e2eCiWebServerTarget: options.e2eWebServerTarget,
+ e2eCiBaseUrl: options.e2eWebServerAddress,
});
case 'detox':
const { detoxApplicationGenerator } = ensurePackage<
diff --git a/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap
index f90484aed48f0..b1b917fc9a9e8 100644
--- a/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap
+++ b/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap
@@ -210,6 +210,60 @@ nxViteTsPaths()],
});"
`;
+exports[`app not nested should add vite types to tsconfigs 1`] = `
+"
+ ///
+ import { defineConfig } from 'vite';
+ import react from '@vitejs/plugin-react';
+ import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
+
+ export default defineConfig({
+ root: __dirname,
+ cacheDir: '../node_modules/.vite/my-app',
+
+ server:{
+ port: 4200,
+ host: 'localhost',
+ },
+
+ preview:{
+ port: 4300,
+ host: 'localhost',
+ },
+
+ plugins: [react(),
+nxViteTsPaths()],
+
+ // Uncomment this if you are using workers.
+ // worker: {
+ // plugins: [ nxViteTsPaths() ],
+ // },
+
+ build: {
+ outDir: '../dist/my-app',
+ emptyOutDir: true,
+ reportCompressedSize: true,
+ commonjsOptions: {
+ transformMixedEsModules: true,
+ },
+ },
+
+
+ test: {
+ watch: false,
+ globals: true,
+ environment: 'jsdom',
+ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+
+ reporters: ['default'],
+ coverage: {
+ reportsDirectory: '../coverage/my-app',
+ provider: 'v8',
+ }
+ },
+ });"
+`;
+
exports[`app not nested should generate files 1`] = `
"// eslint-disable-next-line @typescript-eslint/no-unused-vars
import styles from './app.module.css';
@@ -228,6 +282,133 @@ export default App;
"
`;
+exports[`app not nested should setup playwright correctly for vite 1`] = `
+"import { defineConfig, devices } from '@playwright/test';
+import { nxE2EPreset } from '@nx/playwright/preset';
+
+import { workspaceRoot } from '@nx/devkit';
+
+// For CI, you may want to set BASE_URL to the deployed application.
+const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ ...nxE2EPreset(__filename, { testDir: './src' }),
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ baseURL,
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'npx nx run my-app:preview',
+ url: 'http://localhost:4300',
+ reuseExistingServer: !process.env.CI,
+ cwd: workspaceRoot
+ },
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"] },
+ },
+
+ {
+ name: "firefox",
+ use: { ...devices["Desktop Firefox"] },
+ },
+
+ {
+ name: "webkit",
+ use: { ...devices["Desktop Safari"] },
+ },
+
+ // Uncomment for mobile browsers support
+ /* {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] },
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] },
+ }, */
+
+ // Uncomment for branded browsers
+ /* {
+ name: 'Microsoft Edge',
+ use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ },
+ {
+ name: 'Google Chrome',
+ use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ } */
+ ],
+});
+"
+`;
+
+exports[`app not nested should use preview vite types to tsconfigs 1`] = `
+"
+ ///
+ import { defineConfig } from 'vite';
+ import react from '@vitejs/plugin-react';
+ import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
+
+ export default defineConfig({
+ root: __dirname,
+ cacheDir: '../node_modules/.vite/my-app',
+
+ server:{
+ port: 4200,
+ host: 'localhost',
+ },
+
+ preview:{
+ port: 4300,
+ host: 'localhost',
+ },
+
+ plugins: [react(),
+nxViteTsPaths()],
+
+ // Uncomment this if you are using workers.
+ // worker: {
+ // plugins: [ nxViteTsPaths() ],
+ // },
+
+ build: {
+ outDir: '../dist/my-app',
+ emptyOutDir: true,
+ reportCompressedSize: true,
+ commonjsOptions: {
+ transformMixedEsModules: true,
+ },
+ },
+
+
+ test: {
+ watch: false,
+ globals: true,
+ environment: 'jsdom',
+ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+
+ reporters: ['default'],
+ coverage: {
+ reportsDirectory: '../coverage/my-app',
+ provider: 'v8',
+ }
+ },
+ });"
+`;
+
exports[`app setup React app with --bundler=vite should setup targets with vite configuration 1`] = `null`;
exports[`app should add custom webpack config 1`] = `
diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts
index bd72ff56d2994..e223343c079f6 100644
--- a/packages/react/src/generators/application/application.spec.ts
+++ b/packages/react/src/generators/application/application.spec.ts
@@ -69,6 +69,78 @@ describe('app', () => {
'@nx/react/typings/cssmodule.d.ts',
'@nx/react/typings/image.d.ts',
]);
+ expect(appTree.read('my-app/vite.config.ts', 'utf-8')).toMatchSnapshot();
+ });
+
+ it('should setup cypress correctly for vite', async () => {
+ await applicationGenerator(appTree, {
+ ...schema,
+ bundler: 'vite',
+ unitTestRunner: 'vitest',
+ addPlugin: true,
+ });
+ expect(appTree.read('my-app-e2e/cypress.config.ts', 'utf-8'))
+ .toMatchInlineSnapshot(`
+ "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
+
+ import { defineConfig } from 'cypress';
+
+ export default defineConfig({
+ e2e: { ...nxE2EPreset(__filename, {"cypressDir":"src","bundler":"vite","webServerCommands":{"default":"nx run my-app:serve","production":"nx run my-app:preview"},"ciWebServerCommand":"nx run my-app:preview","ciBaseUrl":"http://localhost:4300"}),
+ baseUrl: 'http://localhost:4200' }
+ });
+ "
+ `);
+ });
+
+ it('should setup playwright correctly for vite', async () => {
+ const nxJson = readNxJson(appTree);
+ nxJson.plugins ??= [];
+ nxJson.plugins.push({
+ plugin: '@nx/vite/plugin',
+ options: {
+ buildTargetName: 'build',
+ previewTargetName: 'preview',
+ },
+ });
+ updateNxJson(appTree, nxJson);
+
+ await applicationGenerator(appTree, {
+ ...schema,
+ bundler: 'vite',
+ unitTestRunner: 'vitest',
+ e2eTestRunner: 'playwright',
+ addPlugin: true,
+ });
+ expect(
+ appTree.read('my-app-e2e/playwright.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should use preview vite types to tsconfigs', async () => {
+ await applicationGenerator(appTree, {
+ ...schema,
+ bundler: 'vite',
+ unitTestRunner: 'vitest',
+ });
+ const tsconfigApp = readJson(appTree, 'my-app/tsconfig.app.json');
+ expect(tsconfigApp.compilerOptions.types).toEqual([
+ 'node',
+ '@nx/react/typings/cssmodule.d.ts',
+ '@nx/react/typings/image.d.ts',
+ 'vite/client',
+ ]);
+ const tsconfigSpec = readJson(appTree, 'my-app/tsconfig.spec.json');
+ expect(tsconfigSpec.compilerOptions.types).toEqual([
+ 'vitest/globals',
+ 'vitest/importMeta',
+ 'vite/client',
+ 'node',
+ 'vitest',
+ '@nx/react/typings/cssmodule.d.ts',
+ '@nx/react/typings/image.d.ts',
+ ]);
+ expect(appTree.read('my-app/vite.config.ts', 'utf-8')).toMatchSnapshot();
});
it('should not overwrite default project if already set', async () => {
diff --git a/packages/react/src/generators/application/lib/add-e2e.ts b/packages/react/src/generators/application/lib/add-e2e.ts
index 2602b12ff1e4f..5da77e12ce49a 100644
--- a/packages/react/src/generators/application/lib/add-e2e.ts
+++ b/packages/react/src/generators/application/lib/add-e2e.ts
@@ -16,19 +16,18 @@ export async function addE2e(
tree: Tree,
options: NormalizedSchema
): Promise {
+ const hasNxBuildPlugin =
+ (options.bundler === 'webpack' && hasWebpackPlugin(tree)) ||
+ (options.bundler === 'vite' && hasVitePlugin(tree));
+ if (!hasNxBuildPlugin) {
+ await webStaticServeGenerator(tree, {
+ buildTarget: `${options.projectName}:build`,
+ targetName: 'serve-static',
+ spa: true,
+ });
+ }
switch (options.e2eTestRunner) {
case 'cypress': {
- const hasNxBuildPlugin =
- (options.bundler === 'webpack' && hasWebpackPlugin(tree)) ||
- (options.bundler === 'vite' && hasVitePlugin(tree));
- if (!hasNxBuildPlugin) {
- await webStaticServeGenerator(tree, {
- buildTarget: `${options.projectName}:build`,
- targetName: 'serve-static',
- spa: true,
- });
- }
-
const { configurationGenerator } = ensurePackage<
typeof import('@nx/cypress')
>('@nx/cypress', nxVersion);
@@ -60,8 +59,10 @@ export async function addE2e(
}
: undefined,
ciWebServerCommand: hasNxBuildPlugin
- ? `nx run ${options.projectName}:serve-static`
+ ? `nx run ${options.projectName}:${options.e2eCiWebServerTarget}`
: undefined,
+ ciBaseUrl:
+ options.bundler === 'vite' ? options.e2eCiBaseUrl : undefined,
});
}
case 'playwright': {
@@ -83,10 +84,10 @@ export async function addE2e(
js: false,
linter: options.linter,
setParserOptionsProject: options.setParserOptionsProject,
- webServerCommand: `${getPackageManagerCommand().exec} nx ${
- options.e2eWebServerTarget
- } ${options.name}`,
- webServerAddress: options.e2eWebServerAddress,
+ webServerCommand: `${getPackageManagerCommand().exec} nx run ${
+ options.projectName
+ }:${options.e2eCiWebServerTarget}`,
+ webServerAddress: options.e2eCiBaseUrl,
rootProject: options.rootProject,
addPlugin: options.addPlugin,
});
diff --git a/packages/react/src/generators/application/lib/normalize-options.ts b/packages/react/src/generators/application/lib/normalize-options.ts
index 136f50b820dd5..5031c90b9f303 100644
--- a/packages/react/src/generators/application/lib/normalize-options.ts
+++ b/packages/react/src/generators/application/lib/normalize-options.ts
@@ -46,32 +46,43 @@ export async function normalizeOptions(
options.rootProject = appProjectRoot === '.';
options.projectNameAndRootFormat = projectNameAndRootFormat;
+ let e2ePort = options.devServerPort ?? 4200;
+
let e2eWebServerTarget = 'serve';
+ let e2eCiWebServerTarget =
+ options.bundler === 'vite' ? 'preview' : 'serve-static';
if (options.addPlugin) {
if (nxJson.plugins) {
for (const plugin of nxJson.plugins) {
if (
options.bundler === 'vite' &&
typeof plugin === 'object' &&
- plugin.plugin === '@nx/vite/plugin' &&
- (plugin.options as VitePluginOptions).serveTargetName
+ plugin.plugin === '@nx/vite/plugin'
) {
- e2eWebServerTarget = (plugin.options as VitePluginOptions)
- .serveTargetName;
+ e2eCiWebServerTarget =
+ (plugin.options as VitePluginOptions)?.previewTargetName ??
+ e2eCiWebServerTarget;
+
+ e2eWebServerTarget =
+ (plugin.options as VitePluginOptions)?.serveTargetName ??
+ e2eWebServerTarget;
} else if (
options.bundler === 'webpack' &&
typeof plugin === 'object' &&
- plugin.plugin === '@nx/webpack/plugin' &&
- (plugin.options as WebpackPluginOptions).serveTargetName
+ plugin.plugin === '@nx/webpack/plugin'
) {
- e2eWebServerTarget = (plugin.options as WebpackPluginOptions)
- .serveTargetName;
+ e2eCiWebServerTarget =
+ (plugin.options as WebpackPluginOptions)?.serveStaticTargetName ??
+ e2eCiWebServerTarget;
+
+ e2eWebServerTarget =
+ (plugin.options as WebpackPluginOptions)?.serveTargetName ??
+ e2eWebServerTarget;
}
}
}
}
- let e2ePort = options.devServerPort ?? 4200;
if (
nxJson.targetDefaults?.[e2eWebServerTarget] &&
nxJson.targetDefaults?.[e2eWebServerTarget].options?.port
@@ -82,6 +93,10 @@ export async function normalizeOptions(
const e2eProjectName = options.rootProject ? 'e2e' : `${appProjectName}-e2e`;
const e2eProjectRoot = options.rootProject ? 'e2e' : `${appProjectRoot}-e2e`;
const e2eWebServerAddress = `http://localhost:${e2ePort}`;
+ const e2eCiBaseUrl =
+ options.bundler === 'vite'
+ ? 'http://localhost:4300'
+ : `http://localhost:${e2ePort}`;
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
@@ -104,6 +119,8 @@ export async function normalizeOptions(
e2eProjectRoot,
e2eWebServerAddress,
e2eWebServerTarget,
+ e2eCiWebServerTarget,
+ e2eCiBaseUrl,
e2ePort,
parsedTags,
fileName,
diff --git a/packages/react/src/generators/application/schema.d.ts b/packages/react/src/generators/application/schema.d.ts
index f0e26f3e9cc58..76f2426ba4720 100644
--- a/packages/react/src/generators/application/schema.d.ts
+++ b/packages/react/src/generators/application/schema.d.ts
@@ -38,6 +38,8 @@ export interface NormalizedSchema extends T {
e2eProjectRoot: string;
e2eWebServerAddress: string;
e2eWebServerTarget: string;
+ e2eCiWebServerTarget: string;
+ e2eCiBaseUrl: string;
e2ePort: number;
parsedTags: string[];
fileName: string;
diff --git a/packages/vite/migrations.json b/packages/vite/migrations.json
index 800eb7be3fa05..4c54200bab427 100644
--- a/packages/vite/migrations.json
+++ b/packages/vite/migrations.json
@@ -32,6 +32,11 @@
"version": "17.3.0-beta.0",
"description": "Move the vitest coverage thresholds in their own object if exists and add reporters.",
"implementation": "./src/migrations/update-17-3-0/vitest-coverage-and-reporters"
+ },
+ "update-19-6-0-add-depends-on-for-preview-server": {
+ "version": "19.6.0-beta.0",
+ "description": "Add dependsOn: [build] to preview targets using preview-server",
+ "implementation": "./src/migrations/update-19-6-0/add-depends-on-for-preview"
}
},
"packageJsonUpdates": {
diff --git a/packages/vite/src/migrations/update-19-6-0/add-depends-on-for-preview.spec.ts b/packages/vite/src/migrations/update-19-6-0/add-depends-on-for-preview.spec.ts
new file mode 100644
index 0000000000000..e3882e44b4bdd
--- /dev/null
+++ b/packages/vite/src/migrations/update-19-6-0/add-depends-on-for-preview.spec.ts
@@ -0,0 +1,74 @@
+import addDependsOnForPreview from './add-depends-on-for-preview';
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import { readJson } from '@nx/devkit';
+
+describe('addDependsOnForPreview', () => {
+ it('should update when preview target exists in project.json', async () => {
+ // ARRANGE
+ const tree = createTreeWithEmptyWorkspace();
+ tree.write(
+ 'apps/app/project.json',
+ JSON.stringify({
+ name: 'app',
+ root: 'apps/app',
+ projectType: 'application',
+ targets: {
+ preview: {
+ executor: '@nx/vite:preview-server',
+ },
+ },
+ })
+ );
+
+ // ACT
+ await addDependsOnForPreview(tree);
+
+ // ASSERT
+ expect(readJson(tree, 'apps/app/project.json').targets)
+ .toMatchInlineSnapshot(`
+ {
+ "preview": {
+ "dependsOn": [
+ "build",
+ ],
+ "executor": "@nx/vite:preview-server",
+ },
+ }
+ `);
+ });
+
+ it('should not update when preview target exists in project.json and has a dependsOn already', async () => {
+ // ARRANGE
+ const tree = createTreeWithEmptyWorkspace();
+ tree.write(
+ 'apps/app/project.json',
+ JSON.stringify({
+ name: 'app',
+ root: 'apps/app',
+ projectType: 'application',
+ targets: {
+ preview: {
+ dependsOn: ['build'],
+ executor: '@nx/vite:preview-server',
+ },
+ },
+ })
+ );
+
+ // ACT
+ await addDependsOnForPreview(tree);
+
+ // ASSERT
+ expect(readJson(tree, 'apps/app/project.json').targets)
+ .toMatchInlineSnapshot(`
+ {
+ "preview": {
+ "dependsOn": [
+ "build",
+ ],
+ "executor": "@nx/vite:preview-server",
+ },
+ }
+ `);
+ });
+});
diff --git a/packages/vite/src/migrations/update-19-6-0/add-depends-on-for-preview.ts b/packages/vite/src/migrations/update-19-6-0/add-depends-on-for-preview.ts
new file mode 100644
index 0000000000000..f78f0a360bc51
--- /dev/null
+++ b/packages/vite/src/migrations/update-19-6-0/add-depends-on-for-preview.ts
@@ -0,0 +1,26 @@
+import {
+ type Tree,
+ formatFiles,
+ readProjectConfiguration,
+ updateProjectConfiguration,
+} from '@nx/devkit';
+import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
+import { VitePreviewServerExecutorOptions } from '../../executors/preview-server/schema';
+
+export default async function (tree: Tree) {
+ forEachExecutorOptions(
+ tree,
+ '@nx/vite:preview-server',
+ (_, projectName, targetName) => {
+ const project = readProjectConfiguration(tree, projectName);
+ project.targets[targetName].dependsOn ??= [];
+ if (project.targets[targetName].dependsOn.includes('build')) {
+ return;
+ }
+ project.targets[targetName].dependsOn.push('build');
+ updateProjectConfiguration(tree, projectName, project);
+ }
+ );
+
+ await formatFiles(tree);
+}
diff --git a/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap
index 1b75426da5b23..1a419719c9407 100644
--- a/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap
+++ b/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap
@@ -126,6 +126,9 @@ exports[`@nx/vite/plugin not root project should create nodes 1`] = `
},
"preview-site": {
"command": "vite preview",
+ "dependsOn": [
+ "build-something",
+ ],
"metadata": {
"description": "Locally preview Vite production build",
"help": {
@@ -209,6 +212,9 @@ exports[`@nx/vite/plugin root project should create nodes 1`] = `
},
"preview": {
"command": "vite preview",
+ "dependsOn": [
+ "build",
+ ],
"metadata": {
"description": "Locally preview Vite production build",
"help": {
diff --git a/packages/vite/src/plugins/plugin.ts b/packages/vite/src/plugins/plugin.ts
index b2b4a888c72eb..2575e392c1625 100644
--- a/packages/vite/src/plugins/plugin.ts
+++ b/packages/vite/src/plugins/plugin.ts
@@ -193,7 +193,10 @@ async function buildViteTargets(
// If running in library mode, then there is nothing to serve.
if (!viteConfig.build?.lib) {
targets[options.serveTargetName] = serveTarget(projectRoot);
- targets[options.previewTargetName] = previewTarget(projectRoot);
+ targets[options.previewTargetName] = previewTarget(
+ projectRoot,
+ options.buildTargetName
+ );
targets[options.serveStaticTargetName] = serveStaticTarget(options) as {};
}
}
@@ -272,9 +275,10 @@ function serveTarget(projectRoot: string) {
return targetConfig;
}
-function previewTarget(projectRoot: string) {
+function previewTarget(projectRoot: string, buildTargetName) {
const targetConfig: TargetConfiguration = {
command: `vite preview`,
+ dependsOn: [buildTargetName],
options: {
cwd: joinPathFragments(projectRoot),
},
diff --git a/packages/vite/src/utils/generator-utils.ts b/packages/vite/src/utils/generator-utils.ts
index 0bf85413acb3c..0b7c2790f155c 100644
--- a/packages/vite/src/utils/generator-utils.ts
+++ b/packages/vite/src/utils/generator-utils.ts
@@ -204,6 +204,7 @@ export function addPreviewTarget(
// Adds a preview target.
project.targets.preview = {
+ dependsOn: ['build'],
executor: '@nx/vite:preview-server',
defaultConfiguration: 'development',
options: previewOptions,
diff --git a/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap
index a5f26bbb1e3a0..f318cac011b93 100644
--- a/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap
+++ b/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap
@@ -1,5 +1,157 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`application generator should set up project correctly for cypress 1`] = `
+"{
+ "root": true,
+ "ignorePatterns": ["**/*"],
+ "plugins": ["@nx"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],
+ "rules": {
+ "@nx/enforce-module-boundaries": [
+ "error",
+ {
+ "enforceBuildableLibDependency": true,
+ "allow": [],
+ "depConstraints": [
+ {
+ "sourceTag": "*",
+ "onlyDependOnLibsWithTags": ["*"]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "extends": ["plugin:@nx/typescript"],
+ "rules": {}
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "extends": ["plugin:@nx/javascript"],
+ "rules": {}
+ }
+ ]
+}
+"
+`;
+
+exports[`application generator should set up project correctly for cypress 2`] = `
+"///
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
+
+export default defineConfig({
+ root: __dirname,
+ cacheDir: '../node_modules/.vite/test',
+
+ server: {
+ port: 4200,
+ host: 'localhost',
+ },
+
+ preview: {
+ port: 4300,
+ host: 'localhost',
+ },
+
+ plugins: [vue(), nxViteTsPaths()],
+
+ // Uncomment this if you are using workers.
+ // worker: {
+ // plugins: [ nxViteTsPaths() ],
+ // },
+
+ build: {
+ outDir: '../dist/test',
+ emptyOutDir: true,
+ reportCompressedSize: true,
+ commonjsOptions: {
+ transformMixedEsModules: true,
+ },
+ },
+
+ test: {
+ watch: false,
+ globals: true,
+ environment: 'jsdom',
+ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+
+ reporters: ['default'],
+ coverage: {
+ reportsDirectory: '../coverage/test',
+ provider: 'v8',
+ },
+ },
+});
+"
+`;
+
+exports[`application generator should set up project correctly for cypress 3`] = `
+"{
+ "extends": [
+ "plugin:vue/vue3-essential",
+ "eslint:recommended",
+ "@vue/eslint-config-typescript",
+ "@vue/eslint-config-prettier/skip-formatting",
+ "../.eslintrc.json"
+ ],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],
+ "rules": {
+ "vue/multi-word-component-names": "off"
+ }
+ }
+ ]
+}
+"
+`;
+
+exports[`application generator should set up project correctly for cypress 4`] = `
+"import { describe, it, expect } from 'vitest';
+
+import { mount } from '@vue/test-utils';
+import App from './App.vue';
+
+describe('App', () => {
+ it('renders properly', async () => {
+ const wrapper = mount(App, {});
+
+ expect(wrapper.text()).toContain('Welcome test 👋');
+ });
+});
+"
+`;
+
+exports[`application generator should set up project correctly for cypress 5`] = `
+"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
+
+import { defineConfig } from 'cypress';
+
+export default defineConfig({
+ e2e: {
+ ...nxE2EPreset(__filename, {
+ cypressDir: 'src',
+ bundler: 'vite',
+ webServerCommands: {
+ default: 'nx run test:serve',
+ production: 'nx run test:preview',
+ },
+ ciWebServerCommand: 'nx run test:preview',
+ ciBaseUrl: 'http://localhost:4300',
+ }),
+ baseUrl: 'http://localhost:4200',
+ },
+});
+"
+`;
+
exports[`application generator should set up project correctly with PascalCase name 1`] = `
"{
"root": true,
@@ -290,6 +442,79 @@ describe('App', () => {
`;
exports[`application generator should set up project correctly with given options 5`] = `
+"import { defineConfig, devices } from '@playwright/test';
+import { nxE2EPreset } from '@nx/playwright/preset';
+
+import { workspaceRoot } from '@nx/devkit';
+
+// For CI, you may want to set BASE_URL to the deployed application.
+const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ ...nxE2EPreset(__filename, { testDir: './src' }),
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ baseURL,
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'npx nx run test:preview',
+ url: 'http://localhost:4300',
+ reuseExistingServer: !process.env.CI,
+ cwd: workspaceRoot,
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+
+ // Uncomment for mobile browsers support
+ /* {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] },
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] },
+ }, */
+
+ // Uncomment for branded browsers
+ /* {
+ name: 'Microsoft Edge',
+ use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ },
+ {
+ name: 'Google Chrome',
+ use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ } */
+ ],
+});
+"
+`;
+
+exports[`application generator should set up project correctly with given options 6`] = `
[
".eslintignore",
".eslintrc.json",
diff --git a/packages/vue/src/generators/application/application.spec.ts b/packages/vue/src/generators/application/application.spec.ts
index 3fbbccfaaaab1..aab1a17a2300c 100644
--- a/packages/vue/src/generators/application/application.spec.ts
+++ b/packages/vue/src/generators/application/application.spec.ts
@@ -1,7 +1,12 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
-import { Tree, readProjectConfiguration } from '@nx/devkit';
+import {
+ Tree,
+ readProjectConfiguration,
+ readNxJson,
+ updateNxJson,
+} from '@nx/devkit';
import { applicationGenerator } from './application';
import { Schema } from './schema';
@@ -21,14 +26,55 @@ describe('application generator', () => {
});
it('should set up project correctly with given options', async () => {
- await applicationGenerator(tree, { ...options, unitTestRunner: 'vitest' });
+ const nxJson = readNxJson(tree);
+ nxJson.plugins ??= [];
+ nxJson.plugins.push({
+ plugin: '@nx/vite/plugin',
+ options: {
+ buildTargetName: 'build',
+ previewTargetName: 'preview',
+ },
+ });
+ updateNxJson(tree, nxJson);
+ await applicationGenerator(tree, {
+ ...options,
+ unitTestRunner: 'vitest',
+ e2eTestRunner: 'playwright',
+ });
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/src/app/App.spec.ts', 'utf-8')).toMatchSnapshot();
+ expect(
+ tree.read('test-e2e/playwright.config.ts', 'utf-8')
+ ).toMatchSnapshot();
expect(listFiles(tree)).toMatchSnapshot();
});
+ it('should set up project correctly for cypress', async () => {
+ const nxJson = readNxJson(tree);
+ nxJson.plugins ??= [];
+ nxJson.plugins.push({
+ plugin: '@nx/vite/plugin',
+ options: {
+ buildTargetName: 'build',
+ previewTargetName: 'preview',
+ },
+ });
+ updateNxJson(tree, nxJson);
+ await applicationGenerator(tree, {
+ ...options,
+ addPlugin: true,
+ unitTestRunner: 'vitest',
+ e2eTestRunner: 'cypress',
+ });
+ expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
+ expect(tree.read('test/vite.config.ts', 'utf-8')).toMatchSnapshot();
+ expect(tree.read('test/.eslintrc.json', 'utf-8')).toMatchSnapshot();
+ expect(tree.read('test/src/app/App.spec.ts', 'utf-8')).toMatchSnapshot();
+ expect(tree.read('test-e2e/cypress.config.ts', 'utf-8')).toMatchSnapshot();
+ });
+
it('should set up project correctly with PascalCase name', async () => {
await applicationGenerator(tree, {
...options,
diff --git a/packages/vue/src/generators/application/lib/add-e2e.ts b/packages/vue/src/generators/application/lib/add-e2e.ts
index d71160c15c380..9268da0e2bd16 100644
--- a/packages/vue/src/generators/application/lib/add-e2e.ts
+++ b/packages/vue/src/generators/application/lib/add-e2e.ts
@@ -15,14 +15,24 @@ export async function addE2e(
tree: Tree,
options: NormalizedSchema
): Promise {
+ const nxJson = readNxJson(tree);
+ const hasPlugin = nxJson.plugins?.find((p) =>
+ typeof p === 'string'
+ ? p === '@nx/vite/plugin'
+ : p.plugin === '@nx/vite/plugin'
+ );
+ const e2eWebServerTarget = hasPlugin
+ ? typeof hasPlugin === 'string'
+ ? 'serve'
+ : (hasPlugin.options as any)?.serveTargetName ?? 'serve'
+ : 'serve';
+ const e2eCiWebServerTarget = hasPlugin
+ ? typeof hasPlugin === 'string'
+ ? 'preview'
+ : (hasPlugin.options as any)?.previewTargetName ?? 'preview'
+ : 'preview';
switch (options.e2eTestRunner) {
case 'cypress': {
- const nxJson = readNxJson(tree);
- const hasPlugin = nxJson.plugins?.some((p) =>
- typeof p === 'string'
- ? p === '@nx/vite/plugin'
- : p.plugin === '@nx/vite/plugin'
- );
if (!hasPlugin) {
await webStaticServeGenerator(tree, {
buildTarget: `${options.projectName}:build`,
@@ -48,9 +58,17 @@ export async function addE2e(
directory: 'src',
bundler: 'vite',
skipFormat: true,
- devServerTarget: `${options.projectName}:serve`,
+ devServerTarget: `${options.projectName}:${e2eWebServerTarget}`,
baseUrl: 'http://localhost:4200',
jsx: true,
+ webServerCommands: hasPlugin
+ ? {
+ default: `nx run ${options.projectName}:${e2eWebServerTarget}`,
+ production: `nx run ${options.projectName}:preview`,
+ }
+ : undefined,
+ ciWebServerCommand: `nx run ${options.projectName}:${e2eCiWebServerTarget}`,
+ ciBaseUrl: 'http://localhost:4300',
});
}
case 'playwright': {
@@ -73,10 +91,10 @@ export async function addE2e(
js: false,
linter: options.linter,
setParserOptionsProject: options.setParserOptionsProject,
- webServerCommand: `${getPackageManagerCommand().exec} nx serve ${
- options.name
- }`,
- webServerAddress: 'http://localhost:4200',
+ webServerCommand: `${getPackageManagerCommand().exec} nx run ${
+ options.projectName
+ }:${e2eCiWebServerTarget}`,
+ webServerAddress: 'http://localhost:4300',
});
}
case 'none':
diff --git a/packages/web/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/web/src/generators/application/__snapshots__/application.spec.ts.snap
index b21d49dd87823..0a8df963bc495 100644
--- a/packages/web/src/generators/application/__snapshots__/application.spec.ts.snap
+++ b/packages/web/src/generators/application/__snapshots__/application.spec.ts.snap
@@ -1,5 +1,224 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`app not nested should generate files if bundler is vite 1`] = `
+"import { defineConfig, devices } from '@playwright/test';
+import { nxE2EPreset } from '@nx/playwright/preset';
+
+import { workspaceRoot } from '@nx/devkit';
+
+// For CI, you may want to set BASE_URL to the deployed application.
+const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ ...nxE2EPreset(__filename, { testDir: './src' }),
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ baseURL,
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'npx nx run my-app:preview',
+ url: 'http://localhost:4300',
+ reuseExistingServer: !process.env.CI,
+ cwd: workspaceRoot,
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+
+ // Uncomment for mobile browsers support
+ /* {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] },
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] },
+ }, */
+
+ // Uncomment for branded browsers
+ /* {
+ name: 'Microsoft Edge',
+ use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ },
+ {
+ name: 'Google Chrome',
+ use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ } */
+ ],
+});
+"
+`;
+
+exports[`app not nested should setup playwright e2e project correctly for webpack 1`] = `
+"import { defineConfig, devices } from '@playwright/test';
+import { nxE2EPreset } from '@nx/playwright/preset';
+
+import { workspaceRoot } from '@nx/devkit';
+
+// For CI, you may want to set BASE_URL to the deployed application.
+const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ ...nxE2EPreset(__filename, { testDir: './src' }),
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ baseURL,
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'npx nx run cool-app:serve-static',
+ url: 'http://localhost:4200',
+ reuseExistingServer: !process.env.CI,
+ cwd: workspaceRoot,
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+
+ // Uncomment for mobile browsers support
+ /* {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] },
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] },
+ }, */
+
+ // Uncomment for branded browsers
+ /* {
+ name: 'Microsoft Edge',
+ use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ },
+ {
+ name: 'Google Chrome',
+ use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ } */
+ ],
+});
+"
+`;
+
+exports[`app not nested should use serve target and port if bundler=vite, e2eTestRunner=playwright, addPlugin=false 1`] = `
+"import { defineConfig, devices } from '@playwright/test';
+import { nxE2EPreset } from '@nx/playwright/preset';
+
+import { workspaceRoot } from '@nx/devkit';
+
+// For CI, you may want to set BASE_URL to the deployed application.
+const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ ...nxE2EPreset(__filename, { testDir: './src' }),
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ baseURL,
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'npx nx run my-app:preview',
+ url: 'http://localhost:4300',
+ reuseExistingServer: !process.env.CI,
+ cwd: workspaceRoot,
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+
+ // Uncomment for mobile browsers support
+ /* {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] },
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] },
+ }, */
+
+ // Uncomment for branded browsers
+ /* {
+ name: 'Microsoft Edge',
+ use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ },
+ {
+ name: 'Google Chrome',
+ use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ } */
+ ],
+});
+"
+`;
+
exports[`app setup web app with --bundler=vite should setup vite configuration 1`] = `null`;
exports[`app should setup eslint 1`] = `
diff --git a/packages/web/src/generators/application/application.legacy.spec.ts b/packages/web/src/generators/application/application.legacy.spec.ts
index 7da4e436afc3c..55616211d8b51 100644
--- a/packages/web/src/generators/application/application.legacy.spec.ts
+++ b/packages/web/src/generators/application/application.legacy.spec.ts
@@ -106,6 +106,16 @@ describe('web app generator (legacy)', () => {
"buildTarget": "my-app:build",
},
},
+ "serve-static": {
+ "dependsOn": [
+ "build",
+ ],
+ "executor": "@nx/web:file-server",
+ "options": {
+ "buildTarget": "my-app:build",
+ "spa": true,
+ },
+ },
"test": {
"executor": "@nx/jest:jest",
"options": {
@@ -179,6 +189,9 @@ describe('web app generator (legacy)', () => {
},
},
"defaultConfiguration": "development",
+ "dependsOn": [
+ "build",
+ ],
"executor": "@nx/vite:preview-server",
"options": {
"buildTarget": "my-vite-app:build",
@@ -201,6 +214,16 @@ describe('web app generator (legacy)', () => {
"buildTarget": "my-vite-app:build",
},
},
+ "serve-static": {
+ "dependsOn": [
+ "build",
+ ],
+ "executor": "@nx/web:file-server",
+ "options": {
+ "buildTarget": "my-vite-app:build",
+ "spa": true,
+ },
+ },
"test": {
"executor": "@nx/jest:jest",
"options": {
diff --git a/packages/web/src/generators/application/application.spec.ts b/packages/web/src/generators/application/application.spec.ts
index 7c46f607846b0..6aec78f324b93 100644
--- a/packages/web/src/generators/application/application.spec.ts
+++ b/packages/web/src/generators/application/application.spec.ts
@@ -1,7 +1,12 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
-import { readProjectConfiguration, Tree } from '@nx/devkit';
+import {
+ readNxJson,
+ readProjectConfiguration,
+ Tree,
+ updateNxJson,
+} from '@nx/devkit';
import { getProjects, readJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
@@ -158,11 +163,103 @@ describe('app', () => {
expect(tree.exists('cool-app-e2e/playwright.config.ts')).toBeTruthy();
});
+ it('should setup cypress e2e project correctly for vite', async () => {
+ await applicationGenerator(tree, {
+ name: 'cool-app',
+ e2eTestRunner: 'cypress',
+ unitTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ bundler: 'vite',
+ addPlugin: true,
+ });
+ expect(tree.read('cool-app-e2e/cypress.config.ts', 'utf-8'))
+ .toMatchInlineSnapshot(`
+ "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
+
+ import { defineConfig } from 'cypress';
+
+ export default defineConfig({
+ e2e: {
+ ...nxE2EPreset(__filename, {
+ cypressDir: 'src',
+ bundler: 'vite',
+ webServerCommands: {
+ default: 'nx run cool-app:serve',
+ production: 'nx run cool-app:preview',
+ },
+ ciWebServerCommand: 'nx run cool-app:preview',
+ ciBaseUrl: 'http://localhost:4300',
+ }),
+ baseUrl: 'http://localhost:4200',
+ },
+ });
+ "
+ `);
+ });
+
+ it('should setup cypress e2e project correctly for webpack', async () => {
+ await applicationGenerator(tree, {
+ name: 'cool-app',
+ e2eTestRunner: 'cypress',
+ unitTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ bundler: 'webpack',
+ addPlugin: true,
+ });
+ expect(tree.read('cool-app-e2e/cypress.config.ts', 'utf-8'))
+ .toMatchInlineSnapshot(`
+ "import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
+
+ import { defineConfig } from 'cypress';
+
+ export default defineConfig({
+ e2e: {
+ ...nxE2EPreset(__filename, {
+ cypressDir: 'src',
+ webServerCommands: {
+ default: 'nx run cool-app:serve',
+ production: 'nx run cool-app:preview',
+ },
+ ciWebServerCommand: 'nx run cool-app:serve-static',
+ }),
+ baseUrl: 'http://localhost:4200',
+ },
+ });
+ "
+ `);
+ });
+
+ it('should setup playwright e2e project correctly for webpack', async () => {
+ await applicationGenerator(tree, {
+ name: 'cool-app',
+ e2eTestRunner: 'playwright',
+ unitTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ bundler: 'webpack',
+ addPlugin: true,
+ });
+ expect(
+ tree.read('cool-app-e2e/playwright.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
it('should generate files if bundler is vite', async () => {
+ const nxJson = readNxJson(tree);
+ nxJson.plugins ??= [];
+ nxJson.plugins.push({
+ plugin: '@nx/vite/plugin',
+ options: {
+ buildTargetName: 'build',
+ previewTargetName: 'preview',
+ },
+ });
+ updateNxJson(tree, nxJson);
await applicationGenerator(tree, {
name: 'my-app',
bundler: 'vite',
projectNameAndRootFormat: 'as-provided',
+ e2eTestRunner: 'playwright',
+ addPlugin: true,
});
expect(tree.exists('my-app/src/main.ts')).toBeTruthy();
expect(tree.exists('my-app/src/app/app.element.ts')).toBeTruthy();
@@ -179,7 +276,9 @@ describe('app', () => {
path: './tsconfig.spec.json',
},
]);
- expect(tree.exists('my-app-e2e/playwright.config.ts')).toBeTruthy();
+ expect(
+ tree.read('my-app-e2e/playwright.config.ts', 'utf-8')
+ ).toMatchSnapshot();
expect(tree.exists('my-app/index.html')).toBeTruthy();
expect(tree.exists('my-app/vite.config.ts')).toBeTruthy();
expect(tree.exists(`my-app/environments/environment.ts`)).toBeFalsy();
@@ -188,6 +287,18 @@ describe('app', () => {
).toBeFalsy();
});
+ it('should use serve target and port if bundler=vite, e2eTestRunner=playwright, addPlugin=false', async () => {
+ await applicationGenerator(tree, {
+ name: 'my-app',
+ bundler: 'vite',
+ projectNameAndRootFormat: 'as-provided',
+ e2eTestRunner: 'playwright',
+ });
+ expect(
+ tree.read('my-app-e2e/playwright.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
it('should extend from root tsconfig.json when no tsconfig.base.json', async () => {
tree.rename('tsconfig.base.json', 'tsconfig.json');
diff --git a/packages/web/src/generators/application/application.ts b/packages/web/src/generators/application/application.ts
index 8547f4c899d64..5b0df944b12fc 100644
--- a/packages/web/src/generators/application/application.ts
+++ b/packages/web/src/generators/application/application.ts
@@ -41,6 +41,8 @@ import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-targ
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { VitePluginOptions } from '@nx/vite/src/plugins/plugin';
import { WebpackPluginOptions } from '@nx/webpack/src/plugins/plugin';
+import { hasVitePlugin } from '../../utils/has-vite-plugin';
+import staticServeConfiguration from '../static-serve/static-serve-configuration';
interface NormalizedSchema extends Schema {
projectName: string;
@@ -49,6 +51,8 @@ interface NormalizedSchema extends Schema {
e2eProjectRoot: string;
e2eWebServerAddress: string;
e2eWebServerTarget: string;
+ e2eCiWebServerTarget: string;
+ e2eCiBaseUrl: string;
e2ePort: number;
parsedTags: string[];
}
@@ -364,6 +368,15 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
tasks.push(lintTask);
}
+ const hasNxBuildPlugin =
+ (options.bundler === 'webpack' && hasWebpackPlugin(host)) ||
+ (options.bundler === 'vite' && hasVitePlugin(host));
+ if (!hasNxBuildPlugin) {
+ await staticServeConfiguration(host, {
+ buildTarget: `${options.projectName}:build`,
+ spa: true,
+ });
+ }
if (options.e2eTestRunner === 'cypress') {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/cypress')
@@ -383,6 +396,16 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
baseUrl: options.e2eWebServerAddress,
directory: 'src',
skipFormat: true,
+ webServerCommands: hasNxBuildPlugin
+ ? {
+ default: `nx run ${options.projectName}:${options.e2eWebServerTarget}`,
+ production: `nx run ${options.projectName}:preview`,
+ }
+ : undefined,
+ ciWebServerCommand: hasNxBuildPlugin
+ ? `nx run ${options.projectName}:${options.e2eCiWebServerTarget}`
+ : undefined,
+ ciBaseUrl: options.bundler === 'vite' ? options.e2eCiBaseUrl : undefined,
});
tasks.push(cypressTask);
} else if (options.e2eTestRunner === 'playwright') {
@@ -405,10 +428,10 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
js: false,
linter: options.linter,
setParserOptionsProject: options.setParserOptionsProject,
- webServerCommand: `${getPackageManagerCommand().exec} nx ${
- options.e2eWebServerTarget
- } ${options.name}`,
- webServerAddress: options.e2eWebServerAddress,
+ webServerCommand: `${getPackageManagerCommand().exec} nx run ${
+ options.projectName
+ }:${options.e2eCiWebServerTarget}`,
+ webServerAddress: options.e2eCiBaseUrl,
addPlugin: options.addPlugin,
});
tasks.push(playwrightTask);
@@ -493,32 +516,43 @@ async function normalizeOptions(
nxJson.useInferencePlugins !== false;
options.addPlugin ??= addPluginDefault;
+ let e2ePort = 4200;
+
let e2eWebServerTarget = 'serve';
+ let e2eCiWebServerTarget =
+ options.bundler === 'vite' ? 'preview' : 'serve-static';
if (options.addPlugin) {
if (nxJson.plugins) {
for (const plugin of nxJson.plugins) {
if (
options.bundler === 'vite' &&
typeof plugin === 'object' &&
- plugin.plugin === '@nx/vite/plugin' &&
- (plugin.options as VitePluginOptions).serveTargetName
+ plugin.plugin === '@nx/vite/plugin'
) {
- e2eWebServerTarget = (plugin.options as VitePluginOptions)
- .serveTargetName;
+ e2eCiWebServerTarget =
+ (plugin.options as VitePluginOptions)?.previewTargetName ??
+ e2eCiWebServerTarget;
+
+ e2eWebServerTarget =
+ (plugin.options as VitePluginOptions)?.serveTargetName ??
+ e2eWebServerTarget;
} else if (
options.bundler === 'webpack' &&
typeof plugin === 'object' &&
- plugin.plugin === '@nx/webpack/plugin' &&
- (plugin.options as WebpackPluginOptions).serveTargetName
+ plugin.plugin === '@nx/webpack/plugin'
) {
- e2eWebServerTarget = (plugin.options as WebpackPluginOptions)
- .serveTargetName;
+ e2eCiWebServerTarget =
+ (plugin.options as WebpackPluginOptions)?.serveStaticTargetName ??
+ e2eCiWebServerTarget;
+
+ e2eWebServerTarget =
+ (plugin.options as WebpackPluginOptions)?.serveTargetName ??
+ e2eWebServerTarget;
}
}
}
}
- let e2ePort = 4200;
if (
nxJson.targetDefaults?.[e2eWebServerTarget] &&
nxJson.targetDefaults?.[e2eWebServerTarget].options?.port
@@ -529,6 +563,10 @@ async function normalizeOptions(
const e2eProjectName = `${appProjectName}-e2e`;
const e2eProjectRoot = `${appProjectRoot}-e2e`;
const e2eWebServerAddress = `http://localhost:${e2ePort}`;
+ const e2eCiBaseUrl =
+ options.bundler === 'vite'
+ ? 'http://localhost:4300'
+ : `http://localhost:${e2ePort}`;
const npmScope = getNpmScope(host);
@@ -554,6 +592,8 @@ async function normalizeOptions(
e2eProjectName,
e2eWebServerAddress,
e2eWebServerTarget,
+ e2eCiWebServerTarget,
+ e2eCiBaseUrl,
e2ePort,
parsedTags,
};
diff --git a/packages/web/src/generators/static-serve/static-serve-configuration.spec.ts b/packages/web/src/generators/static-serve/static-serve-configuration.spec.ts
index f703a50e48f23..469796aec1027 100644
--- a/packages/web/src/generators/static-serve/static-serve-configuration.spec.ts
+++ b/packages/web/src/generators/static-serve/static-serve-configuration.spec.ts
@@ -25,6 +25,9 @@ describe('Static serve configuration generator', () => {
expect(readProjectConfiguration(tree, 'react-app').targets['serve-static'])
.toMatchInlineSnapshot(`
{
+ "dependsOn": [
+ "build",
+ ],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "react-app:build",
@@ -40,6 +43,9 @@ describe('Static serve configuration generator', () => {
readProjectConfiguration(tree, 'angular-app').targets['serve-static']
).toMatchInlineSnapshot(`
{
+ "dependsOn": [
+ "build",
+ ],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "angular-app:build",
@@ -54,6 +60,9 @@ describe('Static serve configuration generator', () => {
expect(readProjectConfiguration(tree, 'storybook').targets['serve-static'])
.toMatchInlineSnapshot(`
{
+ "dependsOn": [
+ "build-storybook",
+ ],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "storybook:build-storybook",
@@ -75,6 +84,9 @@ describe('Static serve configuration generator', () => {
readProjectConfiguration(tree, 'react-app').targets['serve-static-custom']
).toMatchInlineSnapshot(`
{
+ "dependsOn": [
+ "build",
+ ],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "react-app:build",
@@ -101,6 +113,9 @@ describe('Static serve configuration generator', () => {
readProjectConfiguration(tree, 'angular-app').targets['serve-static']
).toMatchInlineSnapshot(`
{
+ "dependsOn": [
+ "build",
+ ],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "angular-app:build",
diff --git a/packages/web/src/generators/static-serve/static-serve-configuration.ts b/packages/web/src/generators/static-serve/static-serve-configuration.ts
index 1fbf247a174ca..233d59cc8bc9a 100644
--- a/packages/web/src/generators/static-serve/static-serve-configuration.ts
+++ b/packages/web/src/generators/static-serve/static-serve-configuration.ts
@@ -23,6 +23,7 @@ interface NormalizedWebStaticServeSchema extends WebStaticServeSchema {
projectName: string;
targetName: string;
spa: boolean;
+ parsedBuildTarget: string;
}
export async function webStaticServeGenerator(
@@ -49,6 +50,7 @@ async function normalizeOptions(
targetName: options.targetName || 'serve-static',
projectName: target.project,
spa: options.spa ?? true,
+ parsedBuildTarget: target.target,
};
const projectConfig = readProjectConfiguration(tree, target.project);
@@ -109,6 +111,7 @@ function addStaticConfig(tree: Tree, opts: NormalizedWebStaticServeSchema) {
Partial
> = {
executor: '@nx/web:file-server',
+ dependsOn: [opts.parsedBuildTarget],
options: {
buildTarget: opts.buildTarget,
staticFilePath: opts.outputPath,
diff --git a/packages/web/src/utils/has-vite-plugin.ts b/packages/web/src/utils/has-vite-plugin.ts
new file mode 100644
index 0000000000000..9430f5030ea07
--- /dev/null
+++ b/packages/web/src/utils/has-vite-plugin.ts
@@ -0,0 +1,10 @@
+import { readNxJson, Tree } from '@nx/devkit';
+
+export function hasVitePlugin(tree: Tree) {
+ const nxJson = readNxJson(tree);
+ return !!nxJson.plugins?.some((p) =>
+ typeof p === 'string'
+ ? p === '@nx/vite/plugin'
+ : p.plugin === '@nx/vite/plugin'
+ );
+}
diff --git a/packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap
index a61d76d9e5f9c..ac4cb429b7f37 100644
--- a/packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap
+++ b/packages/webpack/src/plugins/__snapshots__/plugin.spec.ts.snap
@@ -105,6 +105,9 @@ exports[`@nx/webpack/plugin should create nodes 1`] = `
},
},
"serve-static": {
+ "dependsOn": [
+ "build-something",
+ ],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "build-something",
diff --git a/packages/webpack/src/plugins/plugin.ts b/packages/webpack/src/plugins/plugin.ts
index be92a147cb5ad..6db7bac14f2d2 100644
--- a/packages/webpack/src/plugins/plugin.ts
+++ b/packages/webpack/src/plugins/plugin.ts
@@ -235,6 +235,7 @@ async function createWebpackTargets(
};
targets[options.serveStaticTargetName] = {
+ dependsOn: [options.buildTargetName],
executor: '@nx/web:file-server',
options: {
buildTarget: options.buildTargetName,