This PR updates the `@nx/detox:app` generator to match the new TS solution setup. The `@nx/cypress:configuration` and `@nx/cypress:configuration` generators are also updated so that they can be run on existing projects and generator the correct tsconfig files. The Playwright/Cypress example can be seen as follows: ```shell # Skip e2e nx g @nx/react:app apps/demo --bundler vite --e2eTestRunner none # now configure e2e nx g @nx/playwright --project demo ``` Now if you add this line to `apps/demo/e2e/example.spec.ts`: ``` const x: number = 'a'; ``` And run `nx typecheck demo`, it will pass. This happens because the `e2e/**/*.ts` pattern is missing. Thus, we need to ensure that a `tsconfig.e2e.json` project is added for the Playwright spec files. Same thing with Cypress. The Detox generator does not support adding configuration to existing project, so we don't quite get the same problem. The fix for Detox is just to make sure the tsconfig content is not following the old (integrated) version, but the updated TS solution version. ## Current Behavior Detox TS setup is incorrect. Running Cypress and Playwright configuration generator on existing projects generate invalid setup, such that spec files are not typechecked. ## Expected Behavior E2E generators should all generate correct TS setup. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
367 lines
10 KiB
TypeScript
367 lines
10 KiB
TypeScript
import {
|
|
addDependenciesToPackageJson,
|
|
formatFiles,
|
|
generateFiles,
|
|
GeneratorCallback,
|
|
getPackageManagerCommand,
|
|
joinPathFragments,
|
|
logger,
|
|
offsetFromRoot,
|
|
output,
|
|
readNxJson,
|
|
readProjectConfiguration,
|
|
runTasksInSerial,
|
|
toJS,
|
|
Tree,
|
|
updateJson,
|
|
updateNxJson,
|
|
updateProjectConfiguration,
|
|
workspaceRoot,
|
|
writeJson,
|
|
} from '@nx/devkit';
|
|
import { resolveImportPath } from '@nx/devkit/src/generators/project-name-and-root-utils';
|
|
import { getRelativePathToRootTsConfig } from '@nx/js';
|
|
import { normalizeLinterOption } from '@nx/js/src/utils/generator-prompts';
|
|
import {
|
|
getProjectPackageManagerWorkspaceState,
|
|
getProjectPackageManagerWorkspaceStateWarningTask,
|
|
} from '@nx/js/src/utils/package-manager-workspaces';
|
|
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
|
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
|
|
import { execSync } from 'child_process';
|
|
import { PackageJson } from 'nx/src/utils/package-json';
|
|
import * as path from 'path';
|
|
import { addLinterToPlaywrightProject } from '../../utils/add-linter';
|
|
import { nxVersion } from '../../utils/versions';
|
|
import { initGenerator } from '../init/init';
|
|
import type {
|
|
ConfigurationGeneratorSchema,
|
|
NormalizedGeneratorOptions,
|
|
} from './schema';
|
|
|
|
export function configurationGenerator(
|
|
tree: Tree,
|
|
options: ConfigurationGeneratorSchema
|
|
) {
|
|
return configurationGeneratorInternal(tree, { addPlugin: false, ...options });
|
|
}
|
|
|
|
export async function configurationGeneratorInternal(
|
|
tree: Tree,
|
|
rawOptions: ConfigurationGeneratorSchema
|
|
) {
|
|
const options = await normalizeOptions(tree, rawOptions);
|
|
|
|
const tasks: GeneratorCallback[] = [];
|
|
tasks.push(
|
|
await initGenerator(tree, {
|
|
skipFormat: true,
|
|
skipPackageJson: options.skipPackageJson,
|
|
addPlugin: options.addPlugin,
|
|
})
|
|
);
|
|
|
|
const projectConfig = readProjectConfiguration(tree, options.project);
|
|
const offsetFromProjectRoot = offsetFromRoot(projectConfig.root);
|
|
|
|
generateFiles(tree, path.join(__dirname, 'files'), projectConfig.root, {
|
|
offsetFromRoot: offsetFromProjectRoot,
|
|
projectRoot: projectConfig.root,
|
|
webServerCommand: options.webServerCommand ?? null,
|
|
webServerAddress: options.webServerAddress ?? null,
|
|
...options,
|
|
});
|
|
|
|
const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
|
|
const tsconfigPath = joinPathFragments(projectConfig.root, 'tsconfig.json');
|
|
if (tree.exists(tsconfigPath)) {
|
|
if (isTsSolutionSetup) {
|
|
const tsconfig: any = {
|
|
extends: getRelativePathToRootTsConfig(tree, projectConfig.root),
|
|
compilerOptions: {
|
|
allowJs: true,
|
|
outDir: 'out-tsc/playwright',
|
|
sourceMap: false,
|
|
},
|
|
include: [
|
|
joinPathFragments(options.directory, '**/*.ts'),
|
|
joinPathFragments(options.directory, '**/*.js'),
|
|
'playwright.config.ts',
|
|
],
|
|
exclude: ['out-tsc', 'test-output'],
|
|
};
|
|
|
|
// skip eslint from typechecking since it extends from root file that is outside rootDir
|
|
if (options.linter === 'eslint') {
|
|
tsconfig.exclude.push(
|
|
'eslint.config.js',
|
|
'eslint.config.mjs',
|
|
'eslint.config.cjs'
|
|
);
|
|
}
|
|
|
|
writeJson(
|
|
tree,
|
|
joinPathFragments(projectConfig.root, 'tsconfig.e2e.json'),
|
|
tsconfig
|
|
);
|
|
|
|
updateJson(tree, tsconfigPath, (json) => {
|
|
// add the project tsconfig to the workspace root tsconfig.json references
|
|
json.references ??= [];
|
|
json.references.push({ path: './tsconfig.e2e.json' });
|
|
return json;
|
|
});
|
|
}
|
|
} else {
|
|
const tsconfig: any = {
|
|
extends: getRelativePathToRootTsConfig(tree, projectConfig.root),
|
|
compilerOptions: {
|
|
allowJs: true,
|
|
outDir: `${offsetFromProjectRoot}dist/out-tsc`,
|
|
sourceMap: false,
|
|
},
|
|
include: [
|
|
'**/*.ts',
|
|
'**/*.js',
|
|
'playwright.config.ts',
|
|
'src/**/*.spec.ts',
|
|
'src/**/*.spec.js',
|
|
'src/**/*.test.ts',
|
|
'src/**/*.test.js',
|
|
'src/**/*.d.ts',
|
|
],
|
|
};
|
|
|
|
if (isTsSolutionSetup) {
|
|
tsconfig.exclude = ['out-tsc', 'test-output'];
|
|
// skip eslint from typechecking since it extends from root file that is outside rootDir
|
|
if (options.linter === 'eslint') {
|
|
tsconfig.exclude.push(
|
|
'eslint.config.js',
|
|
'eslint.config.mjs',
|
|
'eslint.config.cjs'
|
|
);
|
|
}
|
|
|
|
tsconfig.compilerOptions.outDir = 'out-tsc/playwright';
|
|
|
|
if (!options.rootProject) {
|
|
updateJson(tree, 'tsconfig.json', (json) => {
|
|
// add the project tsconfig to the workspace root tsconfig.json references
|
|
json.references ??= [];
|
|
json.references.push({ path: './' + projectConfig.root });
|
|
return json;
|
|
});
|
|
}
|
|
} else {
|
|
tsconfig.compilerOptions.outDir = `${offsetFromProjectRoot}dist/out-tsc`;
|
|
tsconfig.compilerOptions.module = 'commonjs';
|
|
}
|
|
|
|
writeJson(tree, tsconfigPath, tsconfig);
|
|
}
|
|
|
|
if (isTsSolutionSetup) {
|
|
const packageJsonPath = joinPathFragments(
|
|
projectConfig.root,
|
|
'package.json'
|
|
);
|
|
if (!tree.exists(packageJsonPath)) {
|
|
const importPath = resolveImportPath(
|
|
tree,
|
|
projectConfig.name,
|
|
projectConfig.root
|
|
);
|
|
|
|
const packageJson: PackageJson = {
|
|
name: importPath,
|
|
version: '0.0.1',
|
|
private: true,
|
|
nx: {
|
|
name: options.project,
|
|
},
|
|
};
|
|
writeJson(tree, packageJsonPath, packageJson);
|
|
}
|
|
|
|
ignoreTestOutput(tree);
|
|
}
|
|
|
|
const hasPlugin = readNxJson(tree).plugins?.some((p) =>
|
|
typeof p === 'string'
|
|
? p === '@nx/playwright/plugin'
|
|
: p.plugin === '@nx/playwright/plugin'
|
|
);
|
|
|
|
if (!hasPlugin) {
|
|
addE2eTarget(tree, options);
|
|
setupE2ETargetDefaults(tree);
|
|
}
|
|
|
|
tasks.push(
|
|
await addLinterToPlaywrightProject(tree, {
|
|
project: options.project,
|
|
linter: options.linter,
|
|
skipPackageJson: options.skipPackageJson,
|
|
js: options.js,
|
|
directory: options.directory,
|
|
setParserOptionsProject: options.setParserOptionsProject,
|
|
rootProject: options.rootProject ?? projectConfig.root === '.',
|
|
addPlugin: options.addPlugin,
|
|
})
|
|
);
|
|
|
|
if (options.js) {
|
|
const { ModuleKind } = ensureTypescript();
|
|
toJS(tree, { extension: '.cjs', module: ModuleKind.CommonJS });
|
|
}
|
|
|
|
recommendVsCodeExtensions(tree);
|
|
|
|
if (!options.skipPackageJson) {
|
|
tasks.push(
|
|
addDependenciesToPackageJson(
|
|
tree,
|
|
{},
|
|
{
|
|
// required since used in playwright config
|
|
'@nx/devkit': nxVersion,
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
if (!options.skipInstall) {
|
|
tasks.push(getBrowsersInstallTask());
|
|
}
|
|
|
|
if (!options.skipFormat) {
|
|
await formatFiles(tree);
|
|
}
|
|
|
|
if (isTsSolutionSetup) {
|
|
const projectPackageManagerWorkspaceState =
|
|
getProjectPackageManagerWorkspaceState(tree, projectConfig.root);
|
|
|
|
if (projectPackageManagerWorkspaceState !== 'included') {
|
|
tasks.push(
|
|
getProjectPackageManagerWorkspaceStateWarningTask(
|
|
projectPackageManagerWorkspaceState,
|
|
tree.root
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
return runTasksInSerial(...tasks);
|
|
}
|
|
|
|
async function normalizeOptions(
|
|
tree: Tree,
|
|
options: ConfigurationGeneratorSchema
|
|
): Promise<NormalizedGeneratorOptions> {
|
|
const nxJson = readNxJson(tree);
|
|
const addPlugin =
|
|
options.addPlugin ??
|
|
(process.env.NX_ADD_PLUGINS !== 'false' &&
|
|
nxJson.useInferencePlugins !== false);
|
|
|
|
const linter = await normalizeLinterOption(tree, options.linter);
|
|
|
|
return {
|
|
...options,
|
|
addPlugin,
|
|
linter,
|
|
directory: options.directory ?? 'e2e',
|
|
};
|
|
}
|
|
|
|
function getBrowsersInstallTask() {
|
|
return () => {
|
|
output.log({
|
|
title: 'Ensuring Playwright is installed.',
|
|
bodyLines: ['use --skipInstall to skip installation.'],
|
|
});
|
|
const pmc = getPackageManagerCommand();
|
|
execSync(`${pmc.exec} playwright install`, {
|
|
cwd: workspaceRoot,
|
|
windowsHide: false,
|
|
});
|
|
};
|
|
}
|
|
|
|
function recommendVsCodeExtensions(tree: Tree): void {
|
|
if (tree.exists('.vscode/extensions.json')) {
|
|
updateJson(tree, '.vscode/extensions.json', (json) => {
|
|
json.recommendations ??= [];
|
|
|
|
const recs = new Set(json.recommendations);
|
|
recs.add('ms-playwright.playwright');
|
|
|
|
json.recommendations = Array.from(recs);
|
|
return json;
|
|
});
|
|
} else {
|
|
writeJson(tree, '.vscode/extensions.json', {
|
|
recommendations: ['ms-playwright.playwright'],
|
|
});
|
|
}
|
|
}
|
|
|
|
function setupE2ETargetDefaults(tree: Tree) {
|
|
const nxJson = readNxJson(tree);
|
|
|
|
if (!nxJson.namedInputs) {
|
|
return;
|
|
}
|
|
|
|
// E2e targets depend on all their project's sources + production sources of dependencies
|
|
nxJson.targetDefaults ??= {};
|
|
|
|
const productionFileSet = !!nxJson.namedInputs?.production;
|
|
nxJson.targetDefaults.e2e ??= {};
|
|
nxJson.targetDefaults.e2e.cache ??= true;
|
|
nxJson.targetDefaults.e2e.inputs ??= [
|
|
'default',
|
|
productionFileSet ? '^production' : '^default',
|
|
];
|
|
|
|
updateNxJson(tree, nxJson);
|
|
}
|
|
|
|
function addE2eTarget(tree: Tree, options: ConfigurationGeneratorSchema) {
|
|
const projectConfig = readProjectConfiguration(tree, options.project);
|
|
if (projectConfig?.targets?.e2e) {
|
|
throw new Error(`Project ${options.project} already has an e2e target.
|
|
Rename or remove the existing e2e target.`);
|
|
}
|
|
projectConfig.targets ??= {};
|
|
projectConfig.targets.e2e = {
|
|
executor: '@nx/playwright:playwright',
|
|
outputs: [`{workspaceRoot}/dist/.playwright/${projectConfig.root}`],
|
|
options: {
|
|
config: `${projectConfig.root}/playwright.config.${
|
|
options.js ? 'cjs' : 'ts'
|
|
}`,
|
|
},
|
|
};
|
|
updateProjectConfiguration(tree, options.project, projectConfig);
|
|
}
|
|
|
|
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;
|