feat(testing): support continuous tasks (#30632)

## Current Behavior

The Cypress and Playwright graph plugins do not infer tasks configured
to take advantage of continuous tasks (do not add the task they run to
start the app/server as a dependency of the e2e task).

## Expected Behavior

The Cypress and Playwright graph plugins should infer tasks configured
to take advantage of continuous tasks.

## Related Issue(s)

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2025-04-14 23:15:17 +02:00 committed by Jason Jean
parent 46888b294c
commit 9a60dec0de
28 changed files with 1300 additions and 17 deletions

View File

@ -444,6 +444,16 @@
} }
}, },
"migrations": { "migrations": {
"/nx-api/angular/migrations/set-continuous-option": {
"description": "Set the `continuous` option to `true` for continuous tasks.",
"file": "generated/packages/angular/migrations/set-continuous-option.json",
"hidden": false,
"name": "set-continuous-option",
"version": "21.0.0-beta.3",
"originalFilePath": "/packages/angular",
"path": "/nx-api/angular/migrations/set-continuous-option",
"type": "migration"
},
"/nx-api/angular/migrations/20.5.0-angular-eslint-package-updates": { "/nx-api/angular/migrations/20.5.0-angular-eslint-package-updates": {
"description": "", "description": "",
"file": "generated/packages/angular/migrations/20.5.0-angular-eslint-package-updates.json", "file": "generated/packages/angular/migrations/20.5.0-angular-eslint-package-updates.json",

View File

@ -439,6 +439,16 @@
} }
], ],
"migrations": [ "migrations": [
{
"description": "Set the `continuous` option to `true` for continuous tasks.",
"file": "generated/packages/angular/migrations/set-continuous-option.json",
"hidden": false,
"name": "set-continuous-option",
"version": "21.0.0-beta.3",
"originalFilePath": "/packages/angular",
"path": "angular/migrations/set-continuous-option",
"type": "migration"
},
{ {
"description": "", "description": "",
"file": "generated/packages/angular/migrations/20.5.0-angular-eslint-package-updates.json", "file": "generated/packages/angular/migrations/20.5.0-angular-eslint-package-updates.json",

View File

@ -0,0 +1,14 @@
{
"name": "set-continuous-option",
"cli": "nx",
"version": "21.0.0-beta.3",
"description": "Set the `continuous` option to `true` for continuous tasks.",
"factory": "./src/migrations/update-21-0-0/set-continuous-option",
"implementation": "/packages/angular/src/migrations/update-21-0-0/set-continuous-option.ts",
"aliases": [],
"hidden": false,
"path": "/packages/angular",
"schema": null,
"type": "migration",
"examplesFile": "#### Set `continuous` Option for Continuous Tasks\n\nThis migration sets the `continuous` option to `true` for tasks that are known to run continuously, and only if the option is not already explicitly set.\n\nSpecifically, it updates Angular targets using the following executors:\n\n- `@angular-devkit/build-angular:dev-server`\n- `@angular-devkit/build-angular:ssr-dev-server`\n- `@nx/angular:dev-server`\n- `@nx/angular:module-federation-dev-server`\n- `@nx/angular:module-federation-dev-ssr`\n\n#### Examples\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```json {% fileName=\"apps/app1/project.json\" %}\n{\n // ...\n \"targets\": {\n // ...\n \"serve\": {\n \"executor\": \"@angular-devkit/build-angular:dev-server\",\n \"options\": {\n \"buildTarget\": \"my-app:build\",\n \"port\": 4200\n }\n }\n }\n}\n```\n\n{% /tab %}\n\n{% tab label=\"After\" %}\n\n```json {% fileName=\"apps/app1/project.json\" highlightLines=[6] %}\n{\n // ...\n \"targets\": {\n // ...\n \"serve\": {\n \"continuous\": true,\n \"executor\": \"@angular-devkit/build-angular:dev-server\",\n \"options\": {\n \"buildTarget\": \"my-app:build\",\n \"port\": 4200\n }\n }\n }\n}\n```\n\n{% /tab %}\n{% /tabs %}\n\nWhen a target is already explicitly configured with a `continuous` option, the migration will not modify it:\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```json {% fileName=\"apps/app1/project.json\" highlightLines=[6] %}\n{\n // ...\n \"targets\": {\n // ...\n \"serve\": {\n \"continuous\": false,\n \"executor\": \"@nx/angular:dev-server\",\n \"options\": {\n \"buildTarget\": \"my-app:build\",\n \"port\": 4200\n }\n }\n }\n}\n```\n\n{% /tab %}\n\n{% tab label=\"After\" %}\n\n```json {% fileName=\"apps/app1/project.json\" highlightLines=[6] %}\n{\n // ...\n \"targets\": {\n // ...\n \"serve\": {\n \"continuous\": false,\n \"executor\": \"@nx/angular:dev-server\",\n \"options\": {\n \"buildTarget\": \"my-app:build\",\n \"port\": 4200\n }\n }\n }\n}\n```\n\n{% /tab %}\n{% /tabs %}\n"
}

View File

@ -356,6 +356,12 @@
}, },
"description": "Update the @angular/cli package version to ~19.2.0.", "description": "Update the @angular/cli package version to ~19.2.0.",
"factory": "./src/migrations/update-20-5-0/update-angular-cli" "factory": "./src/migrations/update-20-5-0/update-angular-cli"
},
"set-continuous-option": {
"cli": "nx",
"version": "21.0.0-beta.3",
"description": "Set the `continuous` option to `true` for continuous tasks.",
"factory": "./src/migrations/update-21-0-0/set-continuous-option"
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {

View File

@ -737,10 +737,12 @@ exports[`app nested should create project configs 1`] = `
"buildTarget": "my-app:build:production", "buildTarget": "my-app:build:production",
}, },
}, },
"continuous": true,
"defaultConfiguration": "development", "defaultConfiguration": "development",
"executor": "@angular-devkit/build-angular:dev-server", "executor": "@angular-devkit/build-angular:dev-server",
}, },
"serve-static": { "serve-static": {
"continuous": true,
"executor": "@nx/web:file-server", "executor": "@nx/web:file-server",
"options": { "options": {
"buildTarget": "my-app:build", "buildTarget": "my-app:build",
@ -853,10 +855,12 @@ exports[`app not nested should create project configs 1`] = `
"buildTarget": "my-app:build:production", "buildTarget": "my-app:build:production",
}, },
}, },
"continuous": true,
"defaultConfiguration": "development", "defaultConfiguration": "development",
"executor": "@angular-devkit/build-angular:dev-server", "executor": "@angular-devkit/build-angular:dev-server",
}, },
"serve-static": { "serve-static": {
"continuous": true,
"executor": "@nx/web:file-server", "executor": "@nx/web:file-server",
"options": { "options": {
"buildTarget": "my-app:build", "buildTarget": "my-app:build",

View File

@ -30,6 +30,7 @@ function addFileServerTarget(
const projectConfig = readProjectConfiguration(tree, options.name); const projectConfig = readProjectConfiguration(tree, options.name);
projectConfig.targets[targetName] = { projectConfig.targets[targetName] = {
continuous: true,
executor: '@nx/web:file-server', executor: '@nx/web:file-server',
options: { options: {
buildTarget: `${options.name}:build`, buildTarget: `${options.name}:build`,

View File

@ -93,6 +93,7 @@ export function createProject(tree: Tree, options: NormalizedSchema) {
defaultConfiguration: 'production', defaultConfiguration: 'production',
}, },
serve: { serve: {
continuous: true,
executor: '@angular-devkit/build-angular:dev-server', executor: '@angular-devkit/build-angular:dev-server',
options: options.port options: options.port
? { ? {

View File

@ -251,6 +251,7 @@ exports[`Host App Generator --ssr should generate the correct files 10`] = `
"serverTarget": "test:server:production", "serverTarget": "test:server:production",
}, },
}, },
"continuous": true,
"defaultConfiguration": "development", "defaultConfiguration": "development",
"executor": "@nx/angular:module-federation-dev-ssr", "executor": "@nx/angular:module-federation-dev-ssr",
} }
@ -475,6 +476,7 @@ exports[`Host App Generator --ssr should generate the correct files for standalo
"serverTarget": "test:server:production", "serverTarget": "test:server:production",
}, },
}, },
"continuous": true,
"defaultConfiguration": "development", "defaultConfiguration": "development",
"executor": "@nx/angular:module-federation-dev-ssr", "executor": "@nx/angular:module-federation-dev-ssr",
} }
@ -700,6 +702,7 @@ exports[`Host App Generator --ssr should generate the correct files for standalo
"serverTarget": "test:server:production", "serverTarget": "test:server:production",
}, },
}, },
"continuous": true,
"defaultConfiguration": "development", "defaultConfiguration": "development",
"executor": "@nx/angular:module-federation-dev-ssr", "executor": "@nx/angular:module-federation-dev-ssr",
} }
@ -910,6 +913,7 @@ exports[`Host App Generator --ssr should generate the correct files when --types
"serverTarget": "test:server:production", "serverTarget": "test:server:production",
}, },
}, },
"continuous": true,
"defaultConfiguration": "development", "defaultConfiguration": "development",
"executor": "@nx/angular:module-federation-dev-ssr", "executor": "@nx/angular:module-federation-dev-ssr",
} }

View File

@ -23,6 +23,7 @@ export function setupServeTarget(host: Tree, options: Schema) {
if (options.mfType === 'remote') { if (options.mfType === 'remote') {
appConfig.targets['serve-static'] = { appConfig.targets['serve-static'] = {
continuous: true,
executor: '@nx/web:file-server', executor: '@nx/web:file-server',
defaultConfiguration: 'production', defaultConfiguration: 'production',
options: { options: {

View File

@ -118,6 +118,7 @@ export function updateProjectConfigForBrowserBuilder(
}; };
projectConfig.targets['serve-ssr'] = { projectConfig.targets['serve-ssr'] = {
continuous: true,
executor: '@angular-devkit/build-angular:ssr-dev-server', executor: '@angular-devkit/build-angular:ssr-dev-server',
configurations: { configurations: {
development: { development: {

View File

@ -0,0 +1,102 @@
#### Set `continuous` Option for Continuous Tasks
This migration sets the `continuous` option to `true` for tasks that are known to run continuously, and only if the option is not already explicitly set.
Specifically, it updates Angular targets using the following executors:
- `@angular-devkit/build-angular:dev-server`
- `@angular-devkit/build-angular:ssr-dev-server`
- `@nx/angular:dev-server`
- `@nx/angular:module-federation-dev-server`
- `@nx/angular:module-federation-dev-ssr`
#### Examples
{% tabs %}
{% tab label="Before" %}
```json {% fileName="apps/app1/project.json" %}
{
// ...
"targets": {
// ...
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
"options": {
"buildTarget": "my-app:build",
"port": 4200
}
}
}
}
```
{% /tab %}
{% tab label="After" %}
```json {% fileName="apps/app1/project.json" highlightLines=[6] %}
{
// ...
"targets": {
// ...
"serve": {
"continuous": true,
"executor": "@angular-devkit/build-angular:dev-server",
"options": {
"buildTarget": "my-app:build",
"port": 4200
}
}
}
}
```
{% /tab %}
{% /tabs %}
When a target is already explicitly configured with a `continuous` option, the migration will not modify it:
{% tabs %}
{% tab label="Before" %}
```json {% fileName="apps/app1/project.json" highlightLines=[6] %}
{
// ...
"targets": {
// ...
"serve": {
"continuous": false,
"executor": "@nx/angular:dev-server",
"options": {
"buildTarget": "my-app:build",
"port": 4200
}
}
}
}
```
{% /tab %}
{% tab label="After" %}
```json {% fileName="apps/app1/project.json" highlightLines=[6] %}
{
// ...
"targets": {
// ...
"serve": {
"continuous": false,
"executor": "@nx/angular:dev-server",
"options": {
"buildTarget": "my-app:build",
"port": 4200
}
}
}
}
```
{% /tab %}
{% /tabs %}

View File

@ -0,0 +1,78 @@
import {
addProjectConfiguration,
readProjectConfiguration,
type Tree,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import migration, { continuousExecutors } from './set-continuous-option';
jest.mock('@nx/devkit', () => ({
...jest.requireActual('@nx/devkit'),
formatFiles: jest.fn(),
}));
describe('set-continuous-option migration', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it.each([...continuousExecutors])(
'should set continuous option to true for targets using "%s" executor',
async (executor) => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
projectType: 'application',
targets: {
serve: {
executor,
options: {},
},
},
});
await migration(tree);
const project = readProjectConfiguration(tree, 'app1');
expect(project.targets.serve.continuous).toBe(true);
}
);
it('should not change continuous option when it is already set', async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
projectType: 'application',
targets: {
serve: {
executor: '@angular-devkit/build-angular:dev-server',
continuous: false,
options: {},
},
},
});
await migration(tree);
const project = readProjectConfiguration(tree, 'app1');
expect(project.targets.serve.continuous).toBe(false);
});
it('should not modify targets using other executors', async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {},
},
},
});
await migration(tree);
const project = readProjectConfiguration(tree, 'app1');
expect(project.targets.build.continuous).toBeUndefined();
});
});

View File

@ -0,0 +1,38 @@
import {
formatFiles,
getProjects,
type Tree,
updateProjectConfiguration,
} from '@nx/devkit';
export const continuousExecutors = new Set([
'@angular-devkit/build-angular:dev-server',
'@angular-devkit/build-angular:ssr-dev-server',
'@nx/angular:dev-server',
'@nx/angular:module-federation-dev-server',
'@nx/angular:module-federation-dev-ssr',
]);
export default async function (tree: Tree) {
const projects = getProjects(tree);
for (const [projectName, projectConfig] of projects) {
let updated = false;
for (const targetConfig of Object.values(projectConfig.targets ?? {})) {
if (
continuousExecutors.has(targetConfig.executor) &&
targetConfig.continuous === undefined
) {
targetConfig.continuous = true;
updated = true;
}
}
if (updated) {
updateProjectConfiguration(tree, projectName, projectConfig);
}
}
await formatFiles(tree);
}

View File

@ -137,6 +137,7 @@ describe('@nx/angular/plugin', () => {
"command": "ng run my-app:serve:production", "command": "ng run my-app:serve:production",
}, },
}, },
"continuous": true,
"metadata": { "metadata": {
"description": "Run the "serve" target for "my-app".", "description": "Run the "serve" target for "my-app".",
"help": { "help": {
@ -435,6 +436,7 @@ describe('@nx/angular/plugin', () => {
"command": "ng run org1-app1:serve:production", "command": "ng run org1-app1:serve:production",
}, },
}, },
"continuous": true,
"metadata": { "metadata": {
"description": "Run the "serve" target for "org1-app1".", "description": "Run the "serve" target for "org1-app1".",
"help": { "help": {
@ -619,6 +621,7 @@ describe('@nx/angular/plugin', () => {
"command": "ng run org2-app1:serve:production", "command": "ng run org2-app1:serve:production",
}, },
}, },
"continuous": true,
"metadata": { "metadata": {
"description": "Run the "serve" target for "org2-app1".", "description": "Run the "serve" target for "org2-app1".",
"help": { "help": {

View File

@ -208,6 +208,7 @@ async function buildAngularProjects(
namedInputs namedInputs
); );
} else if (knownExecutors.devServer.has(angularTarget.builder)) { } else if (knownExecutors.devServer.has(angularTarget.builder)) {
targets[nxTargetName].continuous = true;
targets[nxTargetName].metadata.help.example.options = { port: 4201 }; targets[nxTargetName].metadata.help.example.options = { port: 4201 };
} else if (knownExecutors.extractI18n.has(angularTarget.builder)) { } else if (knownExecutors.extractI18n.has(angularTarget.builder)) {
targets[nxTargetName].metadata.help.example.options = { targets[nxTargetName].metadata.help.example.options = {
@ -233,6 +234,7 @@ async function buildAngularProjects(
namedInputs namedInputs
); );
} else if (knownExecutors.serveSsr.has(angularTarget.builder)) { } else if (knownExecutors.serveSsr.has(angularTarget.builder)) {
targets[nxTargetName].continuous = true;
targets[nxTargetName].metadata.help.example.options = { port: 4201 }; targets[nxTargetName].metadata.help.example.options = { port: 4201 };
} else if (knownExecutors.prerender.has(angularTarget.builder)) { } else if (knownExecutors.prerender.has(angularTarget.builder)) {
prerenderTargets.push({ target: nxTargetName, project: projectName }); prerenderTargets.push({ target: nxTargetName, project: projectName });

View File

@ -139,6 +139,7 @@ export function nxE2EPreset(
webServerCommands: options?.webServerCommands, webServerCommands: options?.webServerCommands,
ciWebServerCommand: options?.ciWebServerCommand, ciWebServerCommand: options?.ciWebServerCommand,
ciBaseUrl: options?.ciBaseUrl, ciBaseUrl: options?.ciBaseUrl,
reuseExistingServer: options?.webServerConfig?.reuseExistingServer,
}, },
async setupNodeEvents(on, config) { async setupNodeEvents(on, config) {

View File

@ -11,6 +11,8 @@ describe('@nx/cypress/plugin', () => {
let createNodesFunction = createNodesV2[1]; let createNodesFunction = createNodesV2[1];
let context: CreateNodesContext; let context: CreateNodesContext;
let tempFs: TempFs; let tempFs: TempFs;
let cwd = process.cwd();
let originalCacheProjectGraph: string | undefined;
beforeEach(async () => { beforeEach(async () => {
tempFs = new TempFs('cypress-plugin'); tempFs = new TempFs('cypress-plugin');
@ -37,12 +39,18 @@ describe('@nx/cypress/plugin', () => {
workspaceRoot: tempFs.tempDir, workspaceRoot: tempFs.tempDir,
configFiles: [], configFiles: [],
}; };
process.chdir(tempFs.tempDir);
originalCacheProjectGraph = process.env.NX_CACHE_PROJECT_GRAPH;
process.env.NX_CACHE_PROJECT_GRAPH = 'false';
}); });
afterEach(() => { afterEach(() => {
jest.resetModules(); jest.resetModules();
tempFs.cleanup(); tempFs.cleanup();
tempFs = null; tempFs = null;
process.chdir(cwd);
process.env.NX_CACHE_PROJECT_GRAPH = originalCacheProjectGraph;
}); });
afterAll(() => { afterAll(() => {
@ -90,6 +98,14 @@ describe('@nx/cypress/plugin', () => {
"command": "cypress run --env webServerCommand="nx run my-app:serve:production"", "command": "cypress run --env webServerCommand="nx run my-app:serve:production"",
}, },
}, },
"dependsOn": [
{
"projects": [
"my-app",
],
"target": "serve",
},
],
"inputs": [ "inputs": [
"default", "default",
"^production", "^production",
@ -124,7 +140,6 @@ describe('@nx/cypress/plugin', () => {
"{projectRoot}/dist/videos", "{projectRoot}/dist/videos",
"{projectRoot}/dist/screenshots", "{projectRoot}/dist/screenshots",
], ],
"parallelism": false,
}, },
"open-cypress": { "open-cypress": {
"command": "cypress open", "command": "cypress open",
@ -451,6 +466,399 @@ describe('@nx/cypress/plugin', () => {
`); `);
}); });
it('should infer dependsOn using the task run in the webServerCommands.default and ciWebServerCommand for the e2e and atomized e2e-ci targets respectively and not set parallelism to false', async () => {
mockCypressConfig(
defineConfig({
e2e: {
...nxE2EPreset(join(tempFs.tempDir, 'cypress.config.js'), {
webServerCommands: {
default: 'npx nx run my-app:serve',
production: 'npx nx run my-app:serve:production',
},
ciWebServerCommand: 'npx nx run my-app:serve-static',
}),
specPattern: '**/*.cy.ts',
videosFolder: './dist/videos',
screenshotsFolder: './dist/screenshots',
},
})
);
const nodes = await createNodesFunction(
['cypress.config.js'],
{ targetName: 'e2e' },
context
);
expect(nodes).toMatchInlineSnapshot(`
[
[
"cypress.config.js",
{
"projects": {
".": {
"metadata": {
"targetGroups": {
"E2E (CI)": [
"e2e-ci--src/test.cy.ts",
"e2e-ci",
],
},
},
"projectType": "application",
"targets": {
"e2e": {
"cache": true,
"command": "cypress run",
"configurations": {
"production": {
"command": "cypress run --env webServerCommand="npx nx run my-app:serve:production"",
},
},
"dependsOn": [
{
"projects": [
"my-app",
],
"target": "serve",
},
],
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"cypress",
],
},
],
"metadata": {
"description": "Runs Cypress Tests",
"help": {
"command": "npx cypress run --help",
"example": {
"args": [
"--dev",
"--headed",
],
},
},
"technologies": [
"cypress",
],
},
"options": {
"cwd": ".",
},
"outputs": [
"{projectRoot}/dist/videos",
"{projectRoot}/dist/screenshots",
],
},
"e2e-ci": {
"cache": true,
"dependsOn": [
{
"params": "forward",
"projects": "self",
"target": "e2e-ci--src/test.cy.ts",
},
],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"cypress",
],
},
],
"metadata": {
"description": "Runs Cypress Tests in CI",
"help": {
"command": "npx cypress run --help",
"example": {
"args": [
"--dev",
"--headed",
],
},
},
"nonAtomizedTarget": "e2e",
"technologies": [
"cypress",
],
},
"outputs": [
"{projectRoot}/dist/videos",
"{projectRoot}/dist/screenshots",
],
"parallelism": false,
},
"e2e-ci--src/test.cy.ts": {
"cache": true,
"command": "cypress run --env webServerCommand="npx nx run my-app:serve-static" --spec src/test.cy.ts --config="{\\"e2e\\":{\\"videosFolder\\":\\"dist/videos/src-test-cy-ts\\",\\"screenshotsFolder\\":\\"dist/screenshots/src-test-cy-ts\\"}}"",
"dependsOn": [
{
"projects": [
"my-app",
],
"target": "serve-static",
},
],
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"cypress",
],
},
],
"metadata": {
"description": "Runs Cypress Tests in src/test.cy.ts in CI",
"help": {
"command": "npx cypress run --help",
"example": {
"args": [
"--dev",
"--headed",
],
},
},
"technologies": [
"cypress",
],
},
"options": {
"cwd": ".",
},
"outputs": [
"{projectRoot}/dist/videos/src-test-cy-ts",
"{projectRoot}/dist/screenshots/src-test-cy-ts",
],
},
"open-cypress": {
"command": "cypress open",
"metadata": {
"description": "Opens Cypress",
"help": {
"command": "npx cypress open --help",
"example": {
"args": [
"--dev",
"--e2e",
],
},
},
"technologies": [
"cypress",
],
},
"options": {
"cwd": ".",
},
},
},
},
},
},
],
]
`);
});
it('should set parallelism to false and not infer commands in dependsOn if reuseExistingServer is false', async () => {
mockCypressConfig(
defineConfig({
e2e: {
...nxE2EPreset(join(tempFs.tempDir, 'cypress.config.js'), {
webServerCommands: {
default: 'npx nx run my-app:serve',
production: 'npx nx run my-app:serve:production',
},
ciWebServerCommand: 'npx nx run my-app:serve-static',
webServerConfig: {
reuseExistingServer: false,
},
}),
specPattern: '**/*.cy.ts',
videosFolder: './dist/videos',
screenshotsFolder: './dist/screenshots',
},
})
);
const nodes = await createNodesFunction(
['cypress.config.js'],
{ targetName: 'e2e' },
context
);
expect(nodes).toMatchInlineSnapshot(`
[
[
"cypress.config.js",
{
"projects": {
".": {
"metadata": {
"targetGroups": {
"E2E (CI)": [
"e2e-ci--src/test.cy.ts",
"e2e-ci",
],
},
},
"projectType": "application",
"targets": {
"e2e": {
"cache": true,
"command": "cypress run",
"configurations": {
"production": {
"command": "cypress run --env webServerCommand="npx nx run my-app:serve:production"",
},
},
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"cypress",
],
},
],
"metadata": {
"description": "Runs Cypress Tests",
"help": {
"command": "npx cypress run --help",
"example": {
"args": [
"--dev",
"--headed",
],
},
},
"technologies": [
"cypress",
],
},
"options": {
"cwd": ".",
},
"outputs": [
"{projectRoot}/dist/videos",
"{projectRoot}/dist/screenshots",
],
"parallelism": false,
},
"e2e-ci": {
"cache": true,
"dependsOn": [
{
"params": "forward",
"projects": "self",
"target": "e2e-ci--src/test.cy.ts",
},
],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"cypress",
],
},
],
"metadata": {
"description": "Runs Cypress Tests in CI",
"help": {
"command": "npx cypress run --help",
"example": {
"args": [
"--dev",
"--headed",
],
},
},
"nonAtomizedTarget": "e2e",
"technologies": [
"cypress",
],
},
"outputs": [
"{projectRoot}/dist/videos",
"{projectRoot}/dist/screenshots",
],
"parallelism": false,
},
"e2e-ci--src/test.cy.ts": {
"cache": true,
"command": "cypress run --env webServerCommand="npx nx run my-app:serve-static" --spec src/test.cy.ts --config="{\\"e2e\\":{\\"videosFolder\\":\\"dist/videos/src-test-cy-ts\\",\\"screenshotsFolder\\":\\"dist/screenshots/src-test-cy-ts\\"}}"",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"cypress",
],
},
],
"metadata": {
"description": "Runs Cypress Tests in src/test.cy.ts in CI",
"help": {
"command": "npx cypress run --help",
"example": {
"args": [
"--dev",
"--headed",
],
},
},
"technologies": [
"cypress",
],
},
"options": {
"cwd": ".",
},
"outputs": [
"{projectRoot}/dist/videos/src-test-cy-ts",
"{projectRoot}/dist/screenshots/src-test-cy-ts",
],
"parallelism": false,
},
"open-cypress": {
"command": "cypress open",
"metadata": {
"description": "Opens Cypress",
"help": {
"command": "npx cypress open --help",
"example": {
"args": [
"--dev",
"--e2e",
],
},
},
"technologies": [
"cypress",
],
},
"options": {
"cwd": ".",
},
},
},
},
},
},
],
]
`);
});
function mockCypressConfig(cypressConfig: Cypress.ConfigOptions) { function mockCypressConfig(cypressConfig: Cypress.ConfigOptions) {
// This isn't JS, but all that really matters here // This isn't JS, but all that really matters here
// is that the hash is different after updating the // is that the hash is different after updating the

View File

@ -36,7 +36,13 @@ export interface CypressPluginOptions {
} }
function readTargetsCache(cachePath: string): Record<string, CypressTargets> { function readTargetsCache(cachePath: string): Record<string, CypressTargets> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {}; try {
return process.env.NX_CACHE_PROJECT_GRAPH !== 'false'
? readJsonFile(cachePath)
: {};
} catch {
return {};
}
} }
function writeTargetsToCache(cachePath: string, results: CypressTargets) { function writeTargetsToCache(cachePath: string, results: CypressTargets) {
@ -257,6 +263,8 @@ async function buildCypressTargets(
const webServerCommands: Record<string, string> = const webServerCommands: Record<string, string> =
pluginPresetOptions?.webServerCommands; pluginPresetOptions?.webServerCommands;
const shouldReuseExistingServer =
pluginPresetOptions?.reuseExistingServer ?? true;
const namedInputs = getNamedInputs(projectRoot, context); const namedInputs = getNamedInputs(projectRoot, context);
@ -274,7 +282,6 @@ async function buildCypressTargets(
cache: true, cache: true,
inputs: getInputs(namedInputs), inputs: getInputs(namedInputs),
outputs: getOutputs(projectRoot, cypressConfig, 'e2e'), outputs: getOutputs(projectRoot, cypressConfig, 'e2e'),
parallelism: false,
metadata: { metadata: {
technologies: ['cypress'], technologies: ['cypress'],
description: 'Runs Cypress Tests', description: 'Runs Cypress Tests',
@ -288,7 +295,23 @@ async function buildCypressTargets(
}; };
if (webServerCommands?.default) { if (webServerCommands?.default) {
const webServerCommandTask = shouldReuseExistingServer
? parseTaskFromCommand(webServerCommands.default)
: null;
if (webServerCommandTask) {
targets[options.targetName].dependsOn = [
{
projects: [webServerCommandTask.project],
target: webServerCommandTask.target,
},
];
} else {
targets[options.targetName].parallelism = false;
}
delete webServerCommands.default; delete webServerCommands.default;
} else {
targets[options.targetName].parallelism = false;
} }
if (Object.keys(webServerCommands ?? {}).length > 0) { if (Object.keys(webServerCommands ?? {}).length > 0) {
@ -329,6 +352,9 @@ async function buildCypressTargets(
const groupName = 'E2E (CI)'; const groupName = 'E2E (CI)';
metadata = { targetGroups: { [groupName]: [] } }; metadata = { targetGroups: { [groupName]: [] } };
const ciTargetGroup = metadata.targetGroups[groupName]; const ciTargetGroup = metadata.targetGroups[groupName];
const ciWebServerCommandTask = shouldReuseExistingServer
? parseTaskFromCommand(ciWebServerCommand)
: null;
for (const file of specFiles) { for (const file of specFiles) {
const relativeSpecFilePath = normalizePath(relative(projectRoot, file)); const relativeSpecFilePath = normalizePath(relative(projectRoot, file));
@ -351,7 +377,6 @@ async function buildCypressTargets(
cwd: projectRoot, cwd: projectRoot,
env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions }, env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions },
}, },
parallelism: false,
metadata: { metadata: {
technologies: ['cypress'], technologies: ['cypress'],
description: `Runs Cypress Tests in ${relativeSpecFilePath} in CI`, description: `Runs Cypress Tests in ${relativeSpecFilePath} in CI`,
@ -368,6 +393,17 @@ async function buildCypressTargets(
projects: 'self', projects: 'self',
params: 'forward', params: 'forward',
}); });
if (ciWebServerCommandTask) {
targets[targetName].dependsOn = [
{
target: ciWebServerCommandTask.target,
projects: [ciWebServerCommandTask.project],
},
];
} else {
targets[targetName].parallelism = false;
}
} }
targets[options.ciTargetName] = { targets[options.ciTargetName] = {
@ -457,3 +493,26 @@ function getInputs(
}, },
]; ];
} }
function parseTaskFromCommand(command: string): {
project: string;
target: string;
} | null {
const nxRunRegex =
/^(?:(?:npx|yarn|bun|pnpm|pnpm exec|pnpx) )?nx run (\S+:\S+)$/;
const infixRegex = /^(?:(?:npx|yarn|bun|pnpm|pnpm exec|pnpx) )?nx (\S+ \S+)$/;
const nxRunMatch = command.match(nxRunRegex);
if (nxRunMatch) {
const [project, target] = nxRunMatch[1].split(':');
return { project, target };
}
const infixMatch = command.match(infixRegex);
if (infixMatch) {
const [target, project] = infixMatch[1].split(' ');
return { project, target };
}
return null;
}

View File

@ -40,12 +40,14 @@ exports[`@nx/next/plugin integrated projects should create nodes 1`] = `
}, },
"my-serve": { "my-serve": {
"command": "next dev", "command": "next dev",
"continuous": true,
"options": { "options": {
"cwd": "my-app", "cwd": "my-app",
}, },
}, },
"my-serve-static": { "my-serve-static": {
"command": "next start", "command": "next start",
"continuous": true,
"dependsOn": [ "dependsOn": [
"my-build", "my-build",
], ],
@ -55,6 +57,7 @@ exports[`@nx/next/plugin integrated projects should create nodes 1`] = `
}, },
"my-start": { "my-start": {
"command": "next start", "command": "next start",
"continuous": true,
"dependsOn": [ "dependsOn": [
"my-build", "my-build",
], ],
@ -117,12 +120,14 @@ exports[`@nx/next/plugin root projects should create nodes 1`] = `
}, },
"dev": { "dev": {
"command": "next dev", "command": "next dev",
"continuous": true,
"options": { "options": {
"cwd": ".", "cwd": ".",
}, },
}, },
"serve-static": { "serve-static": {
"command": "next start", "command": "next start",
"continuous": true,
"dependsOn": [ "dependsOn": [
"build", "build",
], ],
@ -132,6 +137,7 @@ exports[`@nx/next/plugin root projects should create nodes 1`] = `
}, },
"start": { "start": {
"command": "next start", "command": "next start",
"continuous": true,
"dependsOn": [ "dependsOn": [
"build", "build",
], ],

View File

@ -219,6 +219,7 @@ async function getBuildTargetConfig(
function getDevTargetConfig(projectRoot: string) { function getDevTargetConfig(projectRoot: string) {
const targetConfig: TargetConfiguration = { const targetConfig: TargetConfiguration = {
continuous: true,
command: `next dev`, command: `next dev`,
options: { options: {
cwd: projectRoot, cwd: projectRoot,
@ -230,6 +231,7 @@ function getDevTargetConfig(projectRoot: string) {
function getStartTargetConfig(options: NextPluginOptions, projectRoot: string) { function getStartTargetConfig(options: NextPluginOptions, projectRoot: string) {
const targetConfig: TargetConfiguration = { const targetConfig: TargetConfiguration = {
continuous: true,
command: `next start`, command: `next start`,
options: { options: {
cwd: projectRoot, cwd: projectRoot,

View File

@ -26,13 +26,13 @@ export default defineConfig({
webServer: { webServer: {
command: '<%= webServerCommand %>', command: '<%= webServerCommand %>',
url: '<%= webServerAddress %>', url: '<%= webServerAddress %>',
reuseExistingServer: !process.env.CI, reuseExistingServer: true,
cwd: workspaceRoot cwd: workspaceRoot
},<% } else {%> },<% } else {%>
// webServer: { // webServer: {
// command: 'npm run start', // command: 'npm run start',
// url: 'http://127.0.0.1:3000', // url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI, // reuseExistingServer: true,
// cwd: workspaceRoot, // cwd: workspaceRoot,
// },<% } %> // },<% } %>
projects: [ projects: [

View File

@ -137,7 +137,7 @@ function createTestProject(
webServer: { webServer: {
command: 'npx nx serve myapp', command: 'npx nx serve myapp',
url: 'http://localhost:4200', url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI, reuseExistingServer: true,
cwd: workspaceRoot, cwd: workspaceRoot,
}, },
projects: [ projects: [

View File

@ -8,6 +8,8 @@ describe('@nx/playwright/plugin', () => {
let createNodesFunction = createNodesV2[1]; let createNodesFunction = createNodesV2[1];
let context: CreateNodesContext; let context: CreateNodesContext;
let tempFs: TempFs; let tempFs: TempFs;
let cwd = process.cwd();
let originalCacheProjectGraph: string | undefined;
beforeEach(async () => { beforeEach(async () => {
tempFs = new TempFs('playwright-plugin'); tempFs = new TempFs('playwright-plugin');
@ -26,11 +28,16 @@ describe('@nx/playwright/plugin', () => {
workspaceRoot: tempFs.tempDir, workspaceRoot: tempFs.tempDir,
configFiles: [], configFiles: [],
}; };
process.chdir(tempFs.tempDir);
originalCacheProjectGraph = process.env.NX_CACHE_PROJECT_GRAPH;
process.env.NX_CACHE_PROJECT_GRAPH = 'false';
}); });
afterEach(() => { afterEach(() => {
// tempFs.cleanup();
jest.resetModules(); jest.resetModules();
process.chdir(cwd);
process.env.NX_CACHE_PROJECT_GRAPH = originalCacheProjectGraph;
}); });
it('should create nodes with default playwright configuration', async () => { it('should create nodes with default playwright configuration', async () => {
@ -431,6 +438,451 @@ describe('@nx/playwright/plugin', () => {
expect(targets['e2e-ci--tests/ignored/run-me.spec.ts']).not.toBeDefined(); expect(targets['e2e-ci--tests/ignored/run-me.spec.ts']).not.toBeDefined();
expect(targets['e2e-ci--not-tests/run-me.spec.ts']).not.toBeDefined(); expect(targets['e2e-ci--not-tests/run-me.spec.ts']).not.toBeDefined();
}); });
it('should infer dependsOn using the task run in the webServer.command and not set parallelism to false', async () => {
await mockPlaywrightConfig(tempFs, {
testDir: 'tests',
webServer: {
command: 'npx nx run app1:serve',
reuseExistingServer: true,
},
});
await tempFs.createFiles({
'tests/run-me.spec.ts': '',
'tests/run-me-2.spec.ts': '',
});
const results = await createNodesFunction(
['playwright.config.js'],
{
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
context
);
const project = results[0][1].projects['.'];
const { targets } = project;
expect(targets['e2e']).toMatchInlineSnapshot(`
{
"cache": true,
"command": "playwright test",
"dependsOn": [
{
"projects": [
"app1",
],
"target": "serve",
},
],
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests",
"help": {
"command": "npx playwright test --help",
"example": {
"options": {
"workers": 1,
},
},
},
"technologies": [
"playwright",
],
},
"options": {
"cwd": "{projectRoot}",
},
"outputs": [
"{projectRoot}/test-results",
],
}
`);
expect(targets['e2e-ci']).toMatchInlineSnapshot(`
{
"cache": true,
"dependsOn": [
{
"params": "forward",
"projects": "self",
"target": "e2e-ci--tests/run-me-2.spec.ts",
},
{
"params": "forward",
"projects": "self",
"target": "e2e-ci--tests/run-me.spec.ts",
},
],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests in CI",
"help": {
"command": "npx playwright test --help",
"example": {
"options": {
"workers": 1,
},
},
},
"nonAtomizedTarget": "e2e",
"technologies": [
"playwright",
],
},
"outputs": [
"{projectRoot}/test-results",
],
"parallelism": false,
}
`);
expect(project.metadata.targetGroups).toMatchInlineSnapshot(`
{
"E2E (CI)": [
"e2e-ci--tests/run-me-2.spec.ts",
"e2e-ci--tests/run-me.spec.ts",
"e2e-ci",
],
}
`);
expect(targets['e2e-ci--tests/run-me.spec.ts']).toMatchInlineSnapshot(`
{
"cache": true,
"command": "playwright test tests/run-me.spec.ts --output=test-results/tests-run-me-spec-ts",
"dependsOn": [
{
"projects": [
"app1",
],
"target": "serve",
},
],
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests in tests/run-me.spec.ts in CI",
"help": {
"command": "npx playwright test --help",
"example": {
"options": {
"workers": 1,
},
},
},
"technologies": [
"playwright",
],
},
"options": {
"cwd": "{projectRoot}",
"env": {},
},
"outputs": [
"{projectRoot}/test-results/tests-run-me-spec-ts",
],
}
`);
expect(targets['e2e-ci--tests/run-me-2.spec.ts']).toMatchInlineSnapshot(`
{
"cache": true,
"command": "playwright test tests/run-me-2.spec.ts --output=test-results/tests-run-me-2-spec-ts",
"dependsOn": [
{
"projects": [
"app1",
],
"target": "serve",
},
],
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests in tests/run-me-2.spec.ts in CI",
"help": {
"command": "npx playwright test --help",
"example": {
"options": {
"workers": 1,
},
},
},
"technologies": [
"playwright",
],
},
"options": {
"cwd": "{projectRoot}",
"env": {},
},
"outputs": [
"{projectRoot}/test-results/tests-run-me-2-spec-ts",
],
}
`);
});
it('should not set parallelism to false and should infer dependsOn using the tasks run in the different webServer.command that have reuseExistingServer set to true', async () => {
await mockPlaywrightConfig(tempFs, {
testDir: 'tests',
webServer: [
{ command: 'npx nx run app1:serve', reuseExistingServer: true },
{ command: 'npx nx run api1:serve', reuseExistingServer: true },
{ command: 'npx nx run api2:dev', reuseExistingServer: true },
{ command: 'npx nx run api3:serve', reuseExistingServer: false }, // this one should not be included in dependsOn
],
});
await tempFs.createFiles({
'tests/run-me.spec.ts': '',
'tests/run-me-2.spec.ts': '',
});
const results = await createNodesFunction(
['playwright.config.js'],
{
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
context
);
const project = results[0][1].projects['.'];
const { targets } = project;
expect(targets['e2e']).toMatchInlineSnapshot(`
{
"cache": true,
"command": "playwright test",
"dependsOn": [
{
"projects": [
"app1",
"api1",
],
"target": "serve",
},
{
"projects": [
"api2",
],
"target": "dev",
},
],
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests",
"help": {
"command": "npx playwright test --help",
"example": {
"options": {
"workers": 1,
},
},
},
"technologies": [
"playwright",
],
},
"options": {
"cwd": "{projectRoot}",
},
"outputs": [
"{projectRoot}/test-results",
],
}
`);
expect(targets['e2e-ci']).toMatchInlineSnapshot(`
{
"cache": true,
"dependsOn": [
{
"params": "forward",
"projects": "self",
"target": "e2e-ci--tests/run-me-2.spec.ts",
},
{
"params": "forward",
"projects": "self",
"target": "e2e-ci--tests/run-me.spec.ts",
},
],
"executor": "nx:noop",
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests in CI",
"help": {
"command": "npx playwright test --help",
"example": {
"options": {
"workers": 1,
},
},
},
"nonAtomizedTarget": "e2e",
"technologies": [
"playwright",
],
},
"outputs": [
"{projectRoot}/test-results",
],
"parallelism": false,
}
`);
expect(project.metadata.targetGroups).toMatchInlineSnapshot(`
{
"E2E (CI)": [
"e2e-ci--tests/run-me-2.spec.ts",
"e2e-ci--tests/run-me.spec.ts",
"e2e-ci",
],
}
`);
expect(targets['e2e-ci--tests/run-me.spec.ts']).toMatchInlineSnapshot(`
{
"cache": true,
"command": "playwright test tests/run-me.spec.ts --output=test-results/tests-run-me-spec-ts",
"dependsOn": [
{
"projects": [
"app1",
"api1",
],
"target": "serve",
},
{
"projects": [
"api2",
],
"target": "dev",
},
],
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests in tests/run-me.spec.ts in CI",
"help": {
"command": "npx playwright test --help",
"example": {
"options": {
"workers": 1,
},
},
},
"technologies": [
"playwright",
],
},
"options": {
"cwd": "{projectRoot}",
"env": {},
},
"outputs": [
"{projectRoot}/test-results/tests-run-me-spec-ts",
],
}
`);
expect(targets['e2e-ci--tests/run-me-2.spec.ts']).toMatchInlineSnapshot(`
{
"cache": true,
"command": "playwright test tests/run-me-2.spec.ts --output=test-results/tests-run-me-2-spec-ts",
"dependsOn": [
{
"projects": [
"app1",
"api1",
],
"target": "serve",
},
{
"projects": [
"api2",
],
"target": "dev",
},
],
"inputs": [
"default",
"^production",
{
"externalDependencies": [
"@playwright/test",
],
},
],
"metadata": {
"description": "Runs Playwright Tests in tests/run-me-2.spec.ts in CI",
"help": {
"command": "npx playwright test --help",
"example": {
"options": {
"workers": 1,
},
},
},
"technologies": [
"playwright",
],
},
"options": {
"cwd": "{projectRoot}",
"env": {},
},
"outputs": [
"{projectRoot}/test-results/tests-run-me-2-spec-ts",
],
}
`);
});
}); });
async function mockPlaywrightConfig( async function mockPlaywrightConfig(

View File

@ -44,7 +44,13 @@ type PlaywrightTargets = Pick<ProjectConfiguration, 'targets' | 'metadata'>;
function readTargetsCache( function readTargetsCache(
cachePath: string cachePath: string
): Record<string, PlaywrightTargets> { ): Record<string, PlaywrightTargets> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {}; try {
return process.env.NX_CACHE_PROJECT_GRAPH !== 'false'
? readJsonFile(cachePath)
: {};
} catch {
return {};
}
} }
function writeTargetsToCache( function writeTargetsToCache(
@ -159,12 +165,12 @@ async function buildPlaywrightTargets(
const testOutput = getTestOutput(playwrightConfig); const testOutput = getTestOutput(playwrightConfig);
const reporterOutputs = getReporterOutputs(playwrightConfig); const reporterOutputs = getReporterOutputs(playwrightConfig);
const webserverCommandTasks = getWebserverCommandTasks(playwrightConfig);
const baseTargetConfig: TargetConfiguration = { const baseTargetConfig: TargetConfiguration = {
command: 'playwright test', command: 'playwright test',
options: { options: {
cwd: '{projectRoot}', cwd: '{projectRoot}',
}, },
parallelism: false,
metadata: { metadata: {
technologies: ['playwright'], technologies: ['playwright'],
description: 'Runs Playwright Tests', description: 'Runs Playwright Tests',
@ -179,6 +185,12 @@ async function buildPlaywrightTargets(
}, },
}; };
if (webserverCommandTasks.length) {
baseTargetConfig.dependsOn = getDependsOn(webserverCommandTasks);
} else {
baseTargetConfig.parallelism = false;
}
targets[options.targetName] = { targets[options.targetName] = {
...baseTargetConfig, ...baseTargetConfig,
cache: true, cache: true,
@ -438,6 +450,74 @@ function addSubfolderToOutput(output: string, subfolder?: string): string {
return join(output, subfolder); return join(output, subfolder);
} }
function getWebserverCommandTasks(
playwrightConfig: PlaywrightTestConfig
): Array<{ project: string; target: string }> {
if (!playwrightConfig.webServer) {
return [];
}
const tasks: Array<{ project: string; target: string }> = [];
const webServer = Array.isArray(playwrightConfig.webServer)
? playwrightConfig.webServer
: [playwrightConfig.webServer];
for (const server of webServer) {
if (!server.reuseExistingServer) {
continue;
}
const task = parseTaskFromCommand(server.command);
if (task) {
tasks.push(task);
}
}
return tasks;
}
function parseTaskFromCommand(command: string): {
project: string;
target: string;
} | null {
const nxRunRegex =
/^(?:(?:npx|yarn|bun|pnpm|pnpm exec|pnpx) )?nx run (\S+:\S+)$/;
const infixRegex = /^(?:(?:npx|yarn|bun|pnpm|pnpm exec|pnpx) )?nx (\S+ \S+)$/;
const nxRunMatch = command.match(nxRunRegex);
if (nxRunMatch) {
const [project, target] = nxRunMatch[1].split(':');
return { project, target };
}
const infixMatch = command.match(infixRegex);
if (infixMatch) {
const [target, project] = infixMatch[1].split(' ');
return { project, target };
}
return null;
}
function getDependsOn(
tasks: Array<{ project: string; target: string }>
): TargetConfiguration['dependsOn'] {
const projectsPerTask = new Map<string, string[]>();
for (const { project, target } of tasks) {
if (!projectsPerTask.has(target)) {
projectsPerTask.set(target, []);
}
projectsPerTask.get(target).push(project);
}
return Array.from(projectsPerTask.entries()).map(([target, projects]) => ({
projects,
target,
}));
}
function normalizeOutput( function normalizeOutput(
path: string, path: string,
workspaceRoot: string, workspaceRoot: string,

View File

@ -180,7 +180,7 @@ describe('app', () => {
webServer: { webServer: {
command: '${packageCmd} nx run my-app:preview', command: '${packageCmd} nx run my-app:preview',
url: 'http://localhost:4300', url: 'http://localhost:4300',
reuseExistingServer: !process.env.CI, reuseExistingServer: true,
cwd: workspaceRoot cwd: workspaceRoot
}, },
projects: [ projects: [

View File

@ -182,7 +182,7 @@ export default defineConfig({
webServer: { webServer: {
command: 'npx nx run test:serve-static', command: 'npx nx run test:serve-static',
url: 'http://localhost:3000', url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: true,
cwd: workspaceRoot, cwd: workspaceRoot,
}, },
projects: [ projects: [
@ -712,7 +712,7 @@ export default defineConfig({
webServer: { webServer: {
command: 'npx nx run test:serve-static', command: 'npx nx run test:serve-static',
url: 'http://localhost:3000', url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: true,
cwd: workspaceRoot, cwd: workspaceRoot,
}, },
projects: [ projects: [

View File

@ -364,7 +364,7 @@ export default defineConfig({
webServer: { webServer: {
command: 'npx nx run test:preview', command: 'npx nx run test:preview',
url: 'http://localhost:4300', url: 'http://localhost:4300',
reuseExistingServer: !process.env.CI, reuseExistingServer: true,
cwd: workspaceRoot, cwd: workspaceRoot,
}, },
projects: [ projects: [

View File

@ -29,7 +29,7 @@ export default defineConfig({
webServer: { webServer: {
command: 'npx nx run my-app:preview', command: 'npx nx run my-app:preview',
url: 'http://localhost:4300', url: 'http://localhost:4300',
reuseExistingServer: !process.env.CI, reuseExistingServer: true,
cwd: workspaceRoot, cwd: workspaceRoot,
}, },
projects: [ projects: [
@ -101,7 +101,7 @@ export default defineConfig({
webServer: { webServer: {
command: 'npx nx run cool-app:serve-static', command: 'npx nx run cool-app:serve-static',
url: 'http://localhost:4200', url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI, reuseExistingServer: true,
cwd: workspaceRoot, cwd: workspaceRoot,
}, },
projects: [ projects: [
@ -173,7 +173,7 @@ export default defineConfig({
webServer: { webServer: {
command: 'npx nx run my-app:preview', command: 'npx nx run my-app:preview',
url: 'http://localhost:4300', url: 'http://localhost:4300',
reuseExistingServer: !process.env.CI, reuseExistingServer: true,
cwd: workspaceRoot, cwd: workspaceRoot,
}, },
projects: [ projects: [