The following are the main changes in the context of the TS solution setup: - Ensure `name` in `package.json` files is set to the import path for all projects - Set `nx.name` in `package.json` files when the user provides a name different than the package name (import path) - Clean up project generators so they don't set the `nx` property in `package.json` files unless strictly needed - Fix `@nx/vue:application` generator so it creates the Nx config in a `package.json` file for e2e projects - Ensure `@types/node` is installed in `vitest` generator - Fix generated Vite config typing error (surfaced with Vite 6) - Ensure `jsonc-eslint-parser` is installed when the `@nx/dependency-checks` rule is added to the ESLint config - Misc minor alignment changes ## Current Behavior ## Expected Behavior ## Related Issue(s) Fixes #
406 lines
11 KiB
TypeScript
406 lines
11 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 { promptWhenInteractive } from '@nx/devkit/src/generators/prompt';
|
|
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,
|
|
};
|
|
if (options.project !== importPath) {
|
|
packageJson.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);
|
|
|
|
if (!options.webServerCommand || !options.webServerAddress) {
|
|
const { webServerCommand, webServerAddress } =
|
|
await promptForMissingServeData(options.project);
|
|
options.webServerCommand = webServerCommand;
|
|
options.webServerAddress = webServerAddress;
|
|
}
|
|
|
|
return {
|
|
...options,
|
|
addPlugin,
|
|
linter,
|
|
directory: options.directory ?? 'e2e',
|
|
};
|
|
}
|
|
|
|
async function promptForMissingServeData(projectName: string) {
|
|
const { command, port } = await promptWhenInteractive<{
|
|
command: string;
|
|
port: number;
|
|
}>(
|
|
[
|
|
{
|
|
type: 'input',
|
|
name: 'command',
|
|
message: 'What command should be run to serve the application locally?',
|
|
initial: `npx nx serve ${projectName}`,
|
|
},
|
|
{
|
|
type: 'numeral',
|
|
name: 'port',
|
|
message: 'What port will the application be served on?',
|
|
initial: 3000,
|
|
},
|
|
],
|
|
{
|
|
command: `npx nx serve ${projectName}`,
|
|
port: 3000,
|
|
}
|
|
);
|
|
|
|
return {
|
|
webServerCommand: command,
|
|
webServerAddress: `http://localhost:${port}`,
|
|
};
|
|
}
|
|
|
|
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;
|