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
|
||||
|
||||
@ -8,5 +14,5 @@
|
||||
| :--------- | :--------- |
|
||||
| `exclude?` | `string`[] |
|
||||
| `include?` | `string`[] |
|
||||
| `options?` | `unknown` |
|
||||
| `options?` | `T` |
|
||||
| `plugin` | `string` |
|
||||
|
||||
@ -8575,6 +8575,14 @@
|
||||
"children": [],
|
||||
"isExternal": 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,
|
||||
|
||||
@ -2021,6 +2021,15 @@
|
||||
"originalFilePath": "/packages/playwright/src/generators/init/schema.json",
|
||||
"path": "/nx-api/playwright/generators/init",
|
||||
"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"
|
||||
|
||||
@ -1997,6 +1997,15 @@
|
||||
"originalFilePath": "/packages/playwright/src/generators/init/schema.json",
|
||||
"path": "playwright/generators/init",
|
||||
"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",
|
||||
|
||||
@ -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)
|
||||
- [configuration](/nx-api/playwright/generators/configuration)
|
||||
- [init](/nx-api/playwright/generators/init)
|
||||
- [convert-to-inferred](/nx-api/playwright/generators/convert-to-inferred)
|
||||
- [plugin](/nx-api/plugin)
|
||||
- [documents](/nx-api/plugin/documents)
|
||||
- [Overview](/nx-api/plugin/documents/overview)
|
||||
|
||||
@ -34,7 +34,8 @@
|
||||
"tmp": "~0.2.1",
|
||||
"tslib": "^2.3.0",
|
||||
"semver": "^7.5.3",
|
||||
"yargs-parser": "21.1.1"
|
||||
"yargs-parser": "21.1.1",
|
||||
"minimatch": "9.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"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 ??= [];
|
||||
if (
|
||||
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
|
||||
|
||||
@ -438,9 +438,9 @@ export interface NxJsonConfiguration<T = '*' | string[]> {
|
||||
|
||||
export type PluginConfiguration = string | ExpandedPluginConfiguration;
|
||||
|
||||
export type ExpandedPluginConfiguration = {
|
||||
export type ExpandedPluginConfiguration<T = unknown> = {
|
||||
plugin: string;
|
||||
options?: unknown;
|
||||
options?: T;
|
||||
include?: string[];
|
||||
exclude?: string[];
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ export { getExecutorInformation } from './command-line/run/executor-utils';
|
||||
export { readNxJson as readNxJsonFromDisk } from './config/nx-json';
|
||||
export { calculateDefaultProjectName } from './config/calculate-default-project-name';
|
||||
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 { splitTarget } from './utils/split-target';
|
||||
export { combineOptionsForExecutor } from './utils/params';
|
||||
|
||||
@ -11,6 +11,11 @@
|
||||
"factory": "./src/generators/init/init#initGeneratorInternal",
|
||||
"schema": "./src/generators/init/schema.json",
|
||||
"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