UNPKG

@nx/playwright

Version:

The Nx Plugin for Playwright contains executors and generators allowing your workspace to use the powerful Playwright integration testing capabilities.

317 lines (316 loc) 13.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.configurationGenerator = configurationGenerator; exports.configurationGeneratorInternal = configurationGeneratorInternal; const tslib_1 = require("tslib"); const internal_1 = require("@nx/devkit/internal"); const devkit_1 = require("@nx/devkit"); const js_1 = require("@nx/js"); const internal_2 = require("@nx/js/internal"); const deprecation_1 = require("../../utils/deprecation"); const child_process_1 = require("child_process"); const path = tslib_1.__importStar(require("path")); const add_linter_1 = require("../../utils/add-linter"); const assert_supported_playwright_version_1 = require("../../utils/assert-supported-playwright-version"); const versions_1 = require("../../utils/versions"); const init_1 = require("../init/init"); const internal_3 = require("@nx/eslint/internal"); function configurationGenerator(tree, options) { return configurationGeneratorInternal(tree, { addPlugin: false, ...options }); } async function configurationGeneratorInternal(tree, rawOptions) { (0, assert_supported_playwright_version_1.assertSupportedPlaywrightVersion)(tree); const options = await normalizeOptions(tree, rawOptions); const tasks = []; tasks.push(await (0, init_1.initGenerator)(tree, { skipFormat: true, skipPackageJson: options.skipPackageJson, addPlugin: options.addPlugin, })); const projectConfig = (0, devkit_1.readProjectConfiguration)(tree, options.project); const offsetFromProjectRoot = (0, devkit_1.offsetFromRoot)(projectConfig.root); const isTsSolutionSetup = (0, internal_2.isUsingTsSolutionSetup)(tree); // Always emit `playwright.config.mts`. Node forces `.mts` to ESM // regardless of workspace `type`, so Playwright's runtime routes it // through the ESM loader (`requireOrImport` -> dynamic `import()`), // bypassing the pirates CJS-compile path that breaks ESM-shape `.ts` // configs. Nx's native TS strip loads `.mts` directly via `loadTsFile`. // Playwright's configLoader auto-discovers `.mts` (extension list at // configLoader.js:313 is `.ts/.js/.mts/.mjs/.cts/.cjs`). (0, devkit_1.generateFiles)(tree, path.join(__dirname, 'files'), projectConfig.root, { offsetFromRoot: offsetFromProjectRoot, projectRoot: projectConfig.root, webServerCommand: options.webServerCommand ?? null, webServerAddress: options.webServerAddress ?? null, isTsSolutionSetup, ...options, }); const playwrightConfigFile = options.js ? 'playwright.config.mjs' : 'playwright.config.mts'; const tsconfigPath = (0, devkit_1.joinPathFragments)(projectConfig.root, 'tsconfig.json'); if (tree.exists(tsconfigPath)) { if (isTsSolutionSetup) { const tsconfig = { extends: (0, js_1.getRelativePathToRootTsConfig)(tree, projectConfig.root), compilerOptions: { allowJs: true, outDir: 'out-tsc/playwright', sourceMap: false, }, include: [ (0, devkit_1.joinPathFragments)(options.directory, '**/*.ts'), (0, devkit_1.joinPathFragments)(options.directory, '**/*.js'), playwrightConfigFile, ], 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'); } (0, devkit_1.writeJson)(tree, (0, devkit_1.joinPathFragments)(projectConfig.root, 'tsconfig.e2e.json'), tsconfig); (0, devkit_1.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 = { extends: (0, js_1.getRelativePathToRootTsConfig)(tree, projectConfig.root), compilerOptions: { allowJs: true, outDir: `${offsetFromProjectRoot}dist/out-tsc`, sourceMap: false, }, include: [ '**/*.ts', '**/*.js', playwrightConfigFile, '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) { (0, devkit_1.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'; } (0, devkit_1.writeJson)(tree, tsconfigPath, tsconfig); } if (isTsSolutionSetup) { const packageJsonPath = (0, devkit_1.joinPathFragments)(projectConfig.root, 'package.json'); if (!tree.exists(packageJsonPath)) { const importPath = (0, internal_1.resolveImportPath)(tree, projectConfig.name, projectConfig.root); const packageJson = { name: importPath, version: '0.0.1', private: true, }; if (options.project !== importPath) { packageJson.nx = { name: options.project }; } (0, devkit_1.writeJson)(tree, packageJsonPath, packageJson); } ignoreTestOutput(tree, options); } const hasPlugin = (0, devkit_1.readNxJson)(tree).plugins?.some((p) => typeof p === 'string' ? p === '@nx/playwright/plugin' : p.plugin === '@nx/playwright/plugin'); if (!hasPlugin) { (0, deprecation_1.warnPlaywrightExecutorGenerating)(); addE2eTarget(tree, options); setupE2ETargetDefaults(tree); } tasks.push(await (0, add_linter_1.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 } = (0, internal_2.ensureTypescript)(); (0, devkit_1.toJS)(tree, { extension: '.mjs', module: ModuleKind.ESNext }); } recommendVsCodeExtensions(tree); if (!options.skipPackageJson) { tasks.push((0, devkit_1.addDependenciesToPackageJson)(tree, {}, { // required since used in playwright config '@nx/devkit': versions_1.nxVersion, })); } if (!options.skipInstall) { tasks.push(getBrowsersInstallTask()); } if (!options.skipFormat) { await (0, devkit_1.formatFiles)(tree); } if (isTsSolutionSetup) { const projectPackageManagerWorkspaceState = (0, internal_2.getProjectPackageManagerWorkspaceState)(tree, projectConfig.root); if (projectPackageManagerWorkspaceState !== 'included') { tasks.push((0, internal_2.getProjectPackageManagerWorkspaceStateWarningTask)(projectPackageManagerWorkspaceState, tree.root)); } } return (0, devkit_1.runTasksInSerial)(...tasks); } async function normalizeOptions(tree, options) { const nxJson = (0, devkit_1.readNxJson)(tree); const addPlugin = options.addPlugin ?? (process.env.NX_ADD_PLUGINS !== 'false' && nxJson.useInferencePlugins !== false); const linter = await (0, internal_2.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) { const { command, port } = await (0, internal_1.promptWhenInteractive)([ { 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 () => { devkit_1.output.log({ title: 'Ensuring Playwright is installed.', bodyLines: ['use --skipInstall to skip installation.'], }); const pmc = (0, devkit_1.getPackageManagerCommand)(); (0, child_process_1.execSync)(`${pmc.exec} playwright install`, { cwd: devkit_1.workspaceRoot, windowsHide: true, }); }; } function recommendVsCodeExtensions(tree) { if (tree.exists('.vscode/extensions.json')) { (0, devkit_1.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 { (0, devkit_1.writeJson)(tree, '.vscode/extensions.json', { recommendations: ['ms-playwright.playwright'], }); } } function setupE2ETargetDefaults(tree) { const nxJson = (0, devkit_1.readNxJson)(tree); if (!nxJson.namedInputs) { return; } // E2e targets depend on all their project's sources + production sources of dependencies const productionFileSet = !!nxJson.namedInputs?.production; // Either a `target: 'e2e'` default or a default keyed on the executor // we're about to scaffold will apply to the new target — consider both // before deciding to add cache/inputs. Target-keyed wins when both are // present. const existingForTarget = (0, internal_1.findTargetDefault)(nxJson.targetDefaults, { target: 'e2e', }); const existingForExecutor = (0, internal_1.findTargetDefault)(nxJson.targetDefaults, { executor: '@nx/playwright:playwright', }); const existingCache = existingForTarget?.cache ?? existingForExecutor?.cache; const existingInputs = existingForTarget?.inputs ?? existingForExecutor?.inputs; const patch = {}; if (existingCache === undefined) { patch.cache = true; } if (existingInputs === undefined) { patch.inputs = ['default', productionFileSet ? '^production' : '^default']; } if (Object.keys(patch).length > 0) { (0, internal_1.upsertTargetDefault)(tree, nxJson, { target: 'e2e', ...patch }); (0, devkit_1.updateNxJson)(tree, nxJson); } } function addE2eTarget(tree, options) { const projectConfig = (0, devkit_1.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: { // Generator emits `playwright.config.mts` (`.mjs` for `--js`) so the // legacy executor's `--config` flag must point at the same extension. config: `${projectConfig.root}/playwright.config.${options.js ? 'mjs' : 'mts'}`, }, }; (0, devkit_1.updateProjectConfiguration)(tree, options.project, projectConfig); } function ignoreTestOutput(tree, options) { // Make sure playwright outputs are not linted. if (options.linter === 'eslint') { (0, internal_3.addIgnoresToLintConfig)(tree, '', ['**/test-output']); } // Handle gitignore if (!tree.exists('.gitignore')) { devkit_1.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); } exports.default = configurationGenerator;