@nx/cypress 17.1.2 → 17.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/migrations.json CHANGED
@@ -47,6 +47,16 @@
47
47
  "version": "16.8.0-beta.4",
48
48
  "description": "Update to Cypress v13. Most noteable change is video recording is off by default. This migration will only update if the workspace is already on Cypress v12. https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-130",
49
49
  "implementation": "./src/migrations/update-16-8-0/cypress-13"
50
+ },
51
+ "add-nx-metadata": {
52
+ "version": "17.2.0-beta.0",
53
+ "description": "Add devServerTargets into cypress.config.ts files for @nx/cypress/plugin",
54
+ "implementation": "./src/migrations/update-17-2-0/add-dev-server-targets-to-cypress-configs"
55
+ },
56
+ "add-nx-cypress-plugin": {
57
+ "version": "17.2.0-beta.0",
58
+ "description": "Add the @nx/cypress/plugin to nx.json plugins",
59
+ "implementation": "./src/migrations/update-17-2-0/add-nx-cypress-plugin"
50
60
  }
51
61
  },
52
62
  "packageJsonUpdates": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nx/cypress",
3
- "version": "17.1.2",
3
+ "version": "17.2.0-beta.1",
4
4
  "private": false,
5
5
  "description": "The Nx Plugin for Cypress contains executors and generators allowing your workspace to use the powerful Cypress integration testing capabilities.",
6
6
  "repository": {
@@ -34,14 +34,14 @@
34
34
  "migrations": "./migrations.json"
35
35
  },
36
36
  "dependencies": {
37
+ "@nx/devkit": "17.2.0-beta.1",
38
+ "@nx/eslint": "17.2.0-beta.1",
39
+ "@nx/js": "17.2.0-beta.1",
37
40
  "@phenomnomnominal/tsquery": "~5.0.1",
38
41
  "detect-port": "^1.5.1",
39
42
  "semver": "7.5.3",
40
43
  "tslib": "^2.3.0",
41
- "@nx/devkit": "17.1.2",
42
- "@nx/js": "17.1.2",
43
- "@nx/eslint": "17.1.2",
44
- "@nrwl/cypress": "17.1.2"
44
+ "@nrwl/cypress": "17.2.0-beta.1"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "cypress": ">= 3 < 14"
package/plugin.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { createNodes, createDependencies } from './src/plugins/plugin';
package/plugin.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDependencies = exports.createNodes = void 0;
4
+ var plugin_1 = require("./src/plugins/plugin");
5
+ Object.defineProperty(exports, "createNodes", { enumerable: true, get: function () { return plugin_1.createNodes; } });
6
+ Object.defineProperty(exports, "createDependencies", { enumerable: true, get: function () { return plugin_1.createDependencies; } });
@@ -30,26 +30,8 @@ export declare function nxBaseCypressPreset(pathToConfig: string, options?: {
30
30
  * }
31
31
  * })
32
32
  *
33
- * @param pathToConfig will be used to construct the output paths for videos and screenshots
34
33
  */
35
- export declare function nxE2EPreset(pathToConfig: string, options?: NxCypressE2EPresetOptions): {
36
- fileServerFolder: string;
37
- supportFile: string;
38
- specPattern: string;
39
- fixturesFolder: string;
40
- videosFolder: string;
41
- screenshotsFolder: string;
42
- chromeWebSecurity: boolean;
43
- } | {
44
- setupNodeEvents(on: any): void;
45
- fileServerFolder: string;
46
- supportFile: string;
47
- specPattern: string;
48
- fixturesFolder: string;
49
- videosFolder: string;
50
- screenshotsFolder: string;
51
- chromeWebSecurity: boolean;
52
- };
34
+ export declare function nxE2EPreset(pathToConfig: string, options?: NxCypressE2EPresetOptions): any;
53
35
  export type NxCypressE2EPresetOptions = {
54
36
  bundler?: string;
55
37
  /**
@@ -58,5 +40,7 @@ export type NxCypressE2EPresetOptions = {
58
40
  * default is 'src'
59
41
  **/
60
42
  cypressDir?: string;
43
+ devServerTargets?: Record<string, string>;
44
+ ciDevServerTarget?: string;
61
45
  };
62
46
  export {};
@@ -5,6 +5,8 @@ const devkit_1 = require("@nx/devkit");
5
5
  const path_1 = require("path");
6
6
  const fs_1 = require("fs");
7
7
  const preprocessor_vite_1 = require("../src/plugins/preprocessor-vite");
8
+ const ct_helpers_1 = require("../src/utils/ct-helpers");
9
+ const start_dev_server_1 = require("../src/utils/start-dev-server");
8
10
  function nxBaseCypressPreset(pathToConfig, options) {
9
11
  // used to set babel settings for react CT.
10
12
  process.env.NX_CYPRESS_COMPONENT_TEST =
@@ -39,25 +41,50 @@ exports.nxBaseCypressPreset = nxBaseCypressPreset;
39
41
  * }
40
42
  * })
41
43
  *
42
- * @param pathToConfig will be used to construct the output paths for videos and screenshots
43
44
  */
44
45
  function nxE2EPreset(pathToConfig, options) {
45
46
  const basePath = options?.cypressDir || 'src';
46
- const baseConfig = {
47
+ const baseConfig /** Cypress.EndToEndConfigOptions */ = {
47
48
  ...nxBaseCypressPreset(pathToConfig),
48
49
  fileServerFolder: '.',
49
50
  supportFile: `${basePath}/support/e2e.ts`,
50
51
  specPattern: `${basePath}/**/*.cy.{js,jsx,ts,tsx}`,
51
52
  fixturesFolder: `${basePath}/fixtures`,
52
- };
53
- if (options?.bundler === 'vite') {
54
- return {
55
- ...baseConfig,
56
- setupNodeEvents(on) {
53
+ env: {
54
+ devServerTargets: options?.devServerTargets,
55
+ devServerTargetOptions: {},
56
+ ciDevServerTarget: options?.ciDevServerTarget,
57
+ },
58
+ async setupNodeEvents(on, config) {
59
+ if (options?.bundler === 'vite') {
57
60
  on('file:preprocessor', (0, preprocessor_vite_1.default)());
58
- },
59
- };
60
- }
61
+ }
62
+ if (!config.env.devServerTargets) {
63
+ return;
64
+ }
65
+ const devServerTarget = config.env.devServerTarget ?? config.env.devServerTargets['default'];
66
+ if (!devServerTarget) {
67
+ return;
68
+ }
69
+ if (!config.baseUrl && devServerTarget) {
70
+ const graph = await (0, devkit_1.createProjectGraphAsync)();
71
+ const target = (0, devkit_1.parseTargetString)(devServerTarget, graph);
72
+ const context = (0, ct_helpers_1.createExecutorContext)(graph, graph.nodes[target.project].data?.targets, target.project, target.target, target.configuration);
73
+ const devServer = (0, start_dev_server_1.startDevServer)({
74
+ devServerTarget,
75
+ ...config.env.devServerTargetOptions,
76
+ }, context);
77
+ on('after:run', () => {
78
+ devServer.return();
79
+ });
80
+ const devServerValue = (await devServer.next()).value;
81
+ if (!devServerValue) {
82
+ return;
83
+ }
84
+ return { ...config, baseUrl: devServerValue.baseUrl };
85
+ }
86
+ },
87
+ };
61
88
  return baseConfig;
62
89
  }
63
90
  exports.nxE2EPreset = nxE2EPreset;
@@ -1,19 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const devkit_1 = require("@nx/devkit");
4
- const executor_utils_1 = require("nx/src/command-line/run/executor-utils");
5
4
  const fs_1 = require("fs");
6
5
  const path_1 = require("path");
7
6
  const ct_helpers_1 = require("../../utils/ct-helpers");
8
7
  const cypress_version_1 = require("../../utils/cypress-version");
9
- const detectPort = require("detect-port");
8
+ const start_dev_server_1 = require("../../utils/start-dev-server");
10
9
  const Cypress = require('cypress'); // @NOTE: Importing via ES6 messes the whole test dependencies.
11
10
  async function cypressExecutor(options, context) {
12
11
  options = normalizeOptions(options, context);
13
12
  // this is used by cypress component testing presets to build the executor contexts with the correct configuration options.
14
13
  process.env.NX_CYPRESS_TARGET_CONFIGURATION = context.configurationName;
15
14
  let success;
16
- for await (const devServerValues of startDevServer(options, context)) {
15
+ for await (const devServerValues of (0, start_dev_server_1.startDevServer)(options, context)) {
17
16
  try {
18
17
  success = await runCypress(devServerValues.baseUrl, {
19
18
  ...options,
@@ -90,50 +89,6 @@ A generator to migrate from v8 to v10 is provided. See https://nx.dev/cypress/v1
90
89
  `);
91
90
  }
92
91
  }
93
- async function* startDevServer(opts, context) {
94
- // no dev server, return the provisioned base url
95
- if (!opts.devServerTarget || opts.skipServe) {
96
- yield { baseUrl: opts.baseUrl };
97
- return;
98
- }
99
- const parsedDevServerTarget = (0, devkit_1.parseTargetString)(opts.devServerTarget, context);
100
- const [targetSupportsWatchOpt] = getValueFromSchema(context, parsedDevServerTarget, 'watch');
101
- const overrides = {
102
- // @NOTE: Do not forward watch option if not supported by the target dev server,
103
- // this is relevant for running Cypress against dev server target that does not support this option,
104
- // for instance @nguniversal/builders:ssr-dev-server.
105
- ...(targetSupportsWatchOpt ? { watch: opts.watch } : {}),
106
- };
107
- if (opts.port === 'cypress-auto') {
108
- const freePort = await getPortForProject(context, parsedDevServerTarget);
109
- overrides['port'] = freePort;
110
- }
111
- else if (opts.port !== undefined) {
112
- overrides['port'] = opts.port;
113
- // zero is a special case that means any valid port so there is no reason to try to 'lock it'
114
- if (opts.port !== 0) {
115
- const didLock = attemptToLockPort(opts.port);
116
- if (!didLock) {
117
- devkit_1.logger.warn((0, devkit_1.stripIndents) `${opts.port} is potentially already in use by another cypress run.
118
- If the port is in use, try using a different port value or passing --port='cypress-auto' to find a free port.`);
119
- }
120
- }
121
- }
122
- for await (const output of await (0, devkit_1.runExecutor)(parsedDevServerTarget, overrides, context)) {
123
- if (!output.success && !opts.watch)
124
- throw new Error('Could not compile application files');
125
- if (!opts.baseUrl &&
126
- !output.baseUrl &&
127
- !output.info?.baseUrl &&
128
- (output.port || output.info?.port)) {
129
- output.baseUrl = `http://localhost:${output.port ?? output.info?.port}`;
130
- }
131
- yield {
132
- baseUrl: opts.baseUrl || output.baseUrl || output.info?.baseUrl,
133
- portLockFilePath: overrides.port && (0, path_1.join)(__dirname, `${overrides.port}.txt`),
134
- };
135
- }
136
- }
137
92
  /**
138
93
  * @whatItDoes Initialize the Cypress test runner with the provided project configuration.
139
94
  * By default, Cypress will run tests from the CLI without the GUI and provide directly the results in the console output.
@@ -154,8 +109,14 @@ async function runCypress(baseUrl, opts) {
154
109
  if (opts.browser) {
155
110
  options.browser = opts.browser;
156
111
  }
112
+ options.env = {
113
+ devServerTarget: opts.devServerTarget,
114
+ };
157
115
  if (opts.env) {
158
- options.env = opts.env;
116
+ options.env = {
117
+ ...options.env,
118
+ ...opts.env,
119
+ };
159
120
  }
160
121
  if (opts.spec) {
161
122
  options.spec = opts.spec;
@@ -211,110 +172,6 @@ async function runCypress(baseUrl, opts) {
211
172
  */
212
173
  return !result.totalFailed && !result.failures;
213
174
  }
214
- /**
215
- * try to find a free port for the project to run on
216
- * will return undefined if no port is found or the project doesn't have a port option
217
- **/
218
- async function getPortForProject(context, target, defaultPort = 4200) {
219
- const fmtTarget = (0, devkit_1.targetToTargetString)(target);
220
- const [hasPortOpt, schemaPortValue] = getValueFromSchema(context, target, 'port');
221
- let freePort;
222
- if (hasPortOpt) {
223
- let normalizedPortValue;
224
- if (!schemaPortValue) {
225
- devkit_1.logger.info(`NX ${fmtTarget} did not have a defined port value, checking for free port with the default value of ${defaultPort}`);
226
- normalizedPortValue = defaultPort;
227
- }
228
- else {
229
- normalizedPortValue = Number(schemaPortValue);
230
- }
231
- if (isNaN(normalizedPortValue)) {
232
- devkit_1.output.warn({
233
- title: `Port Not a Number`,
234
- bodyLines: [
235
- `The port value found was not a number or can't be parsed to a number`,
236
- `When reading the devServerTarget (${fmtTarget}) schema, expected ${schemaPortValue} to be a number but got NaN.`,
237
- `Nx will use the default value of ${defaultPort} instead.`,
238
- `You can manually specify a port by setting the 'port' option`,
239
- ],
240
- });
241
- normalizedPortValue = defaultPort;
242
- }
243
- try {
244
- let attempts = 0;
245
- // make sure when this check happens in parallel,
246
- // we don't let the same port be used by multiple projects
247
- do {
248
- freePort = await detectPort(freePort || normalizedPortValue);
249
- if (attemptToLockPort(freePort)) {
250
- break;
251
- }
252
- attempts++;
253
- // increment port in case the lock file isn't cleaned up
254
- freePort++;
255
- } while (attempts < 20);
256
- devkit_1.logger.info(`NX Using port ${freePort} for ${fmtTarget}`);
257
- }
258
- catch (err) {
259
- throw new Error((0, devkit_1.stripIndents) `Unable to find a free port for the dev server, ${fmtTarget}.
260
- You can disable auto port detection by specifing a port or not passing a value to --port`);
261
- }
262
- }
263
- else {
264
- devkit_1.output.warn({
265
- title: `No Port Option Found`,
266
- bodyLines: [
267
- `The 'port' option is set to 'cypress-auto', but the devServerTarget (${fmtTarget}) does not have a port option.`,
268
- `Because of this, Nx is unable to verify the port is free before starting the dev server.`,
269
- `This might cause issues if the devServerTarget is trying to use a port that is already in use.`,
270
- ],
271
- });
272
- }
273
- return freePort;
274
- }
275
- /**
276
- * Check if the given target has the given property in it's options.
277
- * if the property is does not have a default value or is not in the actual executor options,
278
- * the value will be undefined even if it's in the executor schema.
279
- **/
280
- function getValueFromSchema(context, target, property) {
281
- let targetOpts;
282
- try {
283
- targetOpts = (0, devkit_1.readTargetOptions)(target, context);
284
- }
285
- catch (e) {
286
- throw new Error(`Unable to read the target options for ${(0, devkit_1.targetToTargetString)(target)}.
287
- Are you sure this is a valid target?
288
- Was trying to read the target for the property: '${property}', but got the following error:
289
- ${e.message || e}`);
290
- }
291
- let targetHasOpt = Object.keys(targetOpts).includes(property);
292
- if (!targetHasOpt) {
293
- // NOTE: readTargetOptions doesn't apply non defaulted values, i.e. @nx/vite has a port options but is optional
294
- // so we double check the schema if readTargetOptions didn't return a value for the property
295
- const projectConfig = context.projectsConfigurations?.projects?.[target.project];
296
- const targetConfig = projectConfig.targets[target.target];
297
- const [collection, executor] = targetConfig.executor.split(':');
298
- const { schema } = (0, executor_utils_1.getExecutorInformation)(collection, executor, context.root);
299
- // NOTE: schema won't have a default since readTargetOptions would have
300
- // already set that and this check wouldn't need to be made
301
- targetHasOpt = Object.keys(schema.properties).includes(property);
302
- }
303
- return [targetHasOpt, targetOpts[property]];
304
- }
305
- function attemptToLockPort(port) {
306
- const portLockFilePath = (0, path_1.join)(__dirname, `${port}.txt`);
307
- try {
308
- if ((0, fs_1.existsSync)(portLockFilePath)) {
309
- return false;
310
- }
311
- (0, fs_1.writeFileSync)(portLockFilePath, 'locked');
312
- return true;
313
- }
314
- catch (err) {
315
- return false;
316
- }
317
- }
318
175
  function cleanupTmpFile(path) {
319
176
  try {
320
177
  if (path && (0, fs_1.existsSync)(path)) {
@@ -7,11 +7,17 @@ const versions_1 = require("../../utils/versions");
7
7
  const base_setup_1 = require("../base-setup/base-setup");
8
8
  async function componentConfigurationGenerator(tree, options) {
9
9
  const opts = normalizeOptions(options);
10
+ const nxJson = (0, devkit_1.readNxJson)(tree);
11
+ const hasPlugin = nxJson.plugins?.some((p) => typeof p === 'string'
12
+ ? p === '@nx/cypress/plugin'
13
+ : p.plugin === '@nx/cypress/plugin');
10
14
  const projectConfig = (0, devkit_1.readProjectConfiguration)(tree, opts.project);
11
15
  const installDepsTask = updateDeps(tree, opts);
12
16
  addProjectFiles(tree, projectConfig, opts);
13
- addTargetToProject(tree, projectConfig, opts);
14
- updateNxJsonConfiguration(tree);
17
+ if (!hasPlugin) {
18
+ addTargetToProject(tree, projectConfig, opts);
19
+ }
20
+ updateNxJsonConfiguration(tree, hasPlugin);
15
21
  updateTsConfigForComponentTesting(tree, projectConfig);
16
22
  if (!opts.skipFormat) {
17
23
  await (0, devkit_1.formatFiles)(tree);
@@ -67,25 +73,26 @@ function addTargetToProject(tree, projectConfig, opts) {
67
73
  };
68
74
  (0, devkit_1.updateProjectConfiguration)(tree, opts.project, projectConfig);
69
75
  }
70
- function updateNxJsonConfiguration(tree) {
76
+ function updateNxJsonConfiguration(tree, hasPlugin) {
71
77
  const nxJson = (0, devkit_1.readNxJson)(tree);
72
- const cacheableOperations = nxJson.tasksRunnerOptions?.default?.options?.cacheableOperations;
73
- if (cacheableOperations && !cacheableOperations.includes('component-test')) {
74
- cacheableOperations.push('component-test');
78
+ const productionFileSet = nxJson.namedInputs?.production;
79
+ if (productionFileSet) {
80
+ nxJson.namedInputs.production = Array.from(new Set([
81
+ ...productionFileSet,
82
+ '!{projectRoot}/cypress/**/*',
83
+ '!{projectRoot}/**/*.cy.[jt]s?(x)',
84
+ '!{projectRoot}/cypress.config.[jt]s',
85
+ ]));
75
86
  }
76
- nxJson.targetDefaults ??= {};
77
- nxJson.targetDefaults['component-test'] ??= {};
78
- nxJson.targetDefaults['component-test'].cache ??= true;
79
- if (nxJson.namedInputs) {
80
- const productionFileSet = nxJson.namedInputs?.production;
81
- if (productionFileSet) {
82
- nxJson.namedInputs.production = Array.from(new Set([
83
- ...productionFileSet,
84
- '!{projectRoot}/cypress/**/*',
85
- '!{projectRoot}/**/*.cy.[jt]s?(x)',
86
- '!{projectRoot}/cypress.config.[jt]s',
87
- ]));
87
+ if (!hasPlugin) {
88
+ const cacheableOperations = nxJson.tasksRunnerOptions?.default?.options?.cacheableOperations;
89
+ if (cacheableOperations &&
90
+ !cacheableOperations.includes('component-test')) {
91
+ cacheableOperations.push('component-test');
88
92
  }
93
+ nxJson.targetDefaults ??= {};
94
+ nxJson.targetDefaults['component-test'] ??= {};
95
+ nxJson.targetDefaults['component-test'].cache ??= true;
89
96
  nxJson.targetDefaults['component-test'] ??= {};
90
97
  nxJson.targetDefaults['component-test'].inputs ??= [
91
98
  'default',
@@ -17,11 +17,18 @@ async function configurationGenerator(tree, options) {
17
17
  if (!(0, cypress_version_1.installedCypressVersion)()) {
18
18
  tasks.push(await (0, init_1.default)(tree, opts));
19
19
  }
20
+ const projectGraph = await (0, devkit_1.createProjectGraphAsync)();
21
+ const nxJson = (0, devkit_1.readNxJson)(tree);
22
+ const hasPlugin = nxJson.plugins?.some((p) => typeof p === 'string'
23
+ ? p === '@nx/cypress/plugin'
24
+ : p.plugin === '@nx/cypress/plugin');
20
25
  if (opts.bundler === 'vite') {
21
26
  tasks.push((0, devkit_1.addDependenciesToPackageJson)(tree, {}, { vite: versions_1.viteVersion }));
22
27
  }
23
- await addFiles(tree, opts);
24
- addTarget(tree, opts);
28
+ await addFiles(tree, opts, projectGraph, hasPlugin);
29
+ if (!hasPlugin) {
30
+ addTarget(tree, opts);
31
+ }
25
32
  const linterTask = await (0, add_linter_1.addLinterToCyProject)(tree, {
26
33
  ...opts,
27
34
  cypressDir: opts.directory,
@@ -46,16 +53,20 @@ Rename or remove the existing e2e target.`);
46
53
  In this case you need to provide a devServerTarget,'<projectName>:<targetName>[:<configName>]', or a baseUrl option`);
47
54
  }
48
55
  options.directory ??= 'src';
56
+ const devServerTarget = options.devServerTarget ??
57
+ (projectConfig.targets.serve ? `${options.project}:serve` : undefined);
58
+ if (!options.baseUrl && !devServerTarget) {
59
+ throw new Error('Either baseUrl or devServerTarget must be provided');
60
+ }
49
61
  return {
50
62
  ...options,
51
63
  bundler: options.bundler ?? 'webpack',
52
64
  rootProject: options.rootProject ?? projectConfig.root === '.',
53
65
  linter: options.linter ?? eslint_1.Linter.EsLint,
54
- devServerTarget: options.devServerTarget ??
55
- (projectConfig.targets.serve ? `${options.project}:serve` : undefined),
66
+ devServerTarget,
56
67
  };
57
68
  }
58
- async function addFiles(tree, options) {
69
+ async function addFiles(tree, options, projectGraph, hasPlugin) {
59
70
  const projectConfig = (0, devkit_1.readProjectConfiguration)(tree, options.project);
60
71
  const cyVersion = (0, cypress_version_1.installedCypressVersion)();
61
72
  const filesToUse = cyVersion && cyVersion < 10 ? 'v9' : 'v10';
@@ -84,10 +95,29 @@ async function addFiles(tree, options) {
84
95
  jsx: options.jsx,
85
96
  });
86
97
  const cyFile = (0, devkit_1.joinPathFragments)(projectConfig.root, 'cypress.config.ts');
98
+ let devServerTargets;
99
+ let ciDevServerTarget;
100
+ if (hasPlugin && !options.baseUrl && options.devServerTarget) {
101
+ devServerTargets = {};
102
+ devServerTargets.default = options.devServerTarget;
103
+ const parsedTarget = (0, devkit_1.parseTargetString)(options.devServerTarget, projectGraph);
104
+ const devServerProjectConfig = (0, devkit_1.readProjectConfiguration)(tree, parsedTarget.project);
105
+ // Add production e2e target if serve target is found
106
+ if (parsedTarget.configuration !== 'production' &&
107
+ devServerProjectConfig.targets[parsedTarget.target]?.configurations?.['production']) {
108
+ devServerTargets.production = `${parsedTarget.project}:${parsedTarget.target}:production`;
109
+ }
110
+ // Add ci/static e2e target if serve target is found
111
+ if (devServerProjectConfig.targets?.['serve-static']) {
112
+ ciDevServerTarget = `${parsedTarget.project}:serve-static`;
113
+ }
114
+ }
87
115
  const updatedCyConfig = await (0, config_1.addDefaultE2EConfig)(tree.read(cyFile, 'utf-8'), {
88
- directory: options.directory,
89
- bundler: options.bundler,
90
- });
116
+ cypressDir: options.directory,
117
+ bundler: options.bundler === 'vite' ? 'vite' : undefined,
118
+ devServerTargets,
119
+ ciDevServerTarget: ciDevServerTarget,
120
+ }, options.baseUrl);
91
121
  tree.write(cyFile, updatedCyConfig);
92
122
  }
93
123
  if (cyVersion &&
@@ -148,9 +178,6 @@ function addTarget(tree, opts) {
148
178
  };
149
179
  }
150
180
  }
151
- else {
152
- throw new Error('Either baseUrl or devServerTarget must be provided');
153
- }
154
181
  (0, devkit_1.updateProjectConfiguration)(tree, opts.project, projectConfig);
155
182
  }
156
183
  exports.default = configurationGenerator;
@@ -13,6 +13,7 @@ function setupE2ETargetDefaults(tree) {
13
13
  nxJson.targetDefaults ??= {};
14
14
  const productionFileSet = !!nxJson.namedInputs?.production;
15
15
  nxJson.targetDefaults.e2e ??= {};
16
+ nxJson.targetDefaults.e2e.cache ??= true;
16
17
  nxJson.targetDefaults.e2e.inputs ??= [
17
18
  'default',
18
19
  productionFileSet ? '^production' : '^default',
@@ -27,13 +28,52 @@ function updateDependencies(tree) {
27
28
  '@types/node': versions_1.typesNodeVersion,
28
29
  });
29
30
  }
31
+ function addPlugin(tree) {
32
+ const nxJson = (0, devkit_1.readNxJson)(tree);
33
+ nxJson.plugins ??= [];
34
+ for (const plugin of nxJson.plugins) {
35
+ if (typeof plugin === 'string'
36
+ ? plugin === '@nx/cypress/plugin'
37
+ : plugin.plugin === '@nx/cypress/plugin') {
38
+ return;
39
+ }
40
+ }
41
+ nxJson.plugins.push({
42
+ plugin: '@nx/cypress/plugin',
43
+ options: {
44
+ targetName: 'e2e',
45
+ componentTestingTargetName: 'component-test',
46
+ },
47
+ });
48
+ (0, devkit_1.updateNxJson)(tree, nxJson);
49
+ }
50
+ function updateProductionFileset(tree) {
51
+ const nxJson = (0, devkit_1.readNxJson)(tree);
52
+ const productionFileset = nxJson.namedInputs?.production;
53
+ if (productionFileset) {
54
+ nxJson.namedInputs.production = Array.from(new Set([
55
+ ...productionFileset,
56
+ '!{projectRoot}/cypress/**/*',
57
+ '!{projectRoot}/**/*.cy.[jt]s?(x)',
58
+ '!{projectRoot}/cypress.config.[jt]s',
59
+ ]));
60
+ }
61
+ (0, devkit_1.updateNxJson)(tree, nxJson);
62
+ }
30
63
  async function cypressInitGenerator(tree, options) {
31
- setupE2ETargetDefaults(tree);
64
+ const addPlugins = process.env.NX_PCV3 === 'true';
65
+ updateProductionFileset(tree);
66
+ if (!addPlugins) {
67
+ setupE2ETargetDefaults(tree);
68
+ }
32
69
  const tasks = [];
33
70
  tasks.push(await (0, js_1.initGenerator)(tree, {
34
71
  ...options,
35
72
  skipFormat: true,
36
73
  }));
74
+ if (addPlugins) {
75
+ addPlugin(tree);
76
+ }
37
77
  if (!options.skipPackageJson) {
38
78
  tasks.push(updateDependencies(tree));
39
79
  }
@@ -2,4 +2,6 @@ export interface Schema {
2
2
  skipPackageJson?: boolean;
3
3
 
4
4
  skipFormat?: boolean;
5
+
6
+ addPlugins?: boolean;
5
7
  }
@@ -0,0 +1,2 @@
1
+ import { Tree } from '@nx/devkit';
2
+ export default function update(tree: Tree): Promise<void>;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const devkit_1 = require("@nx/devkit");
4
+ const executor_options_utils_1 = require("@nx/devkit/src/generators/executor-options-utils");
5
+ const typescript_1 = require("typescript");
6
+ function addDevServerTargets(tree, cypressConfig, devServerTargets) {
7
+ const contents = tree.read(cypressConfig, 'utf-8');
8
+ const sourceFile = (0, typescript_1.createSourceFile)(cypressConfig, contents, typescript_1.ScriptTarget.ESNext);
9
+ const ciDevServerTarget = devServerTargets.ci;
10
+ delete devServerTargets.ci;
11
+ const changes = [];
12
+ const visit = (node) => {
13
+ if ((0, typescript_1.isCallExpression)(node) &&
14
+ (0, typescript_1.isIdentifier)(node.expression) &&
15
+ node.expression.text === 'nxE2EPreset') {
16
+ const argumentContents = ', ' +
17
+ JSON.stringify({
18
+ devServerTargets,
19
+ ciDevServerTarget,
20
+ });
21
+ if (node.arguments.length === 1) {
22
+ changes.push({
23
+ type: devkit_1.ChangeType.Insert,
24
+ index: node.arguments[0].getEnd(),
25
+ text: argumentContents,
26
+ });
27
+ }
28
+ else {
29
+ const lastArgument = node.arguments[node.arguments.length - 1];
30
+ if ((0, typescript_1.isObjectLiteralExpression)(lastArgument)) {
31
+ const lastProperty = lastArgument.properties[lastArgument.properties.length - 1];
32
+ const trailingComma = lastArgument.properties.hasTrailingComma;
33
+ changes.push({
34
+ type: devkit_1.ChangeType.Insert,
35
+ index: lastProperty.getEnd(),
36
+ text: (trailingComma ? '' : ', ') +
37
+ 'devServerTargets: ' +
38
+ JSON.stringify(devServerTargets) +
39
+ (ciDevServerTarget
40
+ ? `, ciDevServerTarget: '${ciDevServerTarget}'`
41
+ : ''),
42
+ });
43
+ }
44
+ }
45
+ }
46
+ else {
47
+ (0, typescript_1.forEachChild)(node, visit);
48
+ }
49
+ };
50
+ (0, typescript_1.forEachChild)(sourceFile, visit);
51
+ tree.write(cypressConfig, (0, devkit_1.applyChangesToString)(contents, changes));
52
+ }
53
+ async function update(tree) {
54
+ const projects = (0, devkit_1.getProjects)(tree);
55
+ const graph = await (0, devkit_1.createProjectGraphAsync)();
56
+ const devServerTargetsMap = new Map();
57
+ (0, executor_options_utils_1.forEachExecutorOptionsInGraph)(graph, '@nx/cypress:cypress', (options, project, target, configuration) => {
58
+ const targetConfig = projects.get(project).targets?.[target];
59
+ if (!targetConfig) {
60
+ return;
61
+ }
62
+ const cypressConfig = options.cypressConfig ?? targetConfig.options?.cypressConfig;
63
+ if (!cypressConfig) {
64
+ return;
65
+ }
66
+ const devServerTargets = {};
67
+ devServerTargets.default = targetConfig.options?.devServerTarget;
68
+ for (const [configuration, configurationOptions] of Object.entries(targetConfig.configurations ?? {})) {
69
+ devServerTargets[configuration] = configurationOptions.devServerTarget;
70
+ }
71
+ devServerTargetsMap.set(cypressConfig, devServerTargets);
72
+ });
73
+ for (const [cypressConfig, devServerTargets] of devServerTargetsMap) {
74
+ addDevServerTargets(tree, cypressConfig, devServerTargets);
75
+ }
76
+ // const configFiles = glob(tree, [createNodes[0]]);
77
+ //
78
+ // const proj = Object.fromEntries(getProjects(tree).entries());
79
+ //
80
+ // const rootMappings = createProjectRootMappingsFromProjectConfigurations(proj);
81
+ //
82
+ // for (const configFile of configFiles) {
83
+ // const siblings = tree.children(dirname(configFile));
84
+ // if (!siblings.includes('project.json')) {
85
+ // continue;
86
+ // }
87
+ //
88
+ // const projectName = findProjectForPath(configFile, rootMappings);
89
+ // const projectConfig = readProjectConfiguration(tree, projectName);
90
+ // const e2eTarget: TargetConfiguration<CypressExecutorOptions> =
91
+ // projectConfig.targets?.e2e;
92
+ //
93
+ // if (!e2eTarget || e2eTarget.executor !== '@nx/cypress:cypress') {
94
+ // continue;
95
+ // }
96
+ //
97
+ // nxMetadata.devServerTarget = e2eTarget.options?.devServerTarget;
98
+ // nxMetadata.productionDevServerTarget =
99
+ // e2eTarget.configurations?.production?.devServerTarget;
100
+ // nxMetadata.ciDevServerTarget =
101
+ // e2eTarget.configurations?.ci?.devServerTarget;
102
+ //
103
+ // if (Object.values(nxMetadata).filter((v) => !!v).length > 0) {
104
+ // let contents = tree.read(configFile, 'utf-8');
105
+ //
106
+ // contents =
107
+ // `import type { NxCypressMetadata } from '@nx/cypress/plugin';\n` +
108
+ // contents +
109
+ // `
110
+ //
111
+ // /**
112
+ // * This is metadata for the @nx/cypress/plugin
113
+ // */
114
+ // export const nx: NxCypressMetadata = ${JSON.stringify(
115
+ // nxMetadata,
116
+ // null,
117
+ // 2
118
+ // )};`;
119
+ //
120
+ // tree.write(configFile, contents);
121
+ // }
122
+ // }
123
+ await (0, devkit_1.formatFiles)(tree);
124
+ }
125
+ exports.default = update;
@@ -0,0 +1,2 @@
1
+ import { Tree } from '@nx/devkit';
2
+ export default function update(tree: Tree): Promise<void>;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const devkit_1 = require("@nx/devkit");
4
+ const plugin_1 = require("../../plugins/plugin");
5
+ const find_project_for_path_1 = require("nx/src/project-graph/utils/find-project-for-path");
6
+ const replace_project_configuration_with_plugin_1 = require("@nx/devkit/src/utils/replace-project-configuration-with-plugin");
7
+ async function update(tree) {
8
+ const proj = Object.fromEntries((0, devkit_1.getProjects)(tree).entries());
9
+ const rootMappings = (0, find_project_for_path_1.createProjectRootMappingsFromProjectConfigurations)(proj);
10
+ await (0, replace_project_configuration_with_plugin_1.replaceProjectConfigurationsWithPlugin)(tree, rootMappings, '@nx/cypress/plugin', plugin_1.createNodes, {
11
+ targetName: 'e2e',
12
+ componentTestingTargetName: 'component-test',
13
+ });
14
+ await (0, devkit_1.formatFiles)(tree);
15
+ }
16
+ exports.default = update;
@@ -0,0 +1,8 @@
1
+ import { CreateDependencies, CreateNodes } from '@nx/devkit';
2
+ export interface CypressPluginOptions {
3
+ ciTargetName?: string;
4
+ targetName?: string;
5
+ componentTestingTargetName?: string;
6
+ }
7
+ export declare const createDependencies: CreateDependencies;
8
+ export declare const createNodes: CreateNodes<CypressPluginOptions>;
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createNodes = exports.createDependencies = void 0;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const path_1 = require("path");
6
+ const internal_1 = require("@nx/js/src/internal");
7
+ const js_1 = require("@nx/js");
8
+ const project_configuration_utils_1 = require("nx/src/project-graph/utils/project-configuration-utils");
9
+ const get_named_inputs_1 = require("@nx/devkit/src/utils/get-named-inputs");
10
+ const fs_1 = require("fs");
11
+ const workspace_context_1 = require("nx/src/utils/workspace-context");
12
+ const calculate_hash_for_create_nodes_1 = require("@nx/devkit/src/utils/calculate-hash-for-create-nodes");
13
+ const cache_directory_1 = require("nx/src/utils/cache-directory");
14
+ const cachePath = (0, path_1.join)(cache_directory_1.projectGraphCacheDirectory, 'cypress.hash');
15
+ const targetsCache = (0, fs_1.existsSync)(cachePath) ? readTargetsCache() : {};
16
+ const calculatedTargets = {};
17
+ function readTargetsCache() {
18
+ return (0, devkit_1.readJsonFile)(cachePath);
19
+ }
20
+ function writeTargetsToCache(targets) {
21
+ (0, devkit_1.writeJsonFile)(cachePath, targets);
22
+ }
23
+ const createDependencies = () => {
24
+ writeTargetsToCache(calculatedTargets);
25
+ return [];
26
+ };
27
+ exports.createDependencies = createDependencies;
28
+ exports.createNodes = [
29
+ '**/cypress.config.{js,ts,mjs,mts,cjs,cts}',
30
+ (configFilePath, options, context) => {
31
+ options = normalizeOptions(options);
32
+ const projectRoot = (0, path_1.dirname)(configFilePath);
33
+ // Do not create a project if package.json and project.json isn't there.
34
+ const siblingFiles = (0, fs_1.readdirSync)((0, path_1.join)(context.workspaceRoot, projectRoot));
35
+ if (!siblingFiles.includes('package.json') &&
36
+ !siblingFiles.includes('project.json')) {
37
+ return {};
38
+ }
39
+ const hash = (0, calculate_hash_for_create_nodes_1.calculateHashForCreateNodes)(projectRoot, options, context);
40
+ const targets = targetsCache[hash]
41
+ ? targetsCache[hash]
42
+ : buildCypressTargets(configFilePath, projectRoot, options, context);
43
+ calculatedTargets[hash] = targets;
44
+ return {
45
+ projects: {
46
+ [projectRoot]: {
47
+ projectType: 'application',
48
+ targets,
49
+ },
50
+ },
51
+ };
52
+ },
53
+ ];
54
+ function getOutputs(projectRoot, cypressConfig, testingType) {
55
+ function getOutput(path) {
56
+ if (path.startsWith('..')) {
57
+ return (0, path_1.join)('{workspaceRoot}', (0, path_1.join)(projectRoot, path));
58
+ }
59
+ else {
60
+ return (0, path_1.join)('{projectRoot}', path);
61
+ }
62
+ }
63
+ const { screenshotsFolder, videosFolder, e2e, component } = cypressConfig;
64
+ const outputs = ['{options.videosFolder}', '{options.screenshotsFolder}'];
65
+ if (videosFolder) {
66
+ outputs.push(getOutput(videosFolder));
67
+ }
68
+ if (screenshotsFolder) {
69
+ outputs.push(getOutput(screenshotsFolder));
70
+ }
71
+ switch (testingType) {
72
+ case 'e2e': {
73
+ if (e2e.videosFolder) {
74
+ outputs.push(getOutput(e2e.videosFolder));
75
+ }
76
+ if (e2e.screenshotsFolder) {
77
+ outputs.push(getOutput(e2e.screenshotsFolder));
78
+ }
79
+ break;
80
+ }
81
+ case 'component': {
82
+ if (component.videosFolder) {
83
+ outputs.push(getOutput(component.videosFolder));
84
+ }
85
+ if (component.screenshotsFolder) {
86
+ outputs.push(getOutput(component.screenshotsFolder));
87
+ }
88
+ break;
89
+ }
90
+ }
91
+ return outputs;
92
+ }
93
+ function buildCypressTargets(configFilePath, projectRoot, options, context) {
94
+ const cypressConfig = getCypressConfig(configFilePath, context);
95
+ const namedInputs = (0, get_named_inputs_1.getNamedInputs)(projectRoot, context);
96
+ const baseTargetConfig = {
97
+ executor: '@nx/cypress:cypress',
98
+ options: {
99
+ cypressConfig: configFilePath,
100
+ },
101
+ };
102
+ const targets = {};
103
+ if ('e2e' in cypressConfig) {
104
+ const e2eTargetDefaults = (0, project_configuration_utils_1.readTargetDefaultsForTarget)(options.targetName, context.nxJsonConfiguration.targetDefaults, '@nx/cypress:cypress');
105
+ targets[options.targetName] = {
106
+ ...baseTargetConfig,
107
+ options: {
108
+ ...baseTargetConfig.options,
109
+ testingType: 'e2e',
110
+ },
111
+ };
112
+ if (e2eTargetDefaults?.cache === undefined) {
113
+ targets[options.targetName].cache = true;
114
+ }
115
+ if (e2eTargetDefaults?.inputs === undefined) {
116
+ targets[options.targetName].inputs =
117
+ 'production' in namedInputs
118
+ ? ['default', '^production']
119
+ : ['default', '^default'];
120
+ }
121
+ if (e2eTargetDefaults?.outputs === undefined) {
122
+ targets[options.targetName].outputs = getOutputs(projectRoot, cypressConfig, 'e2e');
123
+ }
124
+ const cypressEnv = {
125
+ ...cypressConfig.env,
126
+ ...cypressConfig.e2e?.env,
127
+ };
128
+ const devServerTargets = cypressEnv?.devServerTargets;
129
+ if (devServerTargets?.default) {
130
+ targets[options.targetName].options.devServerTarget =
131
+ devServerTargets.default;
132
+ delete devServerTargets.default;
133
+ }
134
+ if (Object.keys(devServerTargets ?? {}).length > 0) {
135
+ targets[options.targetName].configurations ??= {};
136
+ for (const [configuration, devServerTarget] of Object.entries(devServerTargets ?? {})) {
137
+ targets[options.targetName].configurations[configuration] = {
138
+ devServerTarget,
139
+ };
140
+ }
141
+ }
142
+ const ciDevServerTarget = cypressEnv?.ciDevServerTarget;
143
+ if (ciDevServerTarget) {
144
+ const specPatterns = Array.isArray(cypressConfig.e2e.specPattern)
145
+ ? cypressConfig.e2e.specPattern.map((p) => (0, path_1.join)(projectRoot, p))
146
+ : [(0, path_1.join)(projectRoot, cypressConfig.e2e.specPattern)];
147
+ const excludeSpecPatterns = !cypressConfig.e2e
148
+ .excludeSpecPattern
149
+ ? cypressConfig.e2e.excludeSpecPattern
150
+ : Array.isArray(cypressConfig.e2e.excludeSpecPattern)
151
+ ? cypressConfig.e2e.excludeSpecPattern.map((p) => (0, path_1.join)(projectRoot, p))
152
+ : [(0, path_1.join)(projectRoot, cypressConfig.e2e.excludeSpecPattern)];
153
+ const specFiles = (0, workspace_context_1.globWithWorkspaceContext)(context.workspaceRoot, specPatterns, excludeSpecPatterns);
154
+ const dependsOn = [];
155
+ const outputs = getOutputs(projectRoot, cypressConfig, 'e2e');
156
+ const inputs = 'production' in namedInputs
157
+ ? ['default', '^production']
158
+ : ['default', '^default'];
159
+ for (const file of specFiles) {
160
+ const targetName = options.ciTargetName + '--' + file;
161
+ targets[targetName] = {
162
+ ...targets[options.targetName],
163
+ outputs,
164
+ inputs,
165
+ cache: true,
166
+ configurations: undefined,
167
+ options: {
168
+ ...targets[options.targetName].options,
169
+ devServerTarget: ciDevServerTarget,
170
+ spec: file,
171
+ },
172
+ };
173
+ dependsOn.push({
174
+ target: targetName,
175
+ projects: 'self',
176
+ params: 'forward',
177
+ });
178
+ }
179
+ targets[options.ciTargetName] ??= {};
180
+ targets[options.ciTargetName] = {
181
+ executor: 'nx:noop',
182
+ cache: true,
183
+ inputs,
184
+ outputs,
185
+ dependsOn,
186
+ };
187
+ }
188
+ }
189
+ if ('component' in cypressConfig) {
190
+ const componentTestingTargetDefaults = (0, project_configuration_utils_1.readTargetDefaultsForTarget)(options.componentTestingTargetName, context.nxJsonConfiguration.targetDefaults, '@nx/cypress:cypress');
191
+ // This will not override the e2e target if it is the same
192
+ targets[options.componentTestingTargetName] ??= {
193
+ ...baseTargetConfig,
194
+ options: {
195
+ ...baseTargetConfig.options,
196
+ testingType: 'component',
197
+ },
198
+ };
199
+ if (componentTestingTargetDefaults?.cache === undefined) {
200
+ targets[options.componentTestingTargetName].cache = true;
201
+ }
202
+ if (componentTestingTargetDefaults?.inputs === undefined) {
203
+ targets[options.componentTestingTargetName].inputs =
204
+ 'production' in namedInputs
205
+ ? ['default', '^production']
206
+ : ['default', '^default'];
207
+ }
208
+ if (componentTestingTargetDefaults?.outputs === undefined) {
209
+ targets[options.componentTestingTargetName].outputs = getOutputs(projectRoot, cypressConfig, 'component');
210
+ }
211
+ }
212
+ return targets;
213
+ }
214
+ function getCypressConfig(configFilePath, context) {
215
+ const resolvedPath = (0, path_1.join)(context.workspaceRoot, configFilePath);
216
+ let module;
217
+ if (['.ts', '.mts', '.cts'].includes((0, path_1.extname)(configFilePath))) {
218
+ const tsConfigPath = (0, js_1.getRootTsConfigPath)();
219
+ if (tsConfigPath) {
220
+ const unregisterTsProject = (0, internal_1.registerTsProject)(tsConfigPath);
221
+ try {
222
+ module = require(resolvedPath);
223
+ }
224
+ finally {
225
+ unregisterTsProject();
226
+ }
227
+ }
228
+ else {
229
+ module = require(resolvedPath);
230
+ }
231
+ }
232
+ else {
233
+ module = require(resolvedPath);
234
+ }
235
+ return module.default ?? module;
236
+ }
237
+ function normalizeOptions(options) {
238
+ options ??= {};
239
+ options.targetName ??= 'e2e';
240
+ options.componentTestingTargetName ??= 'component-test';
241
+ options.ciTargetName ??= 'e2e-ci';
242
+ return options;
243
+ }
@@ -1,7 +1,5 @@
1
- export declare function addDefaultE2EConfig(cyConfigContents: string, options: {
2
- directory: string;
3
- bundler?: string;
4
- }): Promise<string>;
1
+ import { NxCypressE2EPresetOptions } from '../../plugins/cypress-preset';
2
+ export declare function addDefaultE2EConfig(cyConfigContents: string, options: NxCypressE2EPresetOptions, baseUrl: string): Promise<string>;
5
3
  /**
6
4
  * Adds the nxComponentTestingPreset to the cypress config file
7
5
  * Make sure after calling this the correct import statement is addeda
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.addMountDefinition = exports.addDefaultCTConfig = exports.addDefaultE2EConfig = void 0;
4
4
  const TS_QUERY_EXPORT_CONFIG_PREFIX = ':matches(ExportAssignment, BinaryExpression:has(Identifier[name="module"]):has(Identifier[name="exports"]))';
5
- async function addDefaultE2EConfig(cyConfigContents, options) {
5
+ async function addDefaultE2EConfig(cyConfigContents, options, baseUrl) {
6
6
  if (!cyConfigContents) {
7
7
  throw new Error('The passed in cypress config file is empty!');
8
8
  }
@@ -10,21 +10,22 @@ async function addDefaultE2EConfig(cyConfigContents, options) {
10
10
  const testingTypeConfig = tsquery.query(cyConfigContents, `${TS_QUERY_EXPORT_CONFIG_PREFIX} PropertyAssignment:has(Identifier[name="e2e"])`);
11
11
  let updatedConfigContents = cyConfigContents;
12
12
  if (testingTypeConfig.length === 0) {
13
- const configValue = options.bundler === 'vite'
14
- ? `nxE2EPreset(__filename, { cypressDir: '${options.directory}', bundler: 'vite' })`
15
- : `nxE2EPreset(__filename, { cypressDir: '${options.directory}' })`;
13
+ const configValue = `nxE2EPreset(__filename, ${JSON.stringify(options)})`;
16
14
  updatedConfigContents = tsquery.replace(cyConfigContents, `${TS_QUERY_EXPORT_CONFIG_PREFIX} ObjectLiteralExpression:first-child`, (node) => {
15
+ let baseUrlContents = baseUrl ? `,\nbaseUrl: '${baseUrl}'` : '';
17
16
  if (node.properties.length > 0) {
18
17
  return `{
19
18
  ${node.properties.map((p) => p.getText()).join(',\n')},
20
- e2e: ${configValue}
19
+ e2e: { ...${configValue}${baseUrlContents} }
21
20
  }`;
22
21
  }
23
22
  return `{
24
- e2e: ${configValue}
23
+ e2e: { ...${configValue}${baseUrlContents} }
25
24
  }`;
26
25
  });
27
- updatedConfigContents = `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';\n${updatedConfigContents}`;
26
+ return `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
27
+
28
+ ${updatedConfigContents}`;
28
29
  }
29
30
  return updatedConfigContents;
30
31
  }
@@ -0,0 +1,9 @@
1
+ import { ExecutorContext } from '@nx/devkit';
2
+ import { CypressExecutorOptions } from '../executors/cypress/cypress.impl';
3
+ export declare function startDevServer(opts: Omit<CypressExecutorOptions, 'cypressConfig'>, context: ExecutorContext): AsyncGenerator<{
4
+ baseUrl: any;
5
+ portLockFilePath?: undefined;
6
+ } | {
7
+ baseUrl: any;
8
+ portLockFilePath: string;
9
+ }, void, unknown>;
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startDevServer = void 0;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const path_1 = require("path");
6
+ const detectPort = require("detect-port");
7
+ const executor_utils_1 = require("nx/src/command-line/run/executor-utils");
8
+ const fs_1 = require("fs");
9
+ async function* startDevServer(opts, context) {
10
+ // no dev server, return the provisioned base url
11
+ if (!opts.devServerTarget || opts.skipServe) {
12
+ yield { baseUrl: opts.baseUrl };
13
+ return;
14
+ }
15
+ const parsedDevServerTarget = (0, devkit_1.parseTargetString)(opts.devServerTarget, context);
16
+ const [targetSupportsWatchOpt] = getValueFromSchema(context, parsedDevServerTarget, 'watch');
17
+ const overrides = {
18
+ // @NOTE: Do not forward watch option if not supported by the target dev server,
19
+ // this is relevant for running Cypress against dev server target that does not support this option,
20
+ // for instance @nguniversal/builders:ssr-dev-server.
21
+ ...(targetSupportsWatchOpt ? { watch: opts.watch } : {}),
22
+ };
23
+ if (opts.port === 'cypress-auto') {
24
+ const freePort = await getPortForProject(context, parsedDevServerTarget);
25
+ overrides['port'] = freePort;
26
+ }
27
+ else if (opts.port !== undefined) {
28
+ overrides['port'] = opts.port;
29
+ // zero is a special case that means any valid port so there is no reason to try to 'lock it'
30
+ if (opts.port !== 0) {
31
+ const didLock = attemptToLockPort(opts.port);
32
+ if (!didLock) {
33
+ devkit_1.logger.warn((0, devkit_1.stripIndents) `${opts.port} is potentially already in use by another cypress run.
34
+ If the port is in use, try using a different port value or passing --port='cypress-auto' to find a free port.`);
35
+ }
36
+ }
37
+ }
38
+ for await (const output of await (0, devkit_1.runExecutor)(parsedDevServerTarget, overrides, context)) {
39
+ if (!output.success && !opts.watch)
40
+ throw new Error('Could not compile application files');
41
+ if (!opts.baseUrl &&
42
+ !output.baseUrl &&
43
+ !output.info?.baseUrl &&
44
+ (output.port || output.info?.port)) {
45
+ output.baseUrl = `http://localhost:${output.info.port}`;
46
+ }
47
+ yield {
48
+ baseUrl: opts.baseUrl || output.baseUrl || output.info?.baseUrl,
49
+ portLockFilePath: overrides.port && (0, path_1.join)(__dirname, `${overrides.port}.txt`),
50
+ };
51
+ }
52
+ }
53
+ exports.startDevServer = startDevServer;
54
+ /**
55
+ * try to find a free port for the project to run on
56
+ * will return undefined if no port is found or the project doesn't have a port option
57
+ **/
58
+ async function getPortForProject(context, target, defaultPort = 4200) {
59
+ const fmtTarget = (0, devkit_1.targetToTargetString)(target);
60
+ const [hasPortOpt, schemaPortValue] = getValueFromSchema(context, target, 'port');
61
+ let freePort;
62
+ if (hasPortOpt) {
63
+ let normalizedPortValue;
64
+ if (!schemaPortValue) {
65
+ devkit_1.logger.info(`NX ${fmtTarget} did not have a defined port value, checking for free port with the default value of ${defaultPort}`);
66
+ normalizedPortValue = defaultPort;
67
+ }
68
+ else {
69
+ normalizedPortValue = Number(schemaPortValue);
70
+ }
71
+ if (isNaN(normalizedPortValue)) {
72
+ devkit_1.output.warn({
73
+ title: `Port Not a Number`,
74
+ bodyLines: [
75
+ `The port value found was not a number or can't be parsed to a number`,
76
+ `When reading the devServerTarget (${fmtTarget}) schema, expected ${schemaPortValue} to be a number but got NaN.`,
77
+ `Nx will use the default value of ${defaultPort} instead.`,
78
+ `You can manually specify a port by setting the 'port' option`,
79
+ ],
80
+ });
81
+ normalizedPortValue = defaultPort;
82
+ }
83
+ try {
84
+ let attempts = 0;
85
+ // make sure when this check happens in parallel,
86
+ // we don't let the same port be used by multiple projects
87
+ do {
88
+ freePort = await detectPort(freePort || normalizedPortValue);
89
+ if (attemptToLockPort(freePort)) {
90
+ break;
91
+ }
92
+ attempts++;
93
+ // increment port in case the lock file isn't cleaned up
94
+ freePort++;
95
+ } while (attempts < 20);
96
+ devkit_1.logger.info(`NX Using port ${freePort} for ${fmtTarget}`);
97
+ }
98
+ catch (err) {
99
+ throw new Error((0, devkit_1.stripIndents) `Unable to find a free port for the dev server, ${fmtTarget}.
100
+ You can disable auto port detection by specifing a port or not passing a value to --port`);
101
+ }
102
+ }
103
+ else {
104
+ devkit_1.output.warn({
105
+ title: `No Port Option Found`,
106
+ bodyLines: [
107
+ `The 'port' option is set to 'cypress-auto', but the devServerTarget (${fmtTarget}) does not have a port option.`,
108
+ `Because of this, Nx is unable to verify the port is free before starting the dev server.`,
109
+ `This might cause issues if the devServerTarget is trying to use a port that is already in use.`,
110
+ ],
111
+ });
112
+ }
113
+ return freePort;
114
+ }
115
+ /**
116
+ * Check if the given target has the given property in it's options.
117
+ * if the property is does not have a default value or is not in the actual executor options,
118
+ * the value will be undefined even if it's in the executor schema.
119
+ **/
120
+ function getValueFromSchema(context, target, property) {
121
+ let targetOpts;
122
+ try {
123
+ targetOpts = (0, devkit_1.readTargetOptions)(target, context);
124
+ }
125
+ catch (e) {
126
+ throw new Error(`Unable to read the target options for ${(0, devkit_1.targetToTargetString)(target)}.
127
+ Are you sure this is a valid target?
128
+ Was trying to read the target for the property: '${property}', but got the following error:
129
+ ${e.message || e}`);
130
+ }
131
+ let targetHasOpt = Object.keys(targetOpts).includes(property);
132
+ if (!targetHasOpt) {
133
+ // NOTE: readTargetOptions doesn't apply non defaulted values, i.e. @nx/vite has a port options but is optional
134
+ // so we double check the schema if readTargetOptions didn't return a value for the property
135
+ const projectConfig = context.projectsConfigurations?.projects?.[target.project];
136
+ const targetConfig = projectConfig.targets[target.target];
137
+ const [collection, executor] = targetConfig.executor.split(':');
138
+ const { schema } = (0, executor_utils_1.getExecutorInformation)(collection, executor, context.root, context.projectsConfigurations.projects);
139
+ // NOTE: schema won't have a default since readTargetOptions would have
140
+ // already set that and this check wouldn't need to be made
141
+ targetHasOpt = Object.keys(schema.properties).includes(property);
142
+ }
143
+ return [targetHasOpt, targetOpts[property]];
144
+ }
145
+ function attemptToLockPort(port) {
146
+ const portLockFilePath = (0, path_1.join)(__dirname, `${port}.txt`);
147
+ try {
148
+ if ((0, fs_1.existsSync)(portLockFilePath)) {
149
+ return false;
150
+ }
151
+ (0, fs_1.writeFileSync)(portLockFilePath, 'locked');
152
+ return true;
153
+ }
154
+ catch (err) {
155
+ return false;
156
+ }
157
+ }