fix(testing): adding e2e projects should register e2e-ci targetDefaults (#27185)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->



## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Colum Ferry 2024-08-07 17:25:32 +01:00 committed by GitHub
parent e74db498ca
commit dfd7241ed5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 2425 additions and 82 deletions

View File

@ -569,7 +569,8 @@ describe('app', () => {
it('should add eslint plugin and no lint target to e2e project', async () => {
await generateApp(appTree, 'my-app', { linter: Linter.EsLint });
expect(readNxJson(appTree).plugins).toMatchInlineSnapshot(`
const nxJson = readNxJson(appTree);
expect(nxJson.plugins).toMatchInlineSnapshot(`
[
{
"options": {
@ -588,6 +589,13 @@ describe('app', () => {
},
]
`);
expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
expect(
readProjectConfiguration(appTree, 'my-app-e2e').targets.lint
).toBeUndefined();

View File

@ -12,6 +12,7 @@ import {
import { nxVersion } from '../../../utils/versions';
import { getInstalledAngularVersionInfo } from '../../utils/version-utils';
import type { NormalizedSchema } from './normalized-schema';
import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export async function addE2e(tree: Tree, options: NormalizedSchema) {
// since e2e are separate projects, default to adding plugins
@ -45,6 +46,14 @@ export async function addE2e(tree: Tree, options: NormalizedSchema) {
rootProject: options.rootProject,
addPlugin,
});
if (addPlugin) {
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
'^build',
joinPathFragments(options.e2eProjectRoot, 'cypress.config.ts')
);
}
} else if (options.e2eTestRunner === 'playwright') {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/playwright')
@ -71,6 +80,14 @@ export async function addE2e(tree: Tree, options: NormalizedSchema) {
rootProject: options.rootProject,
addPlugin,
});
if (addPlugin) {
await addE2eCiTargetDefaults(
tree,
'@nx/playwright/plugin',
'^build',
joinPathFragments(options.e2eProjectRoot, 'playwright.config.ts')
);
}
}
}

View File

@ -2,7 +2,7 @@ import { addProjectConfiguration, joinPathFragments, Tree } from '@nx/devkit';
import type { AngularProjectConfiguration } from '../../../utils/types';
import { getInstalledAngularVersionInfo } from '../../utils/version-utils';
import type { NormalizedSchema } from './normalized-schema';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export function createProject(tree: Tree, options: NormalizedSchema) {
const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree);

View File

@ -2,7 +2,7 @@ import type { Tree } from '@nx/devkit';
import { addProjectConfiguration, joinPathFragments } from '@nx/devkit';
import type { AngularProjectConfiguration } from '../../../utils/types';
import type { NormalizedSchema } from './normalized-schema';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export function addProject(
tree: Tree,

View File

@ -35,6 +35,12 @@
"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"
},
"update-19-6-0-add-e2e-ci-target-defaults": {
"cli": "nx",
"version": "19.6.0-beta.0",
"description": "Add inferred ciTargetNames to targetDefaults with dependsOn to ensure dependent application builds are scheduled before atomized tasks.",
"implementation": "./src/migrations/update-19-6-0/add-e2e-ci-target-defaults"
}
},
"packageJsonUpdates": {

View File

@ -0,0 +1,366 @@
import { ProjectGraph, readNxJson, type Tree, updateNxJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
describe('add-e2e-ci-target-defaults', () => {
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 when the plugin is not registered', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = [];
updateNxJson(tree, nxJson);
// ACT
await addE2eCiTargetDefaults(tree);
// ASSERT
expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"lint": {
"cache": true,
},
}
`);
});
it('should add the targetDefaults with the correct ciTargetName and buildTarget when there is one plugin', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = [
{
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
},
];
updateNxJson(tree, nxJson);
addProject(tree, tempFs);
// ACT
await addE2eCiTargetDefaults(tree);
// ASSERT
expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"e2e-ci--**/*": {
"dependsOn": [
"^build",
],
},
"lint": {
"cache": true,
},
}
`);
});
it('should add the targetDefaults with the correct ciTargetNames and buildTargets when there is more than one plugin', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = [
{
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
include: ['app-e2e/**'],
},
{
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'cypress:e2e-ci',
},
include: ['shop-e2e/**'],
},
];
updateNxJson(tree, nxJson);
addProject(tree, tempFs);
addProject(tree, tempFs, {
buildTargetName: 'build',
ciTargetName: 'cypress:e2e-ci',
appName: 'shop',
});
// ACT
await addE2eCiTargetDefaults(tree);
// ASSERT
expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"cypress:e2e-ci--**/*": {
"dependsOn": [
"^build",
],
},
"e2e-ci--**/*": {
"dependsOn": [
"^build",
],
},
"lint": {
"cache": true,
},
}
`);
});
it('should only add the targetDefaults with the correct ciTargetName and buildTargets when there is more than one plugin with only one matching multiple projects', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = [
{
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
include: ['cart-e2e/**'],
},
{
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'cypress:e2e-ci',
},
},
];
updateNxJson(tree, nxJson);
addProject(tree, tempFs);
addProject(tree, tempFs, {
buildTargetName: 'bundle',
ciTargetName: 'cypress:e2e-ci',
appName: 'shop',
});
// ACT
await addE2eCiTargetDefaults(tree);
// ASSERT
expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"cypress:e2e-ci--**/*": {
"dependsOn": [
"^build",
"^bundle",
],
},
"lint": {
"cache": true,
},
}
`);
});
it('should not add the targetDefaults when the ciWebServerCommand is not present', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = [
{
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'cypress:e2e-ci',
},
},
];
updateNxJson(tree, nxJson);
addProject(tree, tempFs, {
appName: 'app',
buildTargetName: 'build',
ciTargetName: 'cypress:e2e-ci',
noCi: true,
});
// ACT
await addE2eCiTargetDefaults(tree);
// ASSERT
expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"lint": {
"cache": true,
},
}
`);
});
});
function addProject(
tree: Tree,
tempFs: TempFs,
overrides: {
ciTargetName: string;
buildTargetName: string;
appName: string;
noCi?: boolean;
} = { ciTargetName: 'e2e-ci', buildTargetName: 'build', appName: 'app' }
) {
const appProjectConfig = {
name: overrides.appName,
root: overrides.appName,
sourceRoot: `${overrides.appName}/src`,
projectType: 'application',
};
const viteConfig = `/// <reference types='vitest' />
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',
},
});
`;
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)
);
tempFs.createFilesSync({
[`${overrides.appName}/vite.config.ts`]: viteConfig,
[`${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]: {},
},
},
};
}

View File

@ -0,0 +1,102 @@
import {
type Tree,
type CreateNodesV2,
formatFiles,
readNxJson,
createProjectGraphAsync,
parseTargetString,
} from '@nx/devkit';
import { addE2eCiTargetDefaults as _addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/internal-api';
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
import {
ProjectConfigurationsError,
retrieveProjectConfigurations,
} from 'nx/src/devkit-internals';
import { tsquery } from '@phenomnomnominal/tsquery';
import { type CypressPluginOptions } from '../../plugins/plugin';
export default async function addE2eCiTargetDefaults(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
);
if (!matchingPluginRegistrations) {
return;
}
const {
createNodesV2,
}: { createNodesV2: CreateNodesV2<CypressPluginOptions> } = 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 serveStaticTarget = graph.nodes[project].data.targets[target];
let resolvedBuildTarget: string;
if (serveStaticTarget.dependsOn) {
resolvedBuildTarget = serveStaticTarget.dependsOn.join(',');
} else {
resolvedBuildTarget =
(configuration
? serveStaticTarget.configurations[configuration].buildTarget
: serveStaticTarget.options.buildTarget) ?? 'build';
}
const buildTarget = `^${resolvedBuildTarget}`;
await _addE2eCiTargetDefaults(tree, pluginName, buildTarget, configFile);
}
}
await formatFiles(tree);
}

View File

@ -1,19 +0,0 @@
import { readNxJson, Tree, updateNxJson } from 'nx/src/devkit-exports';
export function addBuildTargetDefaults(
tree: Tree,
executorName: string,
buildTargetName = 'build'
): void {
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults[executorName] ??= {
cache: true,
dependsOn: [`^${buildTargetName}`],
inputs:
nxJson.namedInputs && 'production' in nxJson.namedInputs
? ['production', '^production']
: ['default', '^default'],
};
updateNxJson(tree, nxJson);
}

View File

@ -0,0 +1,377 @@
import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports';
import { readNxJson, updateNxJson, type Tree } from 'nx/src/devkit-exports';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import { addE2eCiTargetDefaults } from './target-defaults-utils';
describe('target-defaults-utils', () => {
describe('addE2eCiTargetDefaults', () => {
let tree: Tree;
let tempFs: TempFs;
beforeEach(() => {
tempFs = new TempFs('target-defaults-utils');
tree = createTreeWithEmptyWorkspace();
tree.root = tempFs.tempDir;
});
afterEach(() => {
tempFs.cleanup();
jest.resetModules();
});
it('should add e2e-ci--**/* target default for e2e plugin for specified build target when it does not exist', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
});
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
'^build',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
const newNxJson = readNxJson(tree);
expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
});
it('should update existing e2e-ci--**/* target default for e2e plugin for specified build target when it does not exist in dependsOn', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
});
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['e2e-ci--**/*'] = {
dependsOn: ['^build'],
};
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
'^build-base',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
const newNxJson = readNxJson(tree);
expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
"^build-base",
],
}
`);
});
it('should read the ciTargetName and add a new entry when it does not exist', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'cypress:e2e-ci',
},
});
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['e2e-ci--**/*'] = {
dependsOn: ['^build'],
};
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
'^build-base',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
const newNxJson = readNxJson(tree);
expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
expect(newNxJson.targetDefaults['cypress:e2e-ci--**/*'])
.toMatchInlineSnapshot(`
{
"dependsOn": [
"^build-base",
],
}
`);
});
it('should not add additional e2e-ci--**/* target default for e2e plugin when it already exists with build target', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
});
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['e2e-ci--**/*'] = {
dependsOn: ['^build'],
};
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
'^build',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
const newNxJson = readNxJson(tree);
expect(newNxJson.targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"e2e-ci--**/*": {
"dependsOn": [
"^build",
],
},
"lint": {
"cache": true,
},
}
`);
});
it('should do nothing when there are no nxJson.plugins does not exist', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = undefined;
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
'^build',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
const newNxJson = readNxJson(tree);
expect(newNxJson.targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"lint": {
"cache": true,
},
}
`);
});
it('should do nothing when there are nxJson.plugins but e2e plugin is not registered', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/playwright/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
});
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
'^build',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
const newNxJson = readNxJson(tree);
expect(newNxJson.targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"lint": {
"cache": true,
},
}
`);
});
it('should choose the correct plugin when there are includes', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
include: ['libs/**'],
});
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'cypress:e2e-ci',
},
include: ['apps/**'],
});
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
'^build',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
const newNxJson = readNxJson(tree);
expect(newNxJson.targetDefaults['cypress:e2e-ci--**/*'])
.toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
});
it('should choose the correct plugin when there are excludes', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
exclude: ['apps/**'],
});
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'cypress:e2e-ci',
},
exclude: ['libs/**'],
});
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
'^build',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
const newNxJson = readNxJson(tree);
expect(newNxJson.targetDefaults['cypress:e2e-ci--**/*'])
.toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
});
it('should use the default name when the plugin registration is a string', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push('@nx/cypress/plugin');
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
'^build',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
const newNxJson = readNxJson(tree);
expect(newNxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
});
});
});

View File

@ -0,0 +1,98 @@
import {
type CreateNodes,
type CreateNodesV2,
type PluginConfiguration,
type Tree,
readNxJson,
updateNxJson,
} from 'nx/src/devkit-exports';
import { findMatchingConfigFiles } from 'nx/src/devkit-internals';
export function addBuildTargetDefaults(
tree: Tree,
executorName: string,
buildTargetName = 'build'
): void {
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults[executorName] ??= {
cache: true,
dependsOn: [`^${buildTargetName}`],
inputs:
nxJson.namedInputs && 'production' in nxJson.namedInputs
? ['production', '^production']
: ['default', '^default'],
};
updateNxJson(tree, nxJson);
}
export async function addE2eCiTargetDefaults(
tree: Tree,
e2ePlugin: string,
buildTarget: string,
pathToE2EConfigFile: string
): Promise<void> {
const nxJson = readNxJson(tree);
if (!nxJson.plugins) {
return;
}
const e2ePluginRegistrations = nxJson.plugins.filter((p) =>
typeof p === 'string' ? p === e2ePlugin : p.plugin === e2ePlugin
);
if (!e2ePluginRegistrations.length) {
return;
}
const resolvedE2ePlugin: {
createNodes?: CreateNodes;
createNodesV2?: CreateNodesV2;
} = await import(e2ePlugin);
const e2ePluginGlob =
resolvedE2ePlugin.createNodesV2?.[0] ?? resolvedE2ePlugin.createNodes?.[0];
let foundPluginForApplication: PluginConfiguration;
for (let i = 0; i < e2ePluginRegistrations.length; i++) {
let candidatePluginForApplication = e2ePluginRegistrations[i];
if (typeof candidatePluginForApplication === 'string') {
foundPluginForApplication = candidatePluginForApplication;
break;
}
const matchingConfigFiles = findMatchingConfigFiles(
[pathToE2EConfigFile],
e2ePluginGlob,
candidatePluginForApplication.include,
candidatePluginForApplication.exclude
);
if (matchingConfigFiles.length) {
foundPluginForApplication = candidatePluginForApplication;
break;
}
}
if (!foundPluginForApplication) {
return;
}
const ciTargetName =
typeof foundPluginForApplication === 'string'
? 'e2e-ci'
: (foundPluginForApplication.options as any)?.ciTargetName ?? 'e2e-ci';
const ciTargetNameGlob = `${ciTargetName}--**/*`;
nxJson.targetDefaults ??= {};
const e2eCiTargetDefaults = nxJson.targetDefaults[ciTargetNameGlob];
if (!e2eCiTargetDefaults) {
nxJson.targetDefaults[ciTargetNameGlob] = {
dependsOn: [buildTarget],
};
} else {
e2eCiTargetDefaults.dependsOn ??= [];
if (!e2eCiTargetDefaults.dependsOn.includes(buildTarget)) {
e2eCiTargetDefaults.dependsOn.push(buildTarget);
}
}
updateNxJson(tree, nxJson);
}

View File

@ -0,0 +1,173 @@
import { type Tree, readNxJson, updateNxJson } from 'nx/src/devkit-exports';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports';
import { findPluginForConfigFile } from './find-plugin-for-config-file';
describe('find-plugin-for-config-file', () => {
let tree: Tree;
let tempFs: TempFs;
beforeEach(() => {
tempFs = new TempFs('target-defaults-utils');
tree = createTreeWithEmptyWorkspace();
tree.root = tempFs.tempDir;
});
afterEach(() => {
tempFs.cleanup();
jest.resetModules();
});
it('should return the plugin when its registered as just a string', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push('@nx/cypress/plugin');
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
const plugin = await findPluginForConfigFile(
tree,
'@nx/cypress/plugin',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
expect(plugin).toBeTruthy();
expect(plugin).toEqual('@nx/cypress/plugin');
});
it('should return the plugin when it does not have an include or exclude', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
});
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
const plugin = await findPluginForConfigFile(
tree,
'@nx/cypress/plugin',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
expect(plugin).toBeTruthy();
expect(plugin).toMatchInlineSnapshot(`
{
"options": {
"ciTargetName": "e2e-ci",
"targetName": "e2e",
},
"plugin": "@nx/cypress/plugin",
}
`);
});
it('should return the plugin when it the includes finds the config file', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
include: ['libs/**'],
});
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'cypress:e2e-ci',
},
include: ['apps/**'],
});
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
const plugin = await findPluginForConfigFile(
tree,
'@nx/cypress/plugin',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
expect(plugin).toBeTruthy();
expect(plugin).toMatchInlineSnapshot(`
{
"include": [
"apps/**",
],
"options": {
"ciTargetName": "cypress:e2e-ci",
"targetName": "e2e",
},
"plugin": "@nx/cypress/plugin",
}
`);
});
it('should return a valid plugin when it the excludes does not include the config file', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'cypress:e2e-ci',
},
exclude: ['apps/**'],
});
nxJson.plugins.push({
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
exclude: ['libs/**'],
});
updateNxJson(tree, nxJson);
tree.write('apps/myapp-e2e/cypress.config.ts', '');
await tempFs.createFile('apps/myapp-e2e/cypress.config.ts', '');
// ACT
const plugin = await findPluginForConfigFile(
tree,
'@nx/cypress/plugin',
'apps/myapp-e2e/cypress.config.ts'
);
// ASSERT
expect(plugin).toBeTruthy();
expect(plugin).toMatchInlineSnapshot(`
{
"exclude": [
"libs/**",
],
"options": {
"ciTargetName": "e2e-ci",
"targetName": "e2e",
},
"plugin": "@nx/cypress/plugin",
}
`);
});
});

View File

@ -0,0 +1,50 @@
import {
type Tree,
type PluginConfiguration,
readNxJson,
CreateNodes,
CreateNodesV2,
} from 'nx/src/devkit-exports';
import { findMatchingConfigFiles } from 'nx/src/devkit-internals';
export async function findPluginForConfigFile(
tree: Tree,
pluginName: string,
pathToConfigFile: string
): Promise<PluginConfiguration> {
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;
}
}
}
}

View File

@ -12,7 +12,7 @@ import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { esbuildInitGenerator } from '../init/init';
import { EsBuildExecutorOptions } from '../../executors/esbuild/schema';
import { EsBuildProjectSchema } from './schema';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export async function configurationGenerator(
tree: Tree,

View File

@ -3,6 +3,7 @@ import 'nx/src/internal-testing-utils/mock-project-graph';
import {
getProjects,
readJson,
readNxJson,
readProjectConfiguration,
Tree,
} from '@nx/devkit';
@ -282,4 +283,56 @@ describe('app', () => {
});
});
});
describe('cypress', () => {
it('should create e2e app with e2e-ci targetDefaults', async () => {
await expoApplicationGenerator(appTree, {
name: 'my-app',
directory: 'my-dir',
linter: Linter.EsLint,
e2eTestRunner: 'cypress',
js: false,
skipFormat: false,
unitTestRunner: 'none',
projectNameAndRootFormat: 'as-provided',
addPlugin: true,
});
// ASSERT
const nxJson = readNxJson(appTree);
expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(`
{
"dependsOn": [
"^export",
],
}
`);
});
});
describe('playwright', () => {
it('should create e2e app with e2e-ci targetDefaults', async () => {
await expoApplicationGenerator(appTree, {
name: 'my-app',
directory: 'my-dir',
linter: Linter.EsLint,
e2eTestRunner: 'playwright',
js: false,
skipFormat: false,
unitTestRunner: 'none',
projectNameAndRootFormat: 'as-provided',
addPlugin: true,
});
// ASSERT
const nxJson = readNxJson(appTree);
expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchInlineSnapshot(`
{
"dependsOn": [
"^export",
],
}
`);
});
});
});

View File

@ -4,12 +4,15 @@ import {
ensurePackage,
getPackageManagerCommand,
joinPathFragments,
readNxJson,
} from '@nx/devkit';
import { webStaticServeGenerator } from '@nx/web';
import { nxVersion } from '../../../utils/versions';
import { hasExpoPlugin } from '../../../utils/has-expo-plugin';
import { NormalizedSchema } from './normalize-options';
import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file';
export async function addE2e(
tree: Tree,
@ -18,8 +21,7 @@ export async function addE2e(
const hasPlugin = hasExpoPlugin(tree);
switch (options.e2eTestRunner) {
case 'cypress': {
const hasNxExpoPlugin = hasExpoPlugin(tree);
if (!hasNxExpoPlugin) {
if (!hasPlugin) {
await webStaticServeGenerator(tree, {
buildTarget: `${options.projectName}:export`,
targetName: 'serve-static',
@ -39,7 +41,7 @@ export async function addE2e(
tags: [],
});
return await configurationGenerator(tree, {
const e2eTask = await configurationGenerator(tree, {
...options,
project: options.e2eProjectName,
directory: 'src',
@ -49,12 +51,46 @@ export async function addE2e(
devServerTarget: `${options.projectName}:${options.e2eWebServerTarget}`,
port: options.e2ePort,
baseUrl: options.e2eWebServerAddress,
ciWebServerCommand: hasNxExpoPlugin
ciWebServerCommand: hasPlugin
? `nx run ${options.projectName}:serve-static`
: undefined,
jsx: true,
rootProject: options.rootProject,
});
if (
options.addPlugin ||
readNxJson(tree).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/cypress/plugin'
: p.plugin === '@nx/cypress/plugin'
)
) {
let buildTarget = '^export';
if (hasPlugin) {
const matchingExpoPlugin = await findPluginForConfigFile(
tree,
'@nx/expo/plugin',
joinPathFragments(options.appProjectRoot, 'app.json')
);
if (matchingExpoPlugin && typeof matchingExpoPlugin !== 'string') {
buildTarget = `^${
(matchingExpoPlugin.options as any)?.exportTargetName ?? 'export'
}`;
}
}
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
buildTarget,
joinPathFragments(
options.e2eProjectRoot,
`cypress.config.${options.js ? 'js' : 'ts'}`
)
);
}
return e2eTask;
}
case 'playwright': {
const { configurationGenerator } = ensurePackage<
@ -67,7 +103,8 @@ export async function addE2e(
targets: {},
implicitDependencies: [options.projectName],
});
return configurationGenerator(tree, {
const e2eTask = await configurationGenerator(tree, {
project: options.e2eProjectName,
skipFormat: true,
skipPackageJson: options.skipPackageJson,
@ -80,7 +117,39 @@ export async function addE2e(
} ${options.name}`,
webServerAddress: options.e2eWebServerAddress,
rootProject: options.rootProject,
addPlugin: options.addPlugin,
});
if (
options.addPlugin ||
readNxJson(tree).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/playwright/plugin'
: p.plugin === '@nx/playwright/plugin'
)
) {
let buildTarget = '^export';
if (hasPlugin) {
const matchingExpoPlugin = await findPluginForConfigFile(
tree,
'@nx/expo/plugin',
joinPathFragments(options.appProjectRoot, 'app.json')
);
if (matchingExpoPlugin && typeof matchingExpoPlugin !== 'string') {
buildTarget = `^${
(matchingExpoPlugin.options as any)?.exportTargetName ?? 'export'
}`;
}
}
await addE2eCiTargetDefaults(
tree,
'@nx/playwright/plugin',
buildTarget,
joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`)
);
}
return e2eTask;
}
case 'detox':
const { detoxApplicationGenerator } = ensurePackage<

View File

@ -8,7 +8,7 @@ import {
import { hasExpoPlugin } from '../../../utils/has-expo-plugin';
import { NormalizedSchema } from './normalize-options';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export function addProject(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host);

View File

@ -32,7 +32,7 @@ import { NormalizedSchema, normalizeOptions } from './lib/normalize-options';
import { Schema } from './schema';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import { initRootBabelConfig } from '../../utils/init-root-babel-config';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
export async function expoLibraryGenerator(

View File

@ -25,7 +25,7 @@ import {
type ProjectNameAndRootOptions,
} from '@nx/devkit/src/generators/project-name-and-root-utils';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { findMatchingProjects } from 'nx/src/utils/find-matching-projects';
import { type PackageJson } from 'nx/src/utils/package-json';

View File

@ -12,7 +12,7 @@ import { addSwcConfig } from '../../utils/swc/add-swc-config';
import { addSwcDependencies } from '../../utils/swc/add-swc-dependencies';
import { nxVersion } from '../../utils/versions';
import { SetupBuildGeneratorSchema } from './schema';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export async function setupBuildGenerator(
tree: Tree,

View File

@ -2,6 +2,7 @@ import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
getProjects,
readJson,
readNxJson,
readProjectConfiguration,
Tree,
} from '@nx/devkit';
@ -359,28 +360,9 @@ describe('app', () => {
const indexContent = tree.read(`${name}/src/app/page.tsx`, 'utf-8');
expect(indexContent).not.toContain(`import styles from './page.module`);
expect(indexContent).toContain(`import styled from '@emotion/styled'`);
expect(tree.read(`${name}/src/app/layout.tsx`, 'utf-8'))
.toMatchInlineSnapshot(`
"import './global.css';
export const metadata = {
title: 'Welcome to ${name}',
description: 'Generated by create-nx-workspace',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
"
`);
expect(
tree.read(`${name}/src/app/layout.tsx`, 'utf-8')
).toMatchInlineSnapshot(``);
});
it('should add jsxImportSource in tsconfig.json', async () => {
@ -559,6 +541,50 @@ describe('app', () => {
});
});
describe('--e2e-test-runner cypress', () => {
it('should generate e2e-ci targetDefaults', async () => {
const name = uniq();
await applicationGenerator(tree, {
name,
style: 'css',
e2eTestRunner: 'cypress',
projectNameAndRootFormat: 'as-provided',
addPlugin: true,
});
expect(readNxJson(tree).targetDefaults['e2e-ci--**/*'])
.toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
});
});
describe('--e2e-test-runner playwright', () => {
it('should generate e2e-ci targetDefaults', async () => {
const name = uniq();
await applicationGenerator(tree, {
name,
style: 'css',
e2eTestRunner: 'playwright',
projectNameAndRootFormat: 'as-provided',
addPlugin: true,
});
expect(readNxJson(tree).targetDefaults['e2e-ci--**/*'])
.toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
});
});
it('should generate functional components by default', async () => {
const name = uniq();

View File

@ -11,6 +11,8 @@ import { Linter } from '@nx/eslint';
import { nxVersion } from '../../../utils/versions';
import { NormalizedSchema } from './normalize-options';
import { webStaticServeGenerator } from '@nx/web';
import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file';
import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export async function addE2e(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host);
@ -42,7 +44,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
implicitDependencies: [options.projectName],
});
return configurationGenerator(host, {
const e2eTask = await configurationGenerator(host, {
...options,
linter: Linter.EsLint,
project: options.e2eProjectName,
@ -60,6 +62,40 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
? `nx run ${options.projectName}:serve-static`
: undefined,
});
if (
options.addPlugin ||
readNxJson(host).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/cypress/plugin'
: p.plugin === '@nx/cypress/plugin'
)
) {
let buildTarget = '^build';
if (hasPlugin) {
const matchingPlugin = await findPluginForConfigFile(
host,
'@nx/next/plugin',
joinPathFragments(options.appProjectRoot, 'next.config.js')
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildTargetName ?? 'build'
}`;
}
}
await addE2eCiTargetDefaults(
host,
'@nx/cypress/plugin',
buildTarget,
joinPathFragments(
options.e2eProjectRoot,
`cypress.config.${options.js ? 'js' : 'ts'}`
)
);
}
return e2eTask;
} else if (options.e2eTestRunner === 'playwright') {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/playwright')
@ -71,7 +107,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
tags: [],
implicitDependencies: [options.projectName],
});
return configurationGenerator(host, {
const e2eTask = await configurationGenerator(host, {
rootProject: options.rootProject,
project: options.e2eProjectName,
skipFormat: true,
@ -86,6 +122,37 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
} ${options.projectName}`,
addPlugin: options.addPlugin,
});
if (
options.addPlugin ||
readNxJson(host).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/playwright/plugin'
: p.plugin === '@nx/playwright/plugin'
)
) {
let buildTarget = '^build';
if (hasPlugin) {
const matchingPlugin = await findPluginForConfigFile(
host,
'@nx/next/plugin',
joinPathFragments(options.appProjectRoot, 'next.config.js')
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildTargetName ?? 'build'
}`;
}
}
await addE2eCiTargetDefaults(
host,
'@nx/playwright/plugin',
buildTarget,
joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`)
);
}
return e2eTask;
}
return () => {};
}

