feat(testing): add playwright generator to convert from executors to plugin (#22784)
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
This commit is contained in:
parent
0197bfecc9
commit
25eeddc338
@ -1,6 +1,12 @@
|
|||||||
# Type alias: ExpandedPluginConfiguration
|
# Type alias: ExpandedPluginConfiguration\<T\>
|
||||||
|
|
||||||
Ƭ **ExpandedPluginConfiguration**: `Object`
|
Ƭ **ExpandedPluginConfiguration**\<`T`\>: `Object`
|
||||||
|
|
||||||
|
#### Type parameters
|
||||||
|
|
||||||
|
| Name | Type |
|
||||||
|
| :--- | :-------- |
|
||||||
|
| `T` | `unknown` |
|
||||||
|
|
||||||
#### Type declaration
|
#### Type declaration
|
||||||
|
|
||||||
@ -8,5 +14,5 @@
|
|||||||
| :--------- | :--------- |
|
| :--------- | :--------- |
|
||||||
| `exclude?` | `string`[] |
|
| `exclude?` | `string`[] |
|
||||||
| `include?` | `string`[] |
|
| `include?` | `string`[] |
|
||||||
| `options?` | `unknown` |
|
| `options?` | `T` |
|
||||||
| `plugin` | `string` |
|
| `plugin` | `string` |
|
||||||
|
|||||||
@ -8575,6 +8575,14 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "convert-to-inferred",
|
||||||
|
"path": "/nx-api/playwright/generators/convert-to-inferred",
|
||||||
|
"name": "convert-to-inferred",
|
||||||
|
"children": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"disableCollapsible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
|
|||||||
@ -2021,6 +2021,15 @@
|
|||||||
"originalFilePath": "/packages/playwright/src/generators/init/schema.json",
|
"originalFilePath": "/packages/playwright/src/generators/init/schema.json",
|
||||||
"path": "/nx-api/playwright/generators/init",
|
"path": "/nx-api/playwright/generators/init",
|
||||||
"type": "generator"
|
"type": "generator"
|
||||||
|
},
|
||||||
|
"/nx-api/playwright/generators/convert-to-inferred": {
|
||||||
|
"description": "Convert existing Playwright project(s) using `@nx/playwright:playwright` executor to use `@nx/playwright/plugin`.",
|
||||||
|
"file": "generated/packages/playwright/generators/convert-to-inferred.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "convert-to-inferred",
|
||||||
|
"originalFilePath": "/packages/playwright/src/generators/convert-to-inferred/schema.json",
|
||||||
|
"path": "/nx-api/playwright/generators/convert-to-inferred",
|
||||||
|
"type": "generator"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"path": "/nx-api/playwright"
|
"path": "/nx-api/playwright"
|
||||||
|
|||||||
@ -1997,6 +1997,15 @@
|
|||||||
"originalFilePath": "/packages/playwright/src/generators/init/schema.json",
|
"originalFilePath": "/packages/playwright/src/generators/init/schema.json",
|
||||||
"path": "playwright/generators/init",
|
"path": "playwright/generators/init",
|
||||||
"type": "generator"
|
"type": "generator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Convert existing Playwright project(s) using `@nx/playwright:playwright` executor to use `@nx/playwright/plugin`.",
|
||||||
|
"file": "generated/packages/playwright/generators/convert-to-inferred.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "convert-to-inferred",
|
||||||
|
"originalFilePath": "/packages/playwright/src/generators/convert-to-inferred/schema.json",
|
||||||
|
"path": "playwright/generators/convert-to-inferred",
|
||||||
|
"type": "generator"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"githubRoot": "https://github.com/nrwl/nx/blob/master",
|
"githubRoot": "https://github.com/nrwl/nx/blob/master",
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "convert-to-inferred",
|
||||||
|
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
|
||||||
|
"schema": {
|
||||||
|
"$schema": "https://json-schema.org/schema",
|
||||||
|
"$id": "NxPlaywrightConvertToInferred",
|
||||||
|
"description": "Convert existing Playwright project(s) using `@nx/playwright:playwright` executor to use `@nx/playwright/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
|
||||||
|
"title": "Convert Playwright project from executor to plugin",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The project to convert from using the `@nx/playwright:playwright` executor to use `@nx/playwright/plugin`.",
|
||||||
|
"x-priority": "important"
|
||||||
|
},
|
||||||
|
"skipFormat": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to format files at the end of the migration.",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"presets": []
|
||||||
|
},
|
||||||
|
"description": "Convert existing Playwright project(s) using `@nx/playwright:playwright` executor to use `@nx/playwright/plugin`.",
|
||||||
|
"implementation": "/packages/playwright/src/generators/convert-to-inferred/convert-to-inferred.ts",
|
||||||
|
"aliases": [],
|
||||||
|
"hidden": false,
|
||||||
|
"path": "/packages/playwright/src/generators/convert-to-inferred/schema.json",
|
||||||
|
"type": "generator"
|
||||||
|
}
|
||||||
@ -556,6 +556,7 @@
|
|||||||
- [generators](/nx-api/playwright/generators)
|
- [generators](/nx-api/playwright/generators)
|
||||||
- [configuration](/nx-api/playwright/generators/configuration)
|
- [configuration](/nx-api/playwright/generators/configuration)
|
||||||
- [init](/nx-api/playwright/generators/init)
|
- [init](/nx-api/playwright/generators/init)
|
||||||
|
- [convert-to-inferred](/nx-api/playwright/generators/convert-to-inferred)
|
||||||
- [plugin](/nx-api/plugin)
|
- [plugin](/nx-api/plugin)
|
||||||
- [documents](/nx-api/plugin/documents)
|
- [documents](/nx-api/plugin/documents)
|
||||||
- [Overview](/nx-api/plugin/documents/overview)
|
- [Overview](/nx-api/plugin/documents/overview)
|
||||||
|
|||||||
@ -34,7 +34,8 @@
|
|||||||
"tmp": "~0.2.1",
|
"tmp": "~0.2.1",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"semver": "^7.5.3",
|
"semver": "^7.5.3",
|
||||||
"yargs-parser": "21.1.1"
|
"yargs-parser": "21.1.1",
|
||||||
|
"minimatch": "9.0.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"nx": ">= 16 <= 19"
|
"nx": ">= 16 <= 19"
|
||||||
|
|||||||
@ -0,0 +1,331 @@
|
|||||||
|
import type { TargetConfiguration } from 'nx/src/config/workspace-json-project-json';
|
||||||
|
import type {
|
||||||
|
ExpandedPluginConfiguration,
|
||||||
|
NxJsonConfiguration,
|
||||||
|
} from 'nx/src/config/nx-json';
|
||||||
|
import type { Tree } from 'nx/src/generators/tree';
|
||||||
|
import type {
|
||||||
|
CreateNodes,
|
||||||
|
CreateNodesContext,
|
||||||
|
} from 'nx/src/project-graph/plugins';
|
||||||
|
import type { ProjectGraph } from 'nx/src/config/project-graph';
|
||||||
|
import type { RunCommandsOptions } from 'nx/src/executors/run-commands/run-commands.impl';
|
||||||
|
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
|
||||||
|
|
||||||
|
import { minimatch } from 'minimatch';
|
||||||
|
|
||||||
|
import { forEachExecutorOptions } from '../executor-options-utils';
|
||||||
|
import { deleteMatchingProperties } from './plugin-migration-utils';
|
||||||
|
import { requireNx } from '../../../nx';
|
||||||
|
|
||||||
|
const {
|
||||||
|
glob,
|
||||||
|
readNxJson,
|
||||||
|
updateNxJson,
|
||||||
|
mergeTargetConfigurations,
|
||||||
|
updateProjectConfiguration,
|
||||||
|
readProjectConfiguration,
|
||||||
|
retrieveProjectConfigurations,
|
||||||
|
LoadedNxPlugin,
|
||||||
|
ProjectConfigurationsError,
|
||||||
|
} = requireNx();
|
||||||
|
|
||||||
|
type PluginOptionsBuilder<T> = (targetName: string) => T;
|
||||||
|
type PostTargetTransformer = (
|
||||||
|
targetConfiguration: TargetConfiguration
|
||||||
|
) => TargetConfiguration;
|
||||||
|
type SkipTargetFilter = (
|
||||||
|
targetConfiguration: TargetConfiguration
|
||||||
|
) => [boolean, string];
|
||||||
|
|
||||||
|
class ExecutorToPluginMigrator<T> {
|
||||||
|
readonly tree: Tree;
|
||||||
|
readonly #projectGraph: ProjectGraph;
|
||||||
|
readonly #executor: string;
|
||||||
|
readonly #pluginPath: string;
|
||||||
|
readonly #pluginOptionsBuilder: PluginOptionsBuilder<T>;
|
||||||
|
readonly #postTargetTransformer: PostTargetTransformer;
|
||||||
|
readonly #skipTargetFilter: SkipTargetFilter;
|
||||||
|
readonly #specificProjectToMigrate: string;
|
||||||
|
#nxJson: NxJsonConfiguration;
|
||||||
|
#targetDefaultsForExecutor: Partial<TargetConfiguration>;
|
||||||
|
#targetAndProjectsToMigrate: Map<string, Set<string>>;
|
||||||
|
#pluginToAddForTarget: Map<string, ExpandedPluginConfiguration<T>>;
|
||||||
|
#createNodes: CreateNodes<T>;
|
||||||
|
#configFiles: string[];
|
||||||
|
#createNodesResultsForTargets: Map<string, ConfigurationResult>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
tree: Tree,
|
||||||
|
projectGraph: ProjectGraph,
|
||||||
|
executor: string,
|
||||||
|
pluginPath: string,
|
||||||
|
pluginOptionsBuilder: PluginOptionsBuilder<T>,
|
||||||
|
postTargetTransformer: PostTargetTransformer,
|
||||||
|
createNodes: CreateNodes<T>,
|
||||||
|
specificProjectToMigrate?: string,
|
||||||
|
skipTargetFilter?: SkipTargetFilter
|
||||||
|
) {
|
||||||
|
this.tree = tree;
|
||||||
|
this.#projectGraph = projectGraph;
|
||||||
|
this.#executor = executor;
|
||||||
|
this.#pluginPath = pluginPath;
|
||||||
|
this.#pluginOptionsBuilder = pluginOptionsBuilder;
|
||||||
|
this.#postTargetTransformer = postTargetTransformer;
|
||||||
|
this.#createNodes = createNodes;
|
||||||
|
this.#specificProjectToMigrate = specificProjectToMigrate;
|
||||||
|
this.#skipTargetFilter = skipTargetFilter ?? ((...args) => [false, '']);
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(): Promise<void> {
|
||||||
|
await this.#init();
|
||||||
|
for (const targetName of this.#targetAndProjectsToMigrate.keys()) {
|
||||||
|
this.#migrateTarget(targetName);
|
||||||
|
}
|
||||||
|
this.#addPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #init() {
|
||||||
|
const nxJson = readNxJson(this.tree);
|
||||||
|
nxJson.plugins ??= [];
|
||||||
|
this.#nxJson = nxJson;
|
||||||
|
this.#targetAndProjectsToMigrate = new Map();
|
||||||
|
this.#pluginToAddForTarget = new Map();
|
||||||
|
this.#createNodesResultsForTargets = new Map();
|
||||||
|
|
||||||
|
this.#getTargetDefaultsForExecutor();
|
||||||
|
this.#getTargetAndProjectsToMigrate();
|
||||||
|
await this.#getCreateNodesResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
#migrateTarget(targetName: string) {
|
||||||
|
const include: string[] = [];
|
||||||
|
for (const projectName of this.#targetAndProjectsToMigrate.get(
|
||||||
|
targetName
|
||||||
|
)) {
|
||||||
|
include.push(this.#migrateProject(projectName, targetName));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#pluginToAddForTarget.set(targetName, {
|
||||||
|
plugin: this.#pluginPath,
|
||||||
|
options: this.#pluginOptionsBuilder(targetName),
|
||||||
|
include,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#migrateProject(projectName: string, targetName: string) {
|
||||||
|
const projectFromGraph = this.#projectGraph.nodes[projectName];
|
||||||
|
const projectConfig = readProjectConfiguration(this.tree, projectName);
|
||||||
|
|
||||||
|
const createdTarget = this.#getCreatedTargetForProjectRoot(
|
||||||
|
targetName,
|
||||||
|
projectFromGraph.data.root
|
||||||
|
);
|
||||||
|
let projectTarget = projectConfig.targets[targetName];
|
||||||
|
projectTarget = mergeTargetConfigurations(
|
||||||
|
projectTarget,
|
||||||
|
this.#targetDefaultsForExecutor
|
||||||
|
);
|
||||||
|
delete projectTarget.executor;
|
||||||
|
|
||||||
|
deleteMatchingProperties(projectTarget, createdTarget);
|
||||||
|
projectTarget = this.#postTargetTransformer(projectTarget);
|
||||||
|
|
||||||
|
if (
|
||||||
|
projectTarget.options &&
|
||||||
|
Object.keys(projectTarget.options).length === 0
|
||||||
|
) {
|
||||||
|
delete projectTarget.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(projectTarget).length > 0) {
|
||||||
|
projectConfig.targets[targetName] = projectTarget;
|
||||||
|
} else {
|
||||||
|
delete projectConfig.targets[targetName];
|
||||||
|
}
|
||||||
|
updateProjectConfiguration(this.tree, projectName, projectConfig);
|
||||||
|
|
||||||
|
return `${projectFromGraph.data.root}/**/*`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPlugins() {
|
||||||
|
for (const [targetName, plugin] of this.#pluginToAddForTarget.entries()) {
|
||||||
|
const pluginOptions = this.#pluginOptionsBuilder(targetName);
|
||||||
|
|
||||||
|
const existingPlugin = this.#nxJson.plugins.find(
|
||||||
|
(plugin: ExpandedPluginConfiguration<T>) => {
|
||||||
|
if (
|
||||||
|
typeof plugin === 'string' ||
|
||||||
|
plugin.plugin !== this.#pluginPath
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in plugin.options) {
|
||||||
|
if (plugin.options[key] !== pluginOptions[key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
) as ExpandedPluginConfiguration<T>;
|
||||||
|
|
||||||
|
if (existingPlugin?.include) {
|
||||||
|
for (const pluginIncludes of existingPlugin.include) {
|
||||||
|
for (const projectPath of plugin.include) {
|
||||||
|
if (!minimatch(projectPath, pluginIncludes, { dot: true })) {
|
||||||
|
existingPlugin.include.push(projectPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allConfigFilesAreIncluded = this.#configFiles.every(
|
||||||
|
(configFile) => {
|
||||||
|
for (const includePattern of existingPlugin.include) {
|
||||||
|
if (minimatch(configFile, includePattern, { dot: true })) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allConfigFilesAreIncluded) {
|
||||||
|
existingPlugin.include = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingPlugin) {
|
||||||
|
this.#nxJson.plugins.push(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNxJson(this.tree, this.#nxJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getTargetAndProjectsToMigrate() {
|
||||||
|
forEachExecutorOptions(
|
||||||
|
this.tree,
|
||||||
|
this.#executor,
|
||||||
|
(targetConfiguration, projectName, targetName, configurationName) => {
|
||||||
|
if (configurationName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.#specificProjectToMigrate &&
|
||||||
|
projectName !== this.#specificProjectToMigrate
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [skipTarget, reasonTargetWasSkipped] =
|
||||||
|
this.#skipTargetFilter(targetConfiguration);
|
||||||
|
if (skipTarget) {
|
||||||
|
const errorMsg = `${targetName} target on project "${projectName}" cannot be migrated. ${reasonTargetWasSkipped}`;
|
||||||
|
if (this.#specificProjectToMigrate) {
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
} else {
|
||||||
|
console.warn(errorMsg);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#targetAndProjectsToMigrate.has(targetName)) {
|
||||||
|
this.#targetAndProjectsToMigrate.get(targetName).add(projectName);
|
||||||
|
} else {
|
||||||
|
this.#targetAndProjectsToMigrate.set(
|
||||||
|
targetName,
|
||||||
|
new Set([projectName])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.#targetAndProjectsToMigrate.size === 0) {
|
||||||
|
const errorMsg = this.#specificProjectToMigrate
|
||||||
|
? `Project "${
|
||||||
|
this.#specificProjectToMigrate
|
||||||
|
}" does not contain any targets using the "${
|
||||||
|
this.#executor
|
||||||
|
}" executor. Please select a project that does.`
|
||||||
|
: `Could not find any targets using the "${this.#executor}" executor.`;
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#getTargetDefaultsForExecutor() {
|
||||||
|
this.#targetDefaultsForExecutor =
|
||||||
|
this.#nxJson.targetDefaults?.[this.#executor];
|
||||||
|
}
|
||||||
|
|
||||||
|
#getCreatedTargetForProjectRoot(targetName: string, projectRoot: string) {
|
||||||
|
const createdProject = Object.entries(
|
||||||
|
this.#createNodesResultsForTargets.get(targetName)?.projects ?? {}
|
||||||
|
).find(([root]) => root === projectRoot)[1];
|
||||||
|
const createdTarget: TargetConfiguration<RunCommandsOptions> =
|
||||||
|
createdProject.targets[targetName];
|
||||||
|
delete createdTarget.command;
|
||||||
|
delete createdTarget.options?.cwd;
|
||||||
|
|
||||||
|
return createdTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getCreateNodesResults() {
|
||||||
|
for (const targetName of this.#targetAndProjectsToMigrate.keys()) {
|
||||||
|
const loadedPlugin = new LoadedNxPlugin(
|
||||||
|
{
|
||||||
|
createNodes: this.#createNodes,
|
||||||
|
name: this.#pluginPath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugin: this.#pluginPath,
|
||||||
|
options: this.#pluginOptionsBuilder(targetName),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let projectConfigs: ConfigurationResult;
|
||||||
|
try {
|
||||||
|
projectConfigs = await retrieveProjectConfigurations(
|
||||||
|
[loadedPlugin],
|
||||||
|
this.tree.root,
|
||||||
|
this.#nxJson
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ProjectConfigurationsError) {
|
||||||
|
projectConfigs = e.partialProjectConfigurationsResult;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#configFiles = Array.from(projectConfigs.matchingProjectFiles);
|
||||||
|
this.#createNodesResultsForTargets.set(targetName, projectConfigs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function migrateExecutorToPlugin<T>(
|
||||||
|
tree: Tree,
|
||||||
|
projectGraph: ProjectGraph,
|
||||||
|
executor: string,
|
||||||
|
pluginPath: string,
|
||||||
|
pluginOptionsBuilder: PluginOptionsBuilder<T>,
|
||||||
|
postTargetTransformer: PostTargetTransformer,
|
||||||
|
createNodes: CreateNodes<T>,
|
||||||
|
specificProjectToMigrate?: string,
|
||||||
|
skipTargetFilter?: SkipTargetFilter
|
||||||
|
): Promise<void> {
|
||||||
|
const migrator = new ExecutorToPluginMigrator<T>(
|
||||||
|
tree,
|
||||||
|
projectGraph,
|
||||||
|
executor,
|
||||||
|
pluginPath,
|
||||||
|
pluginOptionsBuilder,
|
||||||
|
postTargetTransformer,
|
||||||
|
createNodes,
|
||||||
|
specificProjectToMigrate,
|
||||||
|
skipTargetFilter
|
||||||
|
);
|
||||||
|
await migrator.run();
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import { deleteMatchingProperties } from './plugin-migration-utils';
|
||||||
|
describe('Plugin Migration Utils', () => {
|
||||||
|
describe('deleteMatchingProperties', () => {
|
||||||
|
it('should delete properties that are identical between two different objects, leaving an empty object', () => {
|
||||||
|
// ARRANGE
|
||||||
|
const activeObject = {
|
||||||
|
foo: 1,
|
||||||
|
bar: 'myval',
|
||||||
|
baz: {
|
||||||
|
nested: {
|
||||||
|
key: 'val',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
arr: ['string', 1],
|
||||||
|
};
|
||||||
|
|
||||||
|
const comparableObject = {
|
||||||
|
foo: 1,
|
||||||
|
bar: 'myval',
|
||||||
|
baz: {
|
||||||
|
nested: {
|
||||||
|
key: 'val',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
arr: ['string', 1],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
deleteMatchingProperties(activeObject, comparableObject);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(activeObject).toMatchInlineSnapshot(`{}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete properties that are identical between two different objects, leaving an object containing only the differences', () => {
|
||||||
|
// ARRANGE
|
||||||
|
const activeObject = {
|
||||||
|
foo: 1,
|
||||||
|
bar: 'myval',
|
||||||
|
baz: {
|
||||||
|
nested: {
|
||||||
|
key: 'differentValue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
arr: ['string', 2],
|
||||||
|
};
|
||||||
|
|
||||||
|
const comparableObject = {
|
||||||
|
foo: 1,
|
||||||
|
bar: 'myval',
|
||||||
|
baz: {
|
||||||
|
nested: {
|
||||||
|
key: 'val',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
arr: ['string', 1],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
deleteMatchingProperties(activeObject, comparableObject);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(activeObject).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"arr": [
|
||||||
|
"string",
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
"baz": {
|
||||||
|
"nested": {
|
||||||
|
"key": "differentValue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Iterate through the current target in the project.json and its options comparing it to the target created by the Plugin itself
|
||||||
|
* Delete matching properties from current target.
|
||||||
|
*
|
||||||
|
* _Note: Deletes by reference_
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Run the plugin to get all the projects
|
||||||
|
* const { projects } = await createNodes[1](
|
||||||
|
* playwrightConfigPath,
|
||||||
|
* { targetName, ciTargetName: 'e2e-ci' },
|
||||||
|
* { workspaceRoot: tree.root, nxJsonConfiguration, configFiles }
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* // Find the project that matches the one that is being migrated
|
||||||
|
* const createdProject = Object.entries(projects ?? {}).find(
|
||||||
|
* ([root]) => root === projectFromGraph.data.root
|
||||||
|
* )[1];
|
||||||
|
*
|
||||||
|
* // Get the created TargetConfiguration for the target being migrated
|
||||||
|
* const createdTarget: TargetConfiguration<RunCommandsOptions> =
|
||||||
|
* createdProject.targets[targetName];
|
||||||
|
*
|
||||||
|
* // Delete specific run-commands options
|
||||||
|
* delete createdTarget.command;
|
||||||
|
* delete createdTarget.options?.cwd;
|
||||||
|
*
|
||||||
|
* // Get the TargetConfiguration for the target being migrated from project.json
|
||||||
|
* const projectConfig = readProjectConfiguration(tree, projectName);
|
||||||
|
* let targetToMigrate = projectConfig.targets[targetName];
|
||||||
|
*
|
||||||
|
* // Merge the target defaults for the executor to the target being migrated
|
||||||
|
* target = mergeTargetConfigurations(targetToMigrate, targetDefaultsForExecutor);
|
||||||
|
*
|
||||||
|
* // Delete executor and any additional options that are no longer necessary
|
||||||
|
* delete target.executor;
|
||||||
|
* delete target.options?.config;
|
||||||
|
*
|
||||||
|
* // Run deleteMatchingProperties to delete further options that match what the plugin creates
|
||||||
|
* deleteMatchingProperties(target, createdTarget);
|
||||||
|
*
|
||||||
|
* // Delete the target if it is now empty, otherwise, set it to the updated TargetConfiguration
|
||||||
|
* if (Object.keys(target).length > 0) {
|
||||||
|
* projectConfig.targets[targetName] = target;
|
||||||
|
* } else {
|
||||||
|
* delete projectConfig.targets[targetName];
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* updateProjectConfiguration(tree, projectName, projectConfig);
|
||||||
|
*
|
||||||
|
* @param targetToMigrate The target from project.json
|
||||||
|
* @param createdTarget The target created by the Plugin
|
||||||
|
*/
|
||||||
|
export function deleteMatchingProperties(
|
||||||
|
targetToMigrate: object,
|
||||||
|
createdTarget: object
|
||||||
|
): void {
|
||||||
|
for (const key in targetToMigrate) {
|
||||||
|
if (Array.isArray(targetToMigrate[key])) {
|
||||||
|
if (
|
||||||
|
targetToMigrate[key].every((v) => createdTarget[key]?.includes(v)) &&
|
||||||
|
targetToMigrate[key].length === createdTarget[key]?.length
|
||||||
|
) {
|
||||||
|
delete targetToMigrate[key];
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
typeof targetToMigrate[key] === 'object' &&
|
||||||
|
typeof createdTarget[key] === 'object'
|
||||||
|
) {
|
||||||
|
deleteMatchingProperties(targetToMigrate[key], createdTarget[key]);
|
||||||
|
} else if (targetToMigrate[key] === createdTarget[key]) {
|
||||||
|
delete targetToMigrate[key];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof targetToMigrate[key] === 'object' &&
|
||||||
|
Object.keys(targetToMigrate[key]).length === 0
|
||||||
|
) {
|
||||||
|
delete targetToMigrate[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -46,7 +46,9 @@ export async function addPlugin<PluginOptions>(
|
|||||||
nxJson.plugins ??= [];
|
nxJson.plugins ??= [];
|
||||||
if (
|
if (
|
||||||
nxJson.plugins.some((p) =>
|
nxJson.plugins.some((p) =>
|
||||||
typeof p === 'string' ? p === pluginName : p.plugin === pluginName
|
typeof p === 'string'
|
||||||
|
? p === pluginName
|
||||||
|
: p.plugin === pluginName && !p.include
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Plugin has already been added
|
// Plugin has already been added
|
||||||
|
|||||||
@ -438,9 +438,9 @@ export interface NxJsonConfiguration<T = '*' | string[]> {
|
|||||||
|
|
||||||
export type PluginConfiguration = string | ExpandedPluginConfiguration;
|
export type PluginConfiguration = string | ExpandedPluginConfiguration;
|
||||||
|
|
||||||
export type ExpandedPluginConfiguration = {
|
export type ExpandedPluginConfiguration<T = unknown> = {
|
||||||
plugin: string;
|
plugin: string;
|
||||||
options?: unknown;
|
options?: T;
|
||||||
include?: string[];
|
include?: string[];
|
||||||
exclude?: string[];
|
exclude?: string[];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export { getExecutorInformation } from './command-line/run/executor-utils';
|
|||||||
export { readNxJson as readNxJsonFromDisk } from './config/nx-json';
|
export { readNxJson as readNxJsonFromDisk } from './config/nx-json';
|
||||||
export { calculateDefaultProjectName } from './config/calculate-default-project-name';
|
export { calculateDefaultProjectName } from './config/calculate-default-project-name';
|
||||||
export { retrieveProjectConfigurationsWithAngularProjects } from './project-graph/utils/retrieve-workspace-files';
|
export { retrieveProjectConfigurationsWithAngularProjects } from './project-graph/utils/retrieve-workspace-files';
|
||||||
|
export { mergeTargetConfigurations } from './project-graph/utils/project-configuration-utils';
|
||||||
export { readProjectConfigurationsFromRootMap } from './project-graph/utils/project-configuration-utils';
|
export { readProjectConfigurationsFromRootMap } from './project-graph/utils/project-configuration-utils';
|
||||||
export { splitTarget } from './utils/split-target';
|
export { splitTarget } from './utils/split-target';
|
||||||
export { combineOptionsForExecutor } from './utils/params';
|
export { combineOptionsForExecutor } from './utils/params';
|
||||||
|
|||||||
@ -11,6 +11,11 @@
|
|||||||
"factory": "./src/generators/init/init#initGeneratorInternal",
|
"factory": "./src/generators/init/init#initGeneratorInternal",
|
||||||
"schema": "./src/generators/init/schema.json",
|
"schema": "./src/generators/init/schema.json",
|
||||||
"description": "Initializes a Playwright project in the current workspace"
|
"description": "Initializes a Playwright project in the current workspace"
|
||||||
|
},
|
||||||
|
"convert-to-inferred": {
|
||||||
|
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
|
||||||
|
"schema": "./src/generators/convert-to-inferred/schema.json",
|
||||||
|
"description": "Convert existing Playwright project(s) using `@nx/playwright:playwright` executor to use `@nx/playwright/plugin`."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,609 @@
|
|||||||
|
import {
|
||||||
|
getRelativeProjectJsonSchemaPath,
|
||||||
|
updateProjectConfiguration,
|
||||||
|
} from 'nx/src/generators/utils/project-configuration';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||||
|
import { convertToInferred } from './convert-to-inferred';
|
||||||
|
import {
|
||||||
|
addProjectConfiguration as _addProjectConfiguration,
|
||||||
|
type ExpandedPluginConfiguration,
|
||||||
|
joinPathFragments,
|
||||||
|
type ProjectConfiguration,
|
||||||
|
type ProjectGraph,
|
||||||
|
readNxJson,
|
||||||
|
readProjectConfiguration,
|
||||||
|
type Tree,
|
||||||
|
updateNxJson,
|
||||||
|
writeJson,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { TempFs } from '@nx/devkit/internal-testing-utils';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
let fs: TempFs;
|
||||||
|
|
||||||
|
let projectGraph: ProjectGraph;
|
||||||
|
jest.mock('@nx/devkit', () => ({
|
||||||
|
...jest.requireActual<any>('@nx/devkit'),
|
||||||
|
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
|
||||||
|
return projectGraph;
|
||||||
|
}),
|
||||||
|
updateProjectConfiguration: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((tree, projectName, projectConfiguration) => {
|
||||||
|
function handleEmptyTargets(
|
||||||
|
projectName: string,
|
||||||
|
projectConfiguration: ProjectConfiguration
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
projectConfiguration.targets &&
|
||||||
|
!Object.keys(projectConfiguration.targets).length
|
||||||
|
) {
|
||||||
|
// Re-order `targets` to appear after the `// target` comment.
|
||||||
|
delete projectConfiguration.targets;
|
||||||
|
projectConfiguration[
|
||||||
|
'// targets'
|
||||||
|
] = `to see all targets run: nx show project ${projectName} --web`;
|
||||||
|
projectConfiguration.targets = {};
|
||||||
|
} else {
|
||||||
|
delete projectConfiguration['// targets'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectConfigFile = joinPathFragments(
|
||||||
|
projectConfiguration.root,
|
||||||
|
'project.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tree.exists(projectConfigFile)) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot update Project ${projectName} at ${projectConfiguration.root}. It either doesn't exist yet, or may not use project.json for configuration. Use \`addProjectConfiguration()\` instead if you want to create a new project.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
handleEmptyTargets(projectName, projectConfiguration);
|
||||||
|
writeJson(tree, projectConfigFile, {
|
||||||
|
name: projectConfiguration.name ?? projectName,
|
||||||
|
$schema: getRelativeProjectJsonSchemaPath(tree, projectConfiguration),
|
||||||
|
...projectConfiguration,
|
||||||
|
root: undefined,
|
||||||
|
});
|
||||||
|
projectGraph.nodes[projectName].data = projectConfiguration;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function addProjectConfiguration(
|
||||||
|
tree: Tree,
|
||||||
|
name: string,
|
||||||
|
project: ProjectConfiguration
|
||||||
|
) {
|
||||||
|
_addProjectConfiguration(tree, name, project);
|
||||||
|
projectGraph.nodes[name] = {
|
||||||
|
name: name,
|
||||||
|
type: project.projectType === 'application' ? 'app' : 'lib',
|
||||||
|
data: {
|
||||||
|
projectType: project.projectType,
|
||||||
|
root: project.root,
|
||||||
|
targets: project.targets,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreatePlaywrightTestProjectOptions {
|
||||||
|
appName: string;
|
||||||
|
appRoot: string;
|
||||||
|
e2eTargetName: string;
|
||||||
|
outputPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultCreatePlaywrightTestProjectOptions: CreatePlaywrightTestProjectOptions =
|
||||||
|
{
|
||||||
|
appName: 'myapp-e2e',
|
||||||
|
appRoot: 'myapp-e2e',
|
||||||
|
e2eTargetName: 'e2e',
|
||||||
|
outputPath: '{workspaceRoot}/dist/.playwright/myapp-e2e',
|
||||||
|
};
|
||||||
|
|
||||||
|
function createTestProject(
|
||||||
|
tree: Tree,
|
||||||
|
opts: Partial<CreatePlaywrightTestProjectOptions> = defaultCreatePlaywrightTestProjectOptions
|
||||||
|
) {
|
||||||
|
let projectOpts = { ...defaultCreatePlaywrightTestProjectOptions, ...opts };
|
||||||
|
const project: ProjectConfiguration = {
|
||||||
|
name: projectOpts.appName,
|
||||||
|
root: projectOpts.appRoot,
|
||||||
|
projectType: 'application',
|
||||||
|
targets: {
|
||||||
|
[projectOpts.e2eTargetName]: {
|
||||||
|
executor: '@nx/playwright:playwright',
|
||||||
|
outputs: [projectOpts.outputPath],
|
||||||
|
options: {
|
||||||
|
config: `${projectOpts.appRoot}/playwright.config.ts`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const playwrightConfigContents = `import { defineConfig, devices } from '@playwright/test';
|
||||||
|
import { nxE2EPreset } from '@nx/playwright/preset';
|
||||||
|
import { workspaceRoot } from '@nx/devkit';
|
||||||
|
|
||||||
|
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
...nxE2EPreset(__filename, { testDir: './src' }),
|
||||||
|
use: {
|
||||||
|
baseURL,
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
webServer: {
|
||||||
|
command: 'npx nx serve myapp',
|
||||||
|
url: 'http://localhost:4200',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
cwd: workspaceRoot,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});`;
|
||||||
|
|
||||||
|
tree.write(
|
||||||
|
`${projectOpts.appRoot}/playwright.config.ts`,
|
||||||
|
playwrightConfigContents
|
||||||
|
);
|
||||||
|
fs.createFileSync(
|
||||||
|
`${projectOpts.appRoot}/playwright.config.ts`,
|
||||||
|
playwrightConfigContents
|
||||||
|
);
|
||||||
|
jest.doMock(
|
||||||
|
join(fs.tempDir, `${projectOpts.appRoot}/playwright.config.ts`),
|
||||||
|
() => ({
|
||||||
|
default: {
|
||||||
|
outputDir: '../dist/.playwright/myapp-e2e',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
virtual: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
addProjectConfiguration(tree, project.name, project);
|
||||||
|
fs.createFileSync(
|
||||||
|
`${projectOpts.appRoot}/project.json`,
|
||||||
|
JSON.stringify(project)
|
||||||
|
);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Playwright - Convert Executors To Plugin', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fs = new TempFs('playwright');
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
tree.root = fs.tempDir;
|
||||||
|
|
||||||
|
projectGraph = {
|
||||||
|
nodes: {},
|
||||||
|
dependencies: {},
|
||||||
|
externalNodes: {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--project', () => {
|
||||||
|
it('should setup a new Playwright plugin and only migrate one specific project', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const existingProject = createTestProject(tree, {
|
||||||
|
appRoot: 'existing',
|
||||||
|
appName: 'existing',
|
||||||
|
e2eTargetName: 'e2e',
|
||||||
|
});
|
||||||
|
const project = createTestProject(tree, {
|
||||||
|
e2eTargetName: 'test',
|
||||||
|
});
|
||||||
|
const secondProject = createTestProject(tree, {
|
||||||
|
appRoot: 'second',
|
||||||
|
appName: 'second',
|
||||||
|
e2eTargetName: 'test',
|
||||||
|
});
|
||||||
|
const thirdProject = createTestProject(tree, {
|
||||||
|
appRoot: 'third',
|
||||||
|
appName: 'third',
|
||||||
|
e2eTargetName: 'integration',
|
||||||
|
});
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
nxJson.plugins ??= [];
|
||||||
|
nxJson.plugins.push({
|
||||||
|
plugin: '@nx/playwright/plugin',
|
||||||
|
options: {
|
||||||
|
targetName: 'e2e',
|
||||||
|
ciTargetName: 'e2e-ci',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
updateNxJson(tree, nxJson);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await convertToInferred(tree, { project: 'myapp-e2e', skipFormat: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
// project.json modifications
|
||||||
|
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||||
|
const targetKeys = Object.keys(updatedProject.targets);
|
||||||
|
['test'].forEach((key) => expect(targetKeys).not.toContain(key));
|
||||||
|
|
||||||
|
// nx.json modifications
|
||||||
|
const nxJsonPlugins = readNxJson(tree).plugins;
|
||||||
|
const addedTestPlaywrightPlugin = nxJsonPlugins.find((plugin) => {
|
||||||
|
if (
|
||||||
|
typeof plugin !== 'string' &&
|
||||||
|
plugin.plugin === '@nx/playwright/plugin' &&
|
||||||
|
plugin.include?.length === 1
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(addedTestPlaywrightPlugin).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
(addedTestPlaywrightPlugin as ExpandedPluginConfiguration).include
|
||||||
|
).toEqual(['myapp-e2e/**/*']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add project to existing plugins includes', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const existingProject = createTestProject(tree, {
|
||||||
|
appRoot: 'existing',
|
||||||
|
appName: 'existing',
|
||||||
|
e2eTargetName: 'e2e',
|
||||||
|
});
|
||||||
|
const project = createTestProject(tree, {
|
||||||
|
e2eTargetName: 'e2e',
|
||||||
|
});
|
||||||
|
const secondProject = createTestProject(tree, {
|
||||||
|
appRoot: 'second',
|
||||||
|
appName: 'second',
|
||||||
|
e2eTargetName: 'e2e',
|
||||||
|
});
|
||||||
|
const thirdProject = createTestProject(tree, {
|
||||||
|
appRoot: 'third',
|
||||||
|
appName: 'third',
|
||||||
|
e2eTargetName: 'e2e',
|
||||||
|
});
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
nxJson.plugins ??= [];
|
||||||
|
nxJson.plugins.push({
|
||||||
|
plugin: '@nx/playwright/plugin',
|
||||||
|
include: ['existing/**/*'],
|
||||||
|
options: {
|
||||||
|
targetName: 'e2e',
|
||||||
|
ciTargetName: 'e2e-ci',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
updateNxJson(tree, nxJson);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await convertToInferred(tree, { project: 'myapp-e2e', skipFormat: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
// project.json modifications
|
||||||
|
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||||
|
const targetKeys = Object.keys(updatedProject.targets);
|
||||||
|
['test'].forEach((key) => expect(targetKeys).not.toContain(key));
|
||||||
|
|
||||||
|
// nx.json modifications
|
||||||
|
const nxJsonPlugins = readNxJson(tree).plugins;
|
||||||
|
const addedTestPlaywrightPlugin = nxJsonPlugins.find((plugin) => {
|
||||||
|
if (
|
||||||
|
typeof plugin !== 'string' &&
|
||||||
|
plugin.plugin === '@nx/playwright/plugin' &&
|
||||||
|
plugin.include?.length === 2
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(addedTestPlaywrightPlugin).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
(addedTestPlaywrightPlugin as ExpandedPluginConfiguration).include
|
||||||
|
).toEqual(['existing/**/*', 'myapp-e2e/**/*']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove include when all projects are included', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const existingProject = createTestProject(tree, {
|
||||||
|
appRoot: 'existing',
|
||||||
|
appName: 'existing',
|
||||||
|
e2eTargetName: 'e2e',
|
||||||
|
});
|
||||||
|
const project = createTestProject(tree, {
|
||||||
|
e2eTargetName: 'e2e',
|
||||||
|
});
|
||||||
|
const secondProject = createTestProject(tree, {
|
||||||
|
appRoot: 'second',
|
||||||
|
appName: 'second',
|
||||||
|
e2eTargetName: 'e2e',
|
||||||
|
});
|
||||||
|
const thirdProject = createTestProject(tree, {
|
||||||
|
appRoot: 'third',
|
||||||
|
appName: 'third',
|
||||||
|
e2eTargetName: 'e2e',
|
||||||
|
});
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
nxJson.plugins ??= [];
|
||||||
|
nxJson.plugins.push({
|
||||||
|
plugin: '@nx/playwright/plugin',
|
||||||
|
include: ['existing/**/*', 'second/**/*', 'third/**/*'],
|
||||||
|
options: {
|
||||||
|
targetName: 'e2e',
|
||||||
|
ciTargetName: 'e2e-ci',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
updateNxJson(tree, nxJson);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await convertToInferred(tree, { project: 'myapp-e2e', skipFormat: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
// project.json modifications
|
||||||
|
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||||
|
const targetKeys = Object.keys(updatedProject.targets);
|
||||||
|
['test'].forEach((key) => expect(targetKeys).not.toContain(key));
|
||||||
|
|
||||||
|
// nx.json modifications
|
||||||
|
const nxJsonPlugins = readNxJson(tree).plugins;
|
||||||
|
const addedTestPlaywrightPlugin = nxJsonPlugins.find((plugin) => {
|
||||||
|
if (
|
||||||
|
typeof plugin !== 'string' &&
|
||||||
|
plugin.plugin === '@nx/playwright/plugin' &&
|
||||||
|
!plugin.include
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(addedTestPlaywrightPlugin).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
(addedTestPlaywrightPlugin as ExpandedPluginConfiguration).include
|
||||||
|
).not.toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--all', () => {
|
||||||
|
it('should successfully migrate a project using Playwright executors to plugin', async () => {
|
||||||
|
const project = createTestProject(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await convertToInferred(tree, { skipFormat: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
// project.json modifications
|
||||||
|
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||||
|
const targetKeys = Object.keys(updatedProject.targets);
|
||||||
|
expect(targetKeys).not.toContain('e2e');
|
||||||
|
|
||||||
|
// nx.json modifications
|
||||||
|
const nxJsonPlugins = readNxJson(tree).plugins;
|
||||||
|
const hasPlaywrightPlugin = nxJsonPlugins.find((plugin) =>
|
||||||
|
typeof plugin === 'string'
|
||||||
|
? plugin === '@nx/playwright/plugin'
|
||||||
|
: plugin.plugin === '@nx/playwright/plugin'
|
||||||
|
);
|
||||||
|
expect(hasPlaywrightPlugin).toBeTruthy();
|
||||||
|
if (typeof hasPlaywrightPlugin !== 'string') {
|
||||||
|
[
|
||||||
|
['targetName', 'e2e'],
|
||||||
|
['ciTargetName', 'e2e-ci'],
|
||||||
|
].forEach(([targetOptionName, targetName]) => {
|
||||||
|
expect(hasPlaywrightPlugin.options[targetOptionName]).toEqual(
|
||||||
|
targetName
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should setup Playwright plugin to match projects', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const project = createTestProject(tree, {
|
||||||
|
e2eTargetName: 'test',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await convertToInferred(tree, { skipFormat: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
// project.json modifications
|
||||||
|
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||||
|
const targetKeys = Object.keys(updatedProject.targets);
|
||||||
|
['test'].forEach((key) => expect(targetKeys).not.toContain(key));
|
||||||
|
|
||||||
|
// nx.json modifications
|
||||||
|
const nxJsonPlugins = readNxJson(tree).plugins;
|
||||||
|
const hasPlaywrightPlugin = nxJsonPlugins.find((plugin) =>
|
||||||
|
typeof plugin === 'string'
|
||||||
|
? plugin === '@nx/playwright/plugin'
|
||||||
|
: plugin.plugin === '@nx/playwright/plugin'
|
||||||
|
);
|
||||||
|
expect(hasPlaywrightPlugin).toBeTruthy();
|
||||||
|
if (typeof hasPlaywrightPlugin !== 'string') {
|
||||||
|
[
|
||||||
|
['targetName', 'test'],
|
||||||
|
['ciTargetName', 'e2e-ci'],
|
||||||
|
].forEach(([targetOptionName, targetName]) => {
|
||||||
|
expect(hasPlaywrightPlugin.options[targetOptionName]).toEqual(
|
||||||
|
targetName
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should setup a new Playwright plugin to match only projects migrated', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const existingProject = createTestProject(tree, {
|
||||||
|
appRoot: 'existing',
|
||||||
|
appName: 'existing',
|
||||||
|
e2eTargetName: 'e2e',
|
||||||
|
});
|
||||||
|
const project = createTestProject(tree, {
|
||||||
|
e2eTargetName: 'test',
|
||||||
|
});
|
||||||
|
const secondProject = createTestProject(tree, {
|
||||||
|
appRoot: 'second',
|
||||||
|
appName: 'second',
|
||||||
|
e2eTargetName: 'test',
|
||||||
|
});
|
||||||
|
const thirdProject = createTestProject(tree, {
|
||||||
|
appRoot: 'third',
|
||||||
|
appName: 'third',
|
||||||
|
e2eTargetName: 'integration',
|
||||||
|
});
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
nxJson.plugins ??= [];
|
||||||
|
nxJson.plugins.push({
|
||||||
|
plugin: '@nx/playwright/plugin',
|
||||||
|
options: {
|
||||||
|
targetName: 'e2e',
|
||||||
|
ciTargetName: 'e2e-ci',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
updateNxJson(tree, nxJson);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await convertToInferred(tree, { skipFormat: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
// project.json modifications
|
||||||
|
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||||
|
const targetKeys = Object.keys(updatedProject.targets);
|
||||||
|
['test'].forEach((key) => expect(targetKeys).not.toContain(key));
|
||||||
|
|
||||||
|
// nx.json modifications
|
||||||
|
const nxJsonPlugins = readNxJson(tree).plugins;
|
||||||
|
const addedTestPlaywrightPlugin = nxJsonPlugins.find((plugin) => {
|
||||||
|
if (
|
||||||
|
typeof plugin !== 'string' &&
|
||||||
|
plugin.plugin === '@nx/playwright/plugin' &&
|
||||||
|
plugin.include?.length === 2
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(addedTestPlaywrightPlugin).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
(addedTestPlaywrightPlugin as ExpandedPluginConfiguration).include
|
||||||
|
).toEqual(['myapp-e2e/**/*', 'second/**/*']);
|
||||||
|
|
||||||
|
const addedIntegrationPlaywrightPlugin = nxJsonPlugins.find((plugin) => {
|
||||||
|
if (
|
||||||
|
typeof plugin !== 'string' &&
|
||||||
|
plugin.plugin === '@nx/playwright/plugin' &&
|
||||||
|
plugin.include?.length === 1
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(addedIntegrationPlaywrightPlugin).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
(addedIntegrationPlaywrightPlugin as ExpandedPluginConfiguration)
|
||||||
|
.include
|
||||||
|
).toEqual(['third/**/*']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep Playwright options in project.json', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const project = createTestProject(tree);
|
||||||
|
project.targets.e2e.options.globalTimeout = 100000;
|
||||||
|
updateProjectConfiguration(tree, project.name, project);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await convertToInferred(tree, { skipFormat: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
// project.json modifications
|
||||||
|
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||||
|
expect(updatedProject.targets.e2e).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"global-timeout": 100000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// nx.json modifications
|
||||||
|
const nxJsonPlugins = readNxJson(tree).plugins;
|
||||||
|
const hasPlaywrightPlugin = nxJsonPlugins.find((plugin) =>
|
||||||
|
typeof plugin === 'string'
|
||||||
|
? plugin === '@nx/playwright/plugin'
|
||||||
|
: plugin.plugin === '@nx/playwright/plugin'
|
||||||
|
);
|
||||||
|
expect(hasPlaywrightPlugin).toBeTruthy();
|
||||||
|
if (typeof hasPlaywrightPlugin !== 'string') {
|
||||||
|
[
|
||||||
|
['targetName', 'e2e'],
|
||||||
|
['ciTargetName', 'e2e-ci'],
|
||||||
|
].forEach(([targetOptionName, targetName]) => {
|
||||||
|
expect(hasPlaywrightPlugin.options[targetOptionName]).toEqual(
|
||||||
|
targetName
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add Playwright options found in targetDefaults for the executor to the project.json', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
nxJson.targetDefaults ??= {};
|
||||||
|
nxJson.targetDefaults['@nx/playwright:playwright'] = {
|
||||||
|
options: {
|
||||||
|
globalTimeout: 100000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
updateNxJson(tree, nxJson);
|
||||||
|
const project = createTestProject(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await convertToInferred(tree, { skipFormat: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
// project.json modifications
|
||||||
|
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||||
|
expect(updatedProject.targets.e2e).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"global-timeout": 100000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// nx.json modifications
|
||||||
|
const nxJsonPlugins = readNxJson(tree).plugins;
|
||||||
|
const hasPlaywrightPlugin = nxJsonPlugins.find((plugin) =>
|
||||||
|
typeof plugin === 'string'
|
||||||
|
? plugin === '@nx/playwright/plugin'
|
||||||
|
: plugin.plugin === '@nx/playwright/plugin'
|
||||||
|
);
|
||||||
|
expect(hasPlaywrightPlugin).toBeTruthy();
|
||||||
|
if (typeof hasPlaywrightPlugin !== 'string') {
|
||||||
|
[
|
||||||
|
['targetName', 'e2e'],
|
||||||
|
['ciTargetName', 'e2e-ci'],
|
||||||
|
].forEach(([targetOptionName, targetName]) => {
|
||||||
|
expect(hasPlaywrightPlugin.options[targetOptionName]).toEqual(
|
||||||
|
targetName
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
createProjectGraphAsync,
|
||||||
|
formatFiles,
|
||||||
|
names,
|
||||||
|
type TargetConfiguration,
|
||||||
|
type Tree,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { createNodes, PlaywrightPluginOptions } from '../../plugins/plugin';
|
||||||
|
import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator';
|
||||||
|
|
||||||
|
interface Schema {
|
||||||
|
project?: string;
|
||||||
|
all?: boolean;
|
||||||
|
skipFormat?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function convertToInferred(tree: Tree, options: Schema) {
|
||||||
|
const projectGraph = await createProjectGraphAsync();
|
||||||
|
await migrateExecutorToPlugin<PlaywrightPluginOptions>(
|
||||||
|
tree,
|
||||||
|
projectGraph,
|
||||||
|
'@nx/playwright:playwright',
|
||||||
|
'@nx/playwright/plugin',
|
||||||
|
(targetName) => ({ targetName, ciTargetName: 'e2e-ci' }),
|
||||||
|
postTargetTransformer,
|
||||||
|
createNodes,
|
||||||
|
options.project
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!options.skipFormat) {
|
||||||
|
await formatFiles(tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function postTargetTransformer(
|
||||||
|
target: TargetConfiguration
|
||||||
|
): TargetConfiguration {
|
||||||
|
if (target.options) {
|
||||||
|
if (target.options?.config) {
|
||||||
|
delete target.options.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(target.options)) {
|
||||||
|
const newKeyName = names(key).fileName;
|
||||||
|
delete target.options[key];
|
||||||
|
target.options[newKeyName] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default convertToInferred;
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/schema",
|
||||||
|
"$id": "NxPlaywrightConvertToInferred",
|
||||||
|
"description": "Convert existing Playwright project(s) using `@nx/playwright:playwright` executor to use `@nx/playwright/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
|
||||||
|
"title": "Convert Playwright project from executor to plugin",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The project to convert from using the `@nx/playwright:playwright` executor to use `@nx/playwright/plugin`.",
|
||||||
|
"x-priority": "important"
|
||||||
|
},
|
||||||
|
"skipFormat": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to format files at the end of the migration.",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user