nx/packages/playwright/src/migrations/update-19-6-0/use-serve-static-preview-for-command.ts
Craigory Coppola 0edd1102f7
fix(core): avoid launching default plugins twice (#29539)
<!-- 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
Default plugins are launched twice when loading plugins in a workspace
that has local plugins:
- Once to resolve the local plugin
- Once to be used as an actual plugin

## Expected Behavior
Default plugins are launched once and reused

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #

---------

Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
2025-01-08 18:10:57 -05:00

306 lines
9.2 KiB
TypeScript

import {
CreateNodesV2,
createProjectGraphAsync,
formatFiles,
joinPathFragments,
parseTargetString,
ProjectGraph,
readNxJson,
type Tree,
visitNotIgnoredFiles,
} from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults';
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/loaded-nx-plugin';
import { retrieveProjectConfigurations } from 'nx/src/project-graph/utils/retrieve-workspace-files';
import { ProjectConfigurationsError } from 'nx/src/project-graph/error-types';
import { createNodesV2 as webpackCreateNodesV2 } from '@nx/webpack/src/plugins/plugin';
import { createNodesV2 as viteCreateNodesV2 } from '@nx/vite/plugin';
import type { Node } from 'typescript';
export default async function (tree: Tree) {
const graph = await createProjectGraphAsync();
const collectedProjects: {
projectName: string;
configFile: string;
configFileType: 'webpack' | 'vite';
playwrightConfigFile: string;
commandValueNode: Node;
portFlagValue: string | undefined;
}[] = [];
visitNotIgnoredFiles(tree, '', async (path) => {
if (!path.endsWith('playwright.config.ts')) {
return;
}
let playwrightConfigFileContents = tree.read(path, 'utf-8');
const WEBSERVER_COMMAND_SELECTOR =
'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=command]) > StringLiteral';
let ast = tsquery.ast(playwrightConfigFileContents);
const nodes = tsquery(ast, WEBSERVER_COMMAND_SELECTOR, {
visitAllChildren: true,
});
if (!nodes.length) {
return;
}
const commandValueNode = nodes[0];
const command = commandValueNode.getText();
let project: string;
let portFlagValue: string | undefined;
if (command.includes('nx run')) {
const NX_RUN_TARGET_REGEX =
/(?<=nx run )([^' ]+)(?: [^']*--port[= ](\d+))?/;
const matches = command.match(NX_RUN_TARGET_REGEX);
if (!matches) {
return;
}
const targetString = matches[1];
const parsedTargetString = parseTargetString(targetString, graph);
if (
parsedTargetString.target === 'serve-static' ||
parsedTargetString.target === 'preview'
) {
return;
}
project = parsedTargetString.project;
portFlagValue = matches[2];
} else {
const NX_PROJECT_REGEX =
/(?<=nx [^ ]+ )([^' ]+)(?: [^']*--port[= ](\d+))?/;
const matches = command.match(NX_PROJECT_REGEX);
if (!matches) {
return;
}
project = matches[1];
portFlagValue = matches[2];
}
if (!project || !graph.nodes[project]) {
return;
}
const pathToViteConfig = [
joinPathFragments(graph.nodes[project].data.root, 'vite.config.ts'),
joinPathFragments(graph.nodes[project].data.root, 'vite.config.js'),
].find((p) => tree.exists(p));
const pathToWebpackConfig = [
joinPathFragments(graph.nodes[project].data.root, 'webpack.config.ts'),
joinPathFragments(graph.nodes[project].data.root, 'webpack.config.js'),
].find((p) => tree.exists(p));
const configFile = pathToWebpackConfig ?? pathToViteConfig;
// Only migrate projects that have a webpack or vite config file
if (configFile) {
collectedProjects.push({
projectName: project,
configFile,
configFileType: pathToWebpackConfig ? 'webpack' : 'vite',
playwrightConfigFile: path,
commandValueNode,
portFlagValue,
});
}
});
for (const projectToMigrate of collectedProjects) {
let playwrightConfigFileContents = tree.read(
projectToMigrate.playwrightConfigFile,
'utf-8'
);
const targetName =
(await getServeStaticTargetNameForConfigFile(
tree,
projectToMigrate.configFileType === 'webpack'
? '@nx/webpack/plugin'
: '@nx/vite/plugin',
projectToMigrate.configFile,
projectToMigrate.configFileType === 'webpack'
? 'serve-static'
: 'preview',
projectToMigrate.configFileType === 'webpack'
? 'serveStaticTargetName'
: 'previewTargetName',
projectToMigrate.configFileType === 'webpack'
? webpackCreateNodesV2
: viteCreateNodesV2
)) ??
getServeStaticLikeTarget(
tree,
graph,
projectToMigrate.projectName,
projectToMigrate.configFileType === 'webpack'
? '@nx/web:file-server'
: '@nx/vite:preview-server'
);
if (!targetName) {
continue;
}
const oldCommand = projectToMigrate.commandValueNode.getText();
const newCommand = oldCommand.replace(
/nx.*[^"']/,
`nx run ${projectToMigrate.projectName}:${targetName}${
projectToMigrate.portFlagValue
? ` --port=${projectToMigrate.portFlagValue}`
: ''
}`
);
if (projectToMigrate.configFileType === 'webpack') {
tree.write(
projectToMigrate.playwrightConfigFile,
`${playwrightConfigFileContents.slice(
0,
projectToMigrate.commandValueNode.getStart()
)}${newCommand}${playwrightConfigFileContents.slice(
projectToMigrate.commandValueNode.getEnd()
)}`
);
} else {
tree.write(
projectToMigrate.playwrightConfigFile,
`${playwrightConfigFileContents.slice(
0,
projectToMigrate.commandValueNode.getStart()
)}${newCommand}${playwrightConfigFileContents.slice(
projectToMigrate.commandValueNode.getEnd()
)}`
);
playwrightConfigFileContents = tree.read(
projectToMigrate.playwrightConfigFile,
'utf-8'
);
let ast = tsquery.ast(playwrightConfigFileContents);
const BASE_URL_SELECTOR =
'VariableDeclaration:has(Identifier[name=baseURL])';
const baseUrlNodes = tsquery(ast, BASE_URL_SELECTOR, {
visitAllChildren: true,
});
if (!baseUrlNodes.length) {
return;
}
const serverUrl = `http://localhost:${
projectToMigrate.portFlagValue ?? '4300'
}`;
const baseUrlNode = baseUrlNodes[0];
const newBaseUrlVariableDeclaration = `baseURL = process.env['BASE_URL'] || '${serverUrl}';`;
tree.write(
projectToMigrate.playwrightConfigFile,
`${playwrightConfigFileContents.slice(
0,
baseUrlNode.getStart()
)}${newBaseUrlVariableDeclaration}${playwrightConfigFileContents.slice(
baseUrlNode.getEnd()
)}`
);
playwrightConfigFileContents = tree.read(
projectToMigrate.playwrightConfigFile,
'utf-8'
);
ast = tsquery.ast(playwrightConfigFileContents);
const WEB_SERVER_URL_SELECTOR =
'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=url]) > StringLiteral';
const webServerUrlNodes = tsquery(ast, WEB_SERVER_URL_SELECTOR, {
visitAllChildren: true,
});
if (!webServerUrlNodes.length) {
return;
}
const webServerUrlNode = webServerUrlNodes[0];
const newWebServerUrl = `'${serverUrl}'`;
tree.write(
projectToMigrate.playwrightConfigFile,
`${playwrightConfigFileContents.slice(
0,
webServerUrlNode.getStart()
)}${newWebServerUrl}${playwrightConfigFileContents.slice(
webServerUrlNode.getEnd()
)}`
);
}
}
await addE2eCiTargetDefaults(tree);
await formatFiles(tree);
}
async function getServeStaticTargetNameForConfigFile<T>(
tree: Tree,
pluginName: string,
configFile: string,
defaultTargetName: string,
targetNamePluginOption: keyof T,
createNodesV2: CreateNodesV2<T>
) {
const nxJson = readNxJson(tree);
const matchingPluginRegistrations = nxJson.plugins?.filter((p) =>
typeof p === 'string' ? p === pluginName : p.plugin === pluginName
);
if (!matchingPluginRegistrations) {
return undefined;
}
let targetName = undefined;
for (const plugin of matchingPluginRegistrations) {
let projectConfigs: ConfigurationResult;
try {
const loadedPlugin = new LoadedNxPlugin(
{ createNodesV2, name: pluginName },
plugin
);
projectConfigs = await retrieveProjectConfigurations(
[loadedPlugin],
tree.root,
nxJson
);
} catch (e) {
if (e instanceof ProjectConfigurationsError) {
projectConfigs = e.partialProjectConfigurationsResult;
} else {
throw e;
}
}
if (projectConfigs.matchingProjectFiles.includes(configFile)) {
targetName =
typeof plugin === 'string'
? defaultTargetName
: (plugin.options?.[targetNamePluginOption] as string) ??
defaultTargetName;
}
}
return targetName;
}
function getServeStaticLikeTarget(
tree: Tree,
graph: ProjectGraph,
projectName: string,
executorName: string
) {
if (!graph.nodes[projectName]?.data?.targets) {
return;
}
for (const [targetName, targetOptions] of Object.entries(
graph.nodes[projectName].data.targets
)) {
if (targetOptions.executor && targetOptions.executor === executorName) {
return targetName;
}
}
}