View File

@ -5,7 +5,7 @@ import {
readNxJson,
Tree,
} from '@nx/devkit';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export function addProject(host: Tree, options: NormalizedSchema) {
const targets: Record<string, any> = {};

View File

@ -48,7 +48,7 @@ import { initGenerator } from '../init/init';
import { setupDockerGenerator } from '../setup-docker/setup-docker';
import { Schema } from './schema';
import { hasWebpackPlugin } from '../../utils/has-webpack-plugin';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
export interface NormalizedSchema extends Schema {

View File

@ -22,7 +22,7 @@ import { join } from 'path';
import { tslibVersion, typesNodeVersion } from '../../utils/versions';
import { initGenerator } from '../init/init';
import { Schema } from './schema';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export interface NormalizedSchema extends Schema {
fileName: string;

View File

@ -10,6 +10,14 @@ exports[`app generated files content - as-provided - my-app general application
.cache"
`;
exports[`app generated files content - as-provided - my-app general application should add the nuxt and vitest plugins 1`] = `
{
"dependsOn": [
"^build-static",
],
}
`;
exports[`app generated files content - as-provided - my-app general application should configure eslint correctly 1`] = `
"{
"extends": ["@nuxt/eslint-config", "../.eslintrc.json"],
@ -341,6 +349,14 @@ exports[`app generated files content - as-provided - myApp general application s
.cache"
`;
exports[`app generated files content - as-provided - myApp general application should add the nuxt and vitest plugins 1`] = `
{
"dependsOn": [
"^build-static",
],
}
`;
exports[`app generated files content - as-provided - myApp general application should configure eslint correctly 1`] = `
"{
"extends": ["@nuxt/eslint-config", "../.eslintrc.json"],

View File

@ -96,6 +96,7 @@ describe('app', () => {
nxJson.plugins.find((p) => p.plugin === '@nx/vite/plugin')
)
);
expect(nxJson.targetDefaults['e2e-ci--**/*']).toMatchSnapshot();
});
});

