feat(testing): add support for the ts solution config setup to the cypress plugin (#28637)

<!-- 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:
Leosvel Pérez Espinosa 2024-10-31 18:29:53 +01:00 committed by GitHub
parent c2e31127d9
commit 6af298d0a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 198 additions and 58 deletions

View File

@ -43,8 +43,8 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"enum": ["none", "eslint"],
"x-priority": "important"
},
"js": {
"description": "Generate JavaScript files rather than TypeScript files.",

View File

@ -22,7 +22,7 @@ import './commands';
// add component testing only related command here, such as mount
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {

View File

@ -1,14 +1,13 @@
import { workspaceRoot } from '@nx/devkit';
import { dirname, join, relative } from 'path';
import { lstatSync } from 'fs';
import vitePreprocessor from '../src/plugins/preprocessor-vite';
import { NX_PLUGIN_OPTIONS } from '../src/utils/constants';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { execSync, spawn } from 'child_process';
import { lstatSync } from 'fs';
import { request as httpRequest } from 'http';
import { request as httpsRequest } from 'https';
import { dirname, join, relative } from 'path';
import type { InlineConfig } from 'vite';
import vitePreprocessor from '../src/plugins/preprocessor-vite';
import { NX_PLUGIN_OPTIONS } from '../src/utils/constants';
// Importing the cypress type here causes the angular and next unit
// tests to fail when transpiling, it seems like the cypress types are
@ -54,14 +53,13 @@ export function nxBaseCypressPreset(
: dirname(pathToConfig);
const projectPath = relative(workspaceRoot, normalizedPath);
const offset = relative(normalizedPath, workspaceRoot);
const videosFolder = join(offset, 'dist', 'cypress', projectPath, 'videos');
const screenshotsFolder = join(
offset,
'dist',
'cypress',
projectPath,
'screenshots'
);
const isTsSolutionSetup = isUsingTsSolutionSetup();
const videosFolder = isTsSolutionSetup
? join('test-output', 'cypress', 'videos')
: join(offset, 'dist', 'cypress', projectPath, 'videos');
const screenshotsFolder = isTsSolutionSetup
? join('test-output', 'cypress', 'screenshots')
: join(offset, 'dist', 'cypress', projectPath, 'screenshots');
return {
videosFolder,

View File

@ -4,11 +4,12 @@ import {
generateFiles,
joinPathFragments,
offsetFromRoot,
readJson,
readProjectConfiguration,
updateJson,
readJson,
} from '@nx/devkit';
import { getRelativePathToRootTsConfig } from '@nx/js';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { join } from 'path';
export interface CypressBaseSetupSchema {
@ -44,6 +45,7 @@ export function addBaseCypressSetup(
tsConfigPath: opts.hasTsConfig
? `${opts.offsetFromProjectRoot}tsconfig.json`
: getRelativePathToRootTsConfig(tree, projectConfig.root),
linter: isEslintInstalled(tree) ? 'eslint' : 'none',
ext: '',
};
@ -54,6 +56,15 @@ export function addBaseCypressSetup(
templateVars
);
generateFiles(
tree,
isUsingTsSolutionSetup(tree)
? join(__dirname, 'files/tsconfig/ts-solution')
: join(__dirname, 'files/tsconfig/non-ts-solution'),
projectConfig.root,
templateVars
);
if (options.js) {
if (isEsmProject(tree, projectConfig.root)) {
generateFiles(
@ -144,3 +155,8 @@ function isEsmProject(tree: Tree, projectRoot: string) {
}
return packageJson.type === 'module';
}
function isEslintInstalled(tree: Tree): boolean {
const { dependencies, devDependencies } = readJson(tree, 'package.json');
return !!(dependencies?.eslint || devDependencies?.eslint);
}

View File

@ -10,11 +10,13 @@
// https://on.cypress.io/custom-commands
// ***********************************************
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
declare global {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-namespace<% } %>
namespace Cypress {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-unused-vars<% } %>
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
}

View File

@ -10,11 +10,13 @@
// https://on.cypress.io/custom-commands
// ***********************************************
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
declare global {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-namespace<% } %>
namespace Cypress {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-unused-vars<% } %>
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
}

View File

@ -10,11 +10,13 @@
// https://on.cypress.io/custom-commands
// ***********************************************
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
declare global {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-namespace<% } %>
namespace Cypress {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-unused-vars<% } %>
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
}

View File

@ -10,11 +10,13 @@
// https://on.cypress.io/custom-commands
// ***********************************************
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
declare global {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-namespace<% } %>
namespace Cypress {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-unused-vars<% } %>
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
}

View File

@ -12,7 +12,7 @@
"**/*.js",
"<%= offsetFromProjectRoot %>cypress.config.ts",
"<%= offsetFromProjectRoot %>**/*.cy.ts",
<%_ if (jsx) { _%> "<%= offsetFromProjectRoot %>**/*.cy.tsx",<%_ } _%>
<%_ if (jsx) { _%>"<%= offsetFromProjectRoot %>**/*.cy.tsx",<%_ } _%>
"<%= offsetFromProjectRoot %>**/*.cy.js",
<%_ if (jsx) { _%>"<%= offsetFromProjectRoot %>**/*.cy.jsx",<%_ } _%>
"<%= offsetFromProjectRoot %>**/*.d.ts"

View File

@ -0,0 +1,20 @@
{
"extends": "<%= tsConfigPath %>",
"compilerOptions": {
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
"allowJs": true,
"types": ["cypress", "node"],
"sourceMap": false
},
"include": [
"**/*.ts",
"**/*.js",
"<%= offsetFromProjectRoot %>cypress.config.ts",
"<%= offsetFromProjectRoot %>**/*.cy.ts",
<%_ if (jsx) { _%>"<%= offsetFromProjectRoot %>**/*.cy.tsx",<%_ } _%>
"<%= offsetFromProjectRoot %>**/*.cy.js",
<%_ if (jsx) { _%>"<%= offsetFromProjectRoot %>**/*.cy.jsx",<%_ } _%>
"<%= offsetFromProjectRoot %>**/*.d.ts"
]
}

View File

@ -13,6 +13,7 @@ import {
updateNxJson,
runTasksInSerial,
GeneratorCallback,
readJson,
} from '@nx/devkit';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { installedCypressVersion } from '../../utils/cypress-version';
@ -139,6 +140,7 @@ function addProjectFiles(
...opts,
projectRoot: projectConfig.root,
offsetFromRoot: offsetFromRoot(projectConfig.root),
linter: isEslintInstalled(tree) ? 'eslint' : 'none',
ext: '',
}
);
@ -255,4 +257,9 @@ export function updateTsConfigForComponentTesting(
}
}
function isEslintInstalled(tree: Tree): boolean {
const { dependencies, devDependencies } = readJson(tree, 'package.json');
return !!(dependencies?.eslint || devDependencies?.eslint);
}
export default componentConfigurationGenerator;

View File

@ -17,11 +17,10 @@
import './commands';
// add component testing only related command here, such as mount
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
}
declare global {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-namespace<% } %>
namespace Cypress {<% if (linter === 'eslint') { %>
// eslint-disable-next-line @typescript-eslint/no-unused-vars<% } %>
interface Chainable<Subject> {}
}
}

View File

@ -5,6 +5,7 @@ import {
generateFiles,
GeneratorCallback,
joinPathFragments,
logger,
offsetFromRoot,
parseTargetString,
ProjectConfiguration,
@ -16,13 +17,21 @@ import {
Tree,
updateJson,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import { resolveImportPath } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt';
import { Linter, LinterType } from '@nx/eslint';
import {
getRelativePathToRootTsConfig,
initGenerator as jsInitGenerator,
} from '@nx/js';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import {
getProjectPackageManagerWorkspaceState,
getProjectPackageManagerWorkspaceStateWarningTask,
} from '@nx/js/src/utils/package-manager-workspaces';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { PackageJson } from 'nx/src/utils/package-json';
import { join } from 'path';
import { addLinterToCyProject } from '../../utils/add-linter';
import { addDefaultE2EConfig } from '../../utils/config';
@ -52,7 +61,7 @@ export interface CypressE2EConfigSchema {
addPlugin?: boolean;
}
type NormalizedSchema = ReturnType<typeof normalizeOptions>;
type NormalizedSchema = Awaited<ReturnType<typeof normalizeOptions>>;
export function configurationGenerator(
tree: Tree,
@ -68,10 +77,7 @@ export async function configurationGeneratorInternal(
tree: Tree,
options: CypressE2EConfigSchema
) {
assertNotUsingTsSolutionSetup(tree, 'cypress', 'configuration');
const opts = normalizeOptions(tree, options);
opts.addPlugin ??= process.env.NX_ADD_PLUGINS !== 'false';
const opts = await normalizeOptions(tree, options);
const tasks: GeneratorCallback[] = [];
const projectGraph = await createProjectGraphAsync();
@ -99,6 +105,22 @@ export async function configurationGeneratorInternal(
addTarget(tree, opts, projectGraph);
}
const { root: projectRoot } = readProjectConfiguration(tree, options.project);
const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
if (isTsSolutionSetup) {
createPackageJson(tree, opts);
ignoreTestOutput(tree);
if (!options.rootProject) {
// add the project tsconfig to the workspace root tsconfig.json references
updateJson(tree, 'tsconfig.json', (json) => {
json.references ??= [];
json.references.push({ path: './' + projectRoot });
return json;
});
}
}
const linterTask = await addLinterToCyProject(tree, {
...opts,
cypressDir: opts.directory,
@ -113,6 +135,20 @@ export async function configurationGeneratorInternal(
await formatFiles(tree);
}
if (isTsSolutionSetup) {
const projectPackageManagerWorkspaceState =
getProjectPackageManagerWorkspaceState(tree, projectRoot);
if (projectPackageManagerWorkspaceState !== 'included') {
tasks.push(
getProjectPackageManagerWorkspaceStateWarningTask(
projectPackageManagerWorkspaceState,
tree.root
)
);
}
}
return runTasksInSerial(...tasks);
}
@ -128,7 +164,30 @@ function ensureDependencies(tree: Tree, options: NormalizedSchema) {
return addDependenciesToPackageJson(tree, {}, devDependencies);
}
function normalizeOptions(tree: Tree, options: CypressE2EConfigSchema) {
async function normalizeOptions(tree: Tree, options: CypressE2EConfigSchema) {
const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
let linter = options.linter;
if (!linter) {
const choices = isTsSolutionSetup
? [{ name: 'none' }, { name: 'eslint' }]
: [{ name: 'eslint' }, { name: 'none' }];
const defaultValue = isTsSolutionSetup ? 'none' : 'eslint';
linter = await promptWhenInteractive<{
linter: 'none' | 'eslint';
}>(
{
type: 'select',
name: 'linter',
message: `Which linter would you like to use?`,
choices,
initial: 0,
},
{ linter: defaultValue }
).then(({ linter }) => linter);
}
const projectConfig: ProjectConfiguration | undefined =
readProjectConfiguration(tree, options.project);
if (projectConfig?.targets?.e2e) {
@ -164,7 +223,7 @@ In this case you need to provide a devServerTarget,'<projectName>:<targetName>[:
...options,
bundler: options.bundler ?? 'webpack',
rootProject: options.rootProject ?? projectConfig.root === '.',
linter: options.linter ?? Linter.EsLint,
linter,
devServerTarget,
};
}
@ -352,4 +411,40 @@ function addTarget(
updateProjectConfiguration(tree, opts.project, projectConfig);
}
function createPackageJson(tree: Tree, options: NormalizedSchema) {
const projectConfig = readProjectConfiguration(tree, options.project);
const packageJsonPath = joinPathFragments(projectConfig.root, 'package.json');
if (tree.exists(packageJsonPath)) {
return;
}
const importPath = resolveImportPath(
tree,
projectConfig.name,
projectConfig.root
);
const packageJson: PackageJson = {
name: importPath,
version: '0.0.1',
private: true,
};
writeJson(tree, packageJsonPath, packageJson);
}
function ignoreTestOutput(tree: Tree): void {
if (!tree.exists('.gitignore')) {
logger.warn(`Couldn't find a root .gitignore file to update.`);
}
let content = tree.read('.gitignore', 'utf-8');
if (/^test-output$/gm.test(content)) {
return;
}
content = `${content}\ntest-output\n`;
tree.write('.gitignore', content);
}
export default configurationGenerator;

View File

@ -46,8 +46,8 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"enum": ["none", "eslint"],
"x-priority": "important"
},
"js": {
"description": "Generate JavaScript files rather than TypeScript files.",

View File

@ -11,7 +11,6 @@ import {
updateNxJson,
} from '@nx/devkit';
import { addPlugin as _addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { createNodesV2 } from '../../plugins/plugin';
import { cypressVersion, nxVersion } from '../../utils/versions';
import { Schema } from './schema';
@ -106,8 +105,6 @@ export async function cypressInitGeneratorInternal(
tree: Tree,
options: Schema
) {
assertNotUsingTsSolutionSetup(tree, 'cypress', 'init');
updateProductionFileset(tree);
const nxJson = readNxJson(tree);