View File

@ -7,6 +7,8 @@ import {
} from '@nx/devkit';
import { nxVersion } from '../../../utils/versions';
import { NormalizedSchema } from '../schema';
import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file';
import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export async function addE2e(host: Tree, options: NormalizedSchema) {
if (options.e2eTestRunner === 'cypress') {
@ -21,7 +23,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
tags: [],
implicitDependencies: [options.projectName],
});
return await configurationGenerator(host, {
const e2eTask = await configurationGenerator(host, {
...options,
project: options.e2eProjectName,
directory: 'src',
@ -38,6 +40,33 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
jsx: true,
addPlugin: true,
});
let buildTarget = '^build-static';
const matchingPlugin = await findPluginForConfigFile(
host,
'@nx/nuxt/plugin',
joinPathFragments(
options.appProjectRoot,
`nuxt.config.${options.js ? 'js' : 'ts'}`
)
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildStaticTargetName ?? 'build-static'
}`;
}
await addE2eCiTargetDefaults(
host,
'@nx/cypress/plugin',
buildTarget,
joinPathFragments(
options.e2eProjectRoot,
`cypress.config.${options.js ? 'js' : 'ts'}`
)
);
return e2eTask;
} else if (options.e2eTestRunner === 'playwright') {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/playwright')
@ -48,7 +77,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
targets: {},
implicitDependencies: [options.projectName],
});
return configurationGenerator(host, {
const e2eTask = await configurationGenerator(host, {
project: options.e2eProjectName,
skipFormat: true,
skipPackageJson: options.skipPackageJson,
@ -62,6 +91,30 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
} ${options.projectName}`,
addPlugin: true,
});
let buildTarget = '^build-static';
const matchingPlugin = await findPluginForConfigFile(
host,
'@nx/nuxt/plugin',
joinPathFragments(
options.appProjectRoot,
`nuxt.config.${options.js ? 'js' : 'ts'}`
)
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildStaticTargetName ?? 'build-static'
}`;
}
await addE2eCiTargetDefaults(
host,
'@nx/playwright/plugin',
buildTarget,
joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`)
);
return e2eTask;
}
return () => {};
}

View File

@ -8,11 +8,11 @@ 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,
readProjectConfigurationsFromRootMap,
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';
export { sortObjectByKeys } from './utils/object-sort';

View File

@ -0,0 +1,333 @@
import { ProjectGraph, readNxJson, type Tree, updateNxJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
describe('add-e2e-ci-target-defaults', () => {
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 when the plugin is not registered', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = [];
updateNxJson(tree, nxJson);
// ACT
await addE2eCiTargetDefaults(tree);
// ASSERT
expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"lint": {
"cache": true,
},
}
`);
});
it('should add the targetDefaults with the correct ciTargetName and buildTarget when there is one plugin', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = [
{
plugin: '@nx/playwright/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
},
];
updateNxJson(tree, nxJson);
addProject(tree, tempFs);
// ACT
await addE2eCiTargetDefaults(tree);
// ASSERT
expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"e2e-ci--**/*": {
"dependsOn": [
"^build",
],
},
"lint": {
"cache": true,
},
}
`);
});
it('should add the targetDefaults with the correct ciTargetNames and buildTargets when there is more than one plugin', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = [
{
plugin: '@nx/playwright/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
include: ['app-e2e/**'],
},
{
plugin: '@nx/playwright/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'playwright:e2e-ci',
},
include: ['shop-e2e/**'],
},
];
updateNxJson(tree, nxJson);
addProject(tree, tempFs);
addProject(tree, tempFs, {
buildTargetName: 'build',
ciTargetName: 'playwright:e2e-ci',
appName: 'shop',
});
// ACT
await addE2eCiTargetDefaults(tree);
// ASSERT
expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"e2e-ci--**/*": {
"dependsOn": [
"^build",
],
},
"lint": {
"cache": true,
},
"playwright:e2e-ci--**/*": {
"dependsOn": [
"^build",
],
},
}
`);
});
it('should only add the targetDefaults with the correct ciTargetName and buildTargets when there is more than one plugin with only one matching multiple projects', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = [
{
plugin: '@nx/playwright/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
include: ['cart-e2e/**'],
},
{
plugin: '@nx/playwright/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'playwright:e2e-ci',
},
},
];
updateNxJson(tree, nxJson);
addProject(tree, tempFs);
addProject(tree, tempFs, {
buildTargetName: 'bundle',
ciTargetName: 'playwright:e2e-ci',
appName: 'shop',
});
// ACT
await addE2eCiTargetDefaults(tree);
// ASSERT
expect(readNxJson(tree).targetDefaults).toMatchInlineSnapshot(`
{
"build": {
"cache": true,
},
"lint": {
"cache": true,
},
"playwright:e2e-ci--**/*": {
"dependsOn": [
"^build",
"^bundle",
],
},
}
`);
});
});
function addProject(
tree: Tree,
tempFs: TempFs,
overrides: {
ciTargetName: string;
buildTargetName: string;
appName: string;
noCi?: boolean;
} = { ciTargetName: 'e2e-ci', buildTargetName: 'build', appName: 'app' }
) {
const appProjectConfig = {
name: overrides.appName,
root: overrides.appName,
sourceRoot: `${overrides.appName}/src`,
projectType: 'application',
};
const viteConfig = `/// <reference types='vitest' />
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 playwrightConfig = `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 ${overrides.appName}:serve-static',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});`;
tree.write(`${overrides.appName}/vite.config.ts`, viteConfig);
tree.write(
`${overrides.appName}/project.json`,
JSON.stringify(appProjectConfig)
);
tree.write(`${overrides.appName}-e2e/playwright.config.ts`, playwrightConfig);
tree.write(
`${overrides.appName}-e2e/project.json`,
JSON.stringify(e2eProjectConfig)
);
tempFs.createFilesSync({
[`${overrides.appName}/vite.config.ts`]: viteConfig,
[`${overrides.appName}/project.json`]: JSON.stringify(appProjectConfig),
[`${overrides.appName}-e2e/playwright.config.ts`]: playwrightConfig,
[`${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': {
dependsOn: [overrides.buildTargetName],
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]: {},
},
},
};
}

View File

@ -0,0 +1,116 @@
import {
type Tree,
type CreateNodesV2,
formatFiles,
readNxJson,
createProjectGraphAsync,
parseTargetString,
} from '@nx/devkit';
import { addE2eCiTargetDefaults as _addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/internal-api';
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
import {
ProjectConfigurationsError,
retrieveProjectConfigurations,
} from 'nx/src/devkit-internals';
import { tsquery } from '@phenomnomnominal/tsquery';
import { type PlaywrightPluginOptions } from '../../plugins/plugin';
export default async function addE2eCiTargetDefaults(tree: Tree) {
const pluginName = '@nx/playwright/plugin';
const graph = await createProjectGraphAsync();
const nxJson = readNxJson(tree);
const matchingPluginRegistrations = nxJson.plugins?.filter((p) =>
typeof p === 'string' ? p === pluginName : p.plugin === pluginName
);
if (!matchingPluginRegistrations) {
return;
}
const {
createNodesV2,
}: { createNodesV2: CreateNodesV2<PlaywrightPluginOptions> } = 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');
const ast = tsquery.ast(configFileContents);
const CI_WEBSERVER_COMMAND_SELECTOR =
'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=command]) > StringLiteral';
const nodes = tsquery(ast, CI_WEBSERVER_COMMAND_SELECTOR, {
visitAllChildren: true,
});
if (!nodes.length) {
continue;
}
const ciWebServerCommand = nodes[0].getText();
let serveStaticProject: string;
let serveStaticTarget: string;
let serveStaticConfiguration: string;
if (ciWebServerCommand.includes('nx run')) {
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
);
serveStaticProject = project;
serveStaticTarget = target;
serveStaticConfiguration = configuration;
} else {
const NX_PROJECT_REGEX = 'nx\\s+([^ ]+)\\s+([^ ]+)';
const matches = ciWebServerCommand.match(NX_PROJECT_REGEX);
if (!matches) {
return;
}
serveStaticTarget = matches[1];
serveStaticProject = matches[2];
}
const resolvedServeStaticTarget =
graph.nodes[serveStaticProject].data.targets[serveStaticTarget];
let resolvedBuildTarget: string;
if (resolvedServeStaticTarget.dependsOn) {
resolvedBuildTarget = resolvedServeStaticTarget.dependsOn.join(',');
} else {
resolvedBuildTarget =
(serveStaticConfiguration
? resolvedServeStaticTarget.configurations[serveStaticConfiguration]
.buildTarget
: resolvedServeStaticTarget.options.buildTarget) ?? 'build';
}
const buildTarget = `^${resolvedBuildTarget}`;
await _addE2eCiTargetDefaults(tree, pluginName, buildTarget, configFile);
}
}
}

View File

@ -8,6 +8,7 @@ import {
visitNotIgnoredFiles,
} from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults';
export default async function (tree: Tree) {
const graph = await createProjectGraphAsync();
@ -138,5 +139,6 @@ export default async function (tree: Tree) {
}
});
await addE2eCiTargetDefaults(tree);
await formatFiles(tree);
}

View File

@ -1086,4 +1086,99 @@ describe('app', () => {
}
`);
});
it('should add e2e-ci targetDefaults to nxJson when addPlugin=true with playwright', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
let nxJson = readNxJson(tree);
delete nxJson.targetDefaults;
updateNxJson(tree, nxJson);
// ACT
await applicationGenerator(tree, {
name: 'myapp',
addPlugin: true,
linter: Linter.None,
style: 'none',
e2eTestRunner: 'playwright',
});
// ASSERT
nxJson = readNxJson(tree);
expect(nxJson.targetDefaults).toMatchInlineSnapshot(`
{
"e2e-ci--**/*": {
"dependsOn": [
"^build",
],
},
}
`);
});
it('should add e2e-ci targetDefaults to nxJson when addPlugin=true with cypress', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
let nxJson = readNxJson(tree);
delete nxJson.targetDefaults;
updateNxJson(tree, nxJson);
// ACT
await applicationGenerator(tree, {
name: 'myapp',
addPlugin: true,
linter: Linter.None,
style: 'none',
e2eTestRunner: 'cypress',
});
// ASSERT
nxJson = readNxJson(tree);
expect(nxJson.targetDefaults).toMatchInlineSnapshot(`
{
"e2e-ci--**/*": {
"dependsOn": [
"^build",
],
},
}
`);
});
it('should add e2e-ci targetDefaults to nxJson when addPlugin=true with cypress and use the defined webpack buildTargetName', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
let nxJson = readNxJson(tree);
delete nxJson.targetDefaults;
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/webpack/plugin',
options: {
buildTargetName: 'build-base',
},
});
updateNxJson(tree, nxJson);
// ACT
await applicationGenerator(tree, {
name: 'myapp',
addPlugin: true,
linter: Linter.None,
style: 'none',
bundler: 'webpack',
e2eTestRunner: 'cypress',
});
// ASSERT
nxJson = readNxJson(tree);
expect(nxJson.targetDefaults).toMatchInlineSnapshot(`
{
"e2e-ci--**/*": {
"dependsOn": [
"^build-base",
],
},
}
`);
});
});

View File

@ -4,6 +4,7 @@ import {
ensurePackage,
getPackageManagerCommand,
joinPathFragments,
readNxJson,
} from '@nx/devkit';
import { webStaticServeGenerator } from '@nx/web';
@ -11,6 +12,8 @@ import { nxVersion } from '../../../utils/versions';
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
import { hasVitePlugin } from '../../../utils/has-vite-plugin';
import { NormalizedSchema } from '../schema';
import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file';
import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export async function addE2e(
tree: Tree,
@ -41,7 +44,7 @@ export async function addE2e(
tags: [],
});
return await configurationGenerator(tree, {
const e2eTask = await configurationGenerator(tree, {
...options,
project: options.e2eProjectName,
directory: 'src',
@ -64,6 +67,46 @@ export async function addE2e(
ciBaseUrl:
options.bundler === 'vite' ? options.e2eCiBaseUrl : undefined,
});
if (
options.addPlugin ||
readNxJson(tree).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/cypress/plugin'
: p.plugin === '@nx/cypress/plugin'
)
) {
let buildTarget = '^build';
if (hasNxBuildPlugin) {
const configFile =
options.bundler === 'webpack'
? 'webpack.config.js'
: options.bundler === 'vite'
? `vite.config.${options.js ? 'js' : 'ts'}`
: 'webpack.config.js';
const matchingPlugin = await findPluginForConfigFile(
tree,
`@nx/${options.bundler}/plugin`,
joinPathFragments(options.appProjectRoot, configFile)
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildTargetName ?? 'build'
}`;
}
}
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
buildTarget,
joinPathFragments(
options.e2eProjectRoot,
`cypress.config.${options.js ? 'js' : 'ts'}`
)
);
}
return e2eTask;
}
case 'playwright': {
const { configurationGenerator } = ensurePackage<
@ -76,7 +119,7 @@ export async function addE2e(
targets: {},
implicitDependencies: [options.projectName],
});
return configurationGenerator(tree, {
const e2eTask = await configurationGenerator(tree, {
project: options.e2eProjectName,
skipFormat: true,
skipPackageJson: options.skipPackageJson,
@ -91,6 +134,43 @@ export async function addE2e(
rootProject: options.rootProject,
addPlugin: options.addPlugin,
});
if (
options.addPlugin ||
readNxJson(tree).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/playwright/plugin'
: p.plugin === '@nx/playwright/plugin'
)
) {
let buildTarget = '^build';
if (hasNxBuildPlugin) {
const configFile =
options.bundler === 'webpack'
? 'webpack.config.js'
: options.bundler === 'vite'
? `vite.config.${options.js ? 'js' : 'ts'}`
: 'webpack.config.js';
const matchingPlugin = await findPluginForConfigFile(
tree,
`@nx/${options.bundler}/plugin`,
joinPathFragments(options.appProjectRoot, configFile)
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildTargetName ?? 'build'
}`;
}
}
await addE2eCiTargetDefaults(
tree,
'@nx/playwright/plugin',
buildTarget,
joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`)
);
}
return e2eTask;
}
case 'none':
default:

View File

@ -1,6 +1,6 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { joinPathFragments, readJson, type Tree } from '@nx/devkit';
import { joinPathFragments, readJson, readNxJson, type Tree } from '@nx/devkit';
import * as devkit from '@nx/devkit';
import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
@ -129,6 +129,14 @@ describe('Remix Application', () => {
expectTargetsToBeCorrect(tree, '.');
expect(tree.read('e2e/cypress.config.ts', 'utf-8')).toMatchSnapshot();
expect(readNxJson(tree).targetDefaults['e2e-ci--**/*'])
.toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
});
});
@ -148,6 +156,14 @@ describe('Remix Application', () => {
expectTargetsToBeCorrect(tree, '.');
expect(tree.read('e2e/playwright.config.ts', 'utf-8')).toMatchSnapshot();
expect(readNxJson(tree).targetDefaults['e2e-ci--**/*'])
.toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
});
});

View File

@ -33,7 +33,7 @@ import { NxRemixGeneratorSchema } from './schema';
import { updateDependencies } from '../utils/update-dependencies';
import initGenerator from '../init/init';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { updateJestTestMatch } from '../../utils/testing-config-utils';

View File

@ -6,11 +6,19 @@ import {
updateProjectConfiguration,
ensurePackage,
getPackageManagerCommand,
readNxJson,
} from '@nx/devkit';
import { type NormalizedSchema } from './normalize-options';
import { getPackageVersion } from '../../../utils/versions';
import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file';
import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export async function addE2E(tree: Tree, options: NormalizedSchema) {
const hasRemixPlugin = readNxJson(tree).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/remix/plugin'
: p.plugin === '@nx/remix/plugin'
);
if (options.e2eTestRunner === 'cypress') {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/cypress')
@ -25,7 +33,7 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) {
implicitDependencies: [options.projectName],
});
return await configurationGenerator(tree, {
const e2eTask = await configurationGenerator(tree, {
project: options.e2eProjectName,
directory: 'src',
skipFormat: true,
@ -33,6 +41,40 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) {
baseUrl: options.e2eWebServerAddress,
addPlugin: options.addPlugin,
});
if (
options.addPlugin ||
readNxJson(tree).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/cypress/plugin'
: p.plugin === '@nx/cypress/plugin'
)
) {
let buildTarget = '^build';
if (hasRemixPlugin) {
const matchingPlugin = await findPluginForConfigFile(
tree,
`@nx/remix/plugin`,
joinPathFragments(options.projectRoot, 'remix.config.js')
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildTargetName ?? 'build'
}`;
}
}
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
buildTarget,
joinPathFragments(
options.e2eProjectRoot,
`cypress.config.${options.js ? 'js' : 'ts'}`
)
);
}
return e2eTask;
} else if (options.e2eTestRunner === 'playwright') {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/playwright')
@ -47,7 +89,7 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) {
implicitDependencies: [options.projectName],
});
return configurationGenerator(tree, {
const e2eTask = await configurationGenerator(tree, {
project: options.e2eProjectName,
skipFormat: true,
skipPackageJson: false,
@ -62,6 +104,37 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) {
rootProject: options.rootProject,
addPlugin: options.addPlugin,
});
if (
options.addPlugin ||
readNxJson(tree).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/playwright/plugin'
: p.plugin === '@nx/playwright/plugin'
)
) {
let buildTarget = '^build';
if (hasRemixPlugin) {
const matchingPlugin = await findPluginForConfigFile(
tree,
`@nx/remix/plugin`,
joinPathFragments(options.projectRoot, 'remix.config.js')
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildTargetName ?? 'build'
}`;
}
}
await addE2eCiTargetDefaults(
tree,
'@nx/playwright/plugin',
buildTarget,
joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`)
);
}
return e2eTask;
} else {
return () => {};
}

View File

@ -16,7 +16,7 @@ import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { rollupInitGenerator } from '../init/init';
import { RollupExecutorOptions } from '../../executors/rollup/schema';
import { RollupProjectSchema } from './schema';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import { hasPlugin } from '../../utils/has-plugin';
import { RollupWithNxPluginOptions } from '../../plugins/with-nx/with-nx-options';

View File

@ -14,7 +14,7 @@ import { VitePreviewServerExecutorOptions } from '../executors/preview-server/sc
import { VitestExecutorOptions } from '../executors/test/schema';
import { ViteConfigurationGeneratorSchema } from '../generators/configuration/schema';
import { ensureViteConfigIsCorrect } from './vite-config-edit-utils';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export type Target = 'build' | 'serve' | 'test' | 'preview';
export type TargetFlags = Partial<Record<Target, boolean>>;

View File

@ -40,6 +40,7 @@ describe('application generator', () => {
...options,
unitTestRunner: 'vitest',
e2eTestRunner: 'playwright',
addPlugin: true,
});
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/vite.config.ts', 'utf-8')).toMatchSnapshot();
@ -49,6 +50,14 @@ describe('application generator', () => {
tree.read('test-e2e/playwright.config.ts', 'utf-8')
).toMatchSnapshot();
expect(listFiles(tree)).toMatchSnapshot();
expect(readNxJson(tree).targetDefaults['e2e-ci--**/*'])
.toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
});
it('should set up project correctly for cypress', async () => {

View File

@ -10,6 +10,8 @@ import { webStaticServeGenerator } from '@nx/web';
import { nxVersion } from '../../../utils/versions';
import { NormalizedSchema } from '../schema';
import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file';
import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
export async function addE2e(
tree: Tree,
@ -52,7 +54,7 @@ export async function addE2e(
tags: [],
implicitDependencies: [options.projectName],
});
return await configurationGenerator(tree, {
const e2eTask = await configurationGenerator(tree, {
...options,
project: options.e2eProjectName,
directory: 'src',
@ -70,6 +72,43 @@ export async function addE2e(
ciWebServerCommand: `nx run ${options.projectName}:${e2eCiWebServerTarget}`,
ciBaseUrl: 'http://localhost:4300',
});
if (
options.addPlugin ||
readNxJson(tree).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/cypress/plugin'
: p.plugin === '@nx/cypress/plugin'
)
) {
let buildTarget = '^build';
if (hasPlugin) {
const matchingPlugin = await findPluginForConfigFile(
tree,
`@nx/vite/plugin`,
joinPathFragments(
options.appProjectRoot,
`vite.config.${options.js ? 'js' : 'ts'}`
)
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildTargetName ?? 'build'
}`;
}
}
await addE2eCiTargetDefaults(
tree,
'@nx/cypress/plugin',
buildTarget,
joinPathFragments(
options.e2eProjectRoot,
`cypress.config.${options.js ? 'js' : 'ts'}`
)
);
}
return e2eTask;
}
case 'playwright': {
const { configurationGenerator } = ensurePackage<
@ -82,7 +121,7 @@ export async function addE2e(
targets: {},
implicitDependencies: [options.projectName],
});
return configurationGenerator(tree, {
const e2eTask = await configurationGenerator(tree, {
...options,
project: options.e2eProjectName,
skipFormat: true,
@ -96,6 +135,40 @@ export async function addE2e(
}:${e2eCiWebServerTarget}`,
webServerAddress: 'http://localhost:4300',
});
if (
options.addPlugin ||
readNxJson(tree).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/playwright/plugin'
: p.plugin === '@nx/playwright/plugin'
)
) {
let buildTarget = '^build';
if (hasPlugin) {
const matchingPlugin = await findPluginForConfigFile(
tree,
`@nx/vite/plugin`,
joinPathFragments(
options.appProjectRoot,
`vite.config.${options.js ? 'js' : 'ts'}`
)
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildTargetName ?? 'build'
}`;
}
}
await addE2eCiTargetDefaults(
tree,
'@nx/playwright/plugin',
buildTarget,
joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`)
);
}
return e2eTask;
}
case 'none':
default:

View File

@ -43,6 +43,14 @@ describe('app', () => {
expect(readProjectConfiguration(tree, 'my-app-e2e').root).toEqual(
'my-app-e2e'
);
expect(readNxJson(tree).targetDefaults['e2e-ci--**/*'])
.toMatchInlineSnapshot(`
{
"dependsOn": [
"^build",
],
}
`);
}, 60_000);
it('should update tags and implicit dependencies', async () => {

View File

@ -9,6 +9,7 @@ import {
joinPathFragments,
names,
offsetFromRoot,
type PluginConfiguration,
readNxJson,
readProjectConfiguration,
runTasksInSerial,
@ -37,12 +38,15 @@ import { webInitGenerator } from '../init/init';
import { Schema } from './schema';
import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope';
import { hasWebpackPlugin } from '../../utils/has-webpack-plugin';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import {
addBuildTargetDefaults,
addE2eCiTargetDefaults,
} from '@nx/devkit/src/generators/target-defaults-utils';
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';
import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file';
interface NormalizedSchema extends Schema {
projectName: string;
@ -368,10 +372,20 @@ 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) {
const nxJson = readNxJson(host);
let hasPlugin: PluginConfiguration | undefined;
let buildPlugin: string;
let buildConfigFile: string;
if (options.bundler === 'webpack' || options.bundler === 'vite') {
buildPlugin = `@nx/${options.bundler}/plugin`;
buildConfigFile =
options.bundler === 'webpack' ? 'webpack.config.js' : `vite.config.ts`;
hasPlugin = nxJson.plugins?.find((p) =>
typeof p === 'string' ? p === buildPlugin : p.plugin === buildPlugin
);
}
if (!hasPlugin) {
await staticServeConfiguration(host, {
buildTarget: `${options.projectName}:build`,
spa: true,
@ -396,17 +410,47 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
baseUrl: options.e2eWebServerAddress,
directory: 'src',
skipFormat: true,
webServerCommands: hasNxBuildPlugin
webServerCommands: hasPlugin
? {
default: `nx run ${options.projectName}:${options.e2eWebServerTarget}`,
production: `nx run ${options.projectName}:preview`,
}
: undefined,
ciWebServerCommand: hasNxBuildPlugin
ciWebServerCommand: hasPlugin
? `nx run ${options.projectName}:${options.e2eCiWebServerTarget}`
: undefined,
ciBaseUrl: options.bundler === 'vite' ? options.e2eCiBaseUrl : undefined,
});
if (
options.addPlugin ||
readNxJson(host).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/cypress/plugin'
: p.plugin === '@nx/cypress/plugin'
)
) {
let buildTarget = '^build';
if (hasPlugin) {
const matchingPlugin = await findPluginForConfigFile(
host,
buildPlugin,
joinPathFragments(options.appProjectRoot, buildConfigFile)
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildTargetName ?? 'build'
}`;
}
}
await addE2eCiTargetDefaults(
host,
'@nx/cypress/plugin',
buildTarget,
joinPathFragments(options.e2eProjectRoot, `cypress.config.ts`)
);
}
tasks.push(cypressTask);
} else if (options.e2eTestRunner === 'playwright') {
const { configurationGenerator: playwrightConfigGenerator } = ensurePackage<
@ -434,6 +478,36 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
webServerAddress: options.e2eCiBaseUrl,
addPlugin: options.addPlugin,
});
if (
options.addPlugin ||
readNxJson(host).plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/playwright/plugin'
: p.plugin === '@nx/playwright/plugin'
)
) {
let buildTarget = '^build';
if (hasPlugin) {
const matchingPlugin = await findPluginForConfigFile(
host,
buildPlugin,
joinPathFragments(options.appProjectRoot, buildConfigFile)
);
if (matchingPlugin && typeof matchingPlugin !== 'string') {
buildTarget = `^${
(matchingPlugin.options as any)?.buildTargetName ?? 'build'
}`;
}
}
await addE2eCiTargetDefaults(
host,
'@nx/playwright/plugin',
buildTarget,
joinPathFragments(options.e2eProjectRoot, `playwright.config.ts`)
);
}
tasks.push(playwrightTask);
}
if (options.unitTestRunner === 'jest') {

View File

@ -15,7 +15,7 @@ import { webpackInitGenerator } from '../init/init';
import { ConfigurationGeneratorSchema } from './schema';
import { WebpackExecutorOptions } from '../../executors/webpack/schema';
import { hasPlugin } from '../../utils/has-plugin';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { ensureDependencies } from '../../utils/ensure-dependencies';
export function configurationGenerator(

View File

@ -115,6 +115,7 @@
"@nx/nx-dev/ui-theme": ["nx-dev/ui-theme/src/index.ts"],
"@nx/nx-dev/util-ai": ["nx-dev/util-ai/src/index.ts"],
"@nx/playwright": ["packages/playwright/index.ts"],
"@nx/playwright/*": ["packages/playwright/*"],
"@nx/plugin": ["packages/plugin"],
"@nx/plugin/*": ["packages/plugin/*"],
"@nx/react": ["packages/react"],