chore(core): copy isNxExecutor out of Workspaces class (#17996)

This commit is contained in:
Emily Xiong 2023-07-14 01:55:42 -04:00 committed by GitHub
parent 6ccbbbc98f
commit 1c6a359130
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 283 additions and 227 deletions

View File

@ -1,13 +1,9 @@
import type { Schema } from './schema';
import {
logger,
readCachedProjectGraph,
workspaceRoot,
Workspaces,
} from '@nx/devkit';
import { logger, readCachedProjectGraph, workspaceRoot } from '@nx/devkit';
import { scheduleTarget } from 'nx/src/adapter/ngcli-adapter';
import { executeWebpackDevServerBuilder } from '../webpack-dev-server/webpack-dev-server.impl';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
import { getExecutorInformation } from 'nx/src/command-line/run/executor-utils';
import {
getDynamicRemotes,
getStaticRemotes,
@ -25,7 +21,6 @@ export function executeModuleFederationDevServerBuilder(
const projectGraph = readCachedProjectGraph();
const { projects: workspaceProjects } =
readProjectsConfigurationFromProjectGraph(projectGraph);
const ws = new Workspaces(workspaceRoot);
const project = workspaceProjects[context.target.project];
let pathToManifestFile = join(
@ -101,7 +96,11 @@ export function executeModuleFederationDevServerBuilder(
if (options.verbose) {
const [collection, executor] =
workspaceProjects[remote].targets[target].executor.split(':');
const { schema } = ws.readExecutor(collection, executor);
const { schema } = getExecutorInformation(
collection,
executor,
workspaceRoot
);
if (schema.additionalProperties || 'verbose' in schema.properties) {
runOptions.verbose = options.verbose;

View File

@ -1,6 +1,7 @@
import type { Schema } from './schema';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
import { readCachedProjectGraph, workspaceRoot, Workspaces } from '@nx/devkit';
import { getExecutorInformation } from 'nx/src/command-line/run/executor-utils';
import { readCachedProjectGraph, workspaceRoot } from '@nx/devkit';
import {
getDynamicRemotes,
getStaticRemotes,
@ -21,7 +22,6 @@ export function executeModuleFederationDevSSRBuilder(
const projectGraph = readCachedProjectGraph();
const { projects: workspaceProjects } =
readProjectsConfigurationFromProjectGraph(projectGraph);
const ws = new Workspaces(workspaceRoot);
const project = workspaceProjects[context.target.project];
let pathToManifestFile = join(
@ -90,7 +90,11 @@ export function executeModuleFederationDevSSRBuilder(
if (options.verbose) {
const [collection, executor] =
workspaceProjects[remote].targets[target].executor.split(':');
const { schema } = ws.readExecutor(collection, executor);
const { schema } = getExecutorInformation(
collection,
executor,
workspaceRoot
);
if (schema.additionalProperties || 'verbose' in schema.properties) {
runOptions.verbose = options.verbose;

View File

@ -1,5 +1,6 @@
import { getTempTailwindPath } from '../../utils/ct-helpers';
import { ExecutorContext, stripIndents } from '@nx/devkit';
import * as executorUtils from 'nx/src/command-line/run/executor-utils';
import * as path from 'path';
import { installedCypressVersion } from '../../utils/cypress-version';
import cypressExecutor, { CypressExecutorOptions } from './cypress.impl';
@ -43,9 +44,17 @@ describe('Cypress builder', () => {
},
},
} as any;
(devkit as any).readTargetOptions = jest.fn().mockReturnValue({
jest.spyOn(devkit, 'readTargetOptions').mockReturnValue({
watch: true,
});
jest.spyOn(executorUtils, 'getExecutorInformation').mockReturnValue({
schema: { properties: {} },
hasherFactory: jest.fn(),
implementationFactory: jest.fn(),
batchImplementationFactory: jest.fn(),
isNgCompat: true,
isNxExecutor: true,
});
let runExecutor: any;
let mockGetTailwindPath: jest.Mock<ReturnType<typeof getTempTailwindPath>> =
getTempTailwindPath as any;
@ -57,9 +66,6 @@ describe('Cypress builder', () => {
baseUrl: 'http://localhost:4200',
},
]);
(devkit as any).Workspaces = jest.fn().mockReturnValue({
readExecutor: () => ({ schema: { properties: {} } }),
});
(devkit as any).stripIndents = (s) => s;
(devkit as any).parseTargetString = (s) => {
const [project, target, configuration] = s.split(':');

View File

@ -5,11 +5,11 @@ import {
readTargetOptions,
runExecutor,
stripIndents,
Workspaces,
Target,
targetToTargetString,
output,
} from '@nx/devkit';
import { getExecutorInformation } from 'nx/src/command-line/run/executor-utils';
import 'dotenv/config';
import { existsSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
import { basename, dirname, join } from 'path';
@ -421,9 +421,12 @@ ${e.message || e}`);
context.projectsConfigurations?.projects?.[target.project];
const targetConfig = projectConfig.targets[target.target];
const workspace = new Workspaces(context.root);
const [collection, executor] = targetConfig.executor.split(':');
const { schema } = workspace.readExecutor(collection, executor);
const { schema } = getExecutorInformation(
collection,
executor,
context.root
);
// NOTE: schema won't have a default since readTargetOptions would have
// already set that and this check wouldn't need to be made

View File

@ -4,7 +4,7 @@ import type { ExecutorContext } from 'nx/src/config/misc-interfaces';
import { combineOptionsForExecutor } from 'nx/src/utils/params';
import { requireNx } from '../../nx';
const { Workspaces } = requireNx();
const { Workspaces, getExecutorInformation } = requireNx();
/**
* Reads and combines options for a given target.
@ -22,7 +22,11 @@ export function readTargetOptions<T = any>(
const ws = new Workspaces(context.root);
const [nodeModule, executorName] = targetConfiguration.executor.split(':');
const { schema } = ws.readExecutor(nodeModule, executorName);
const { schema } = getExecutorInformation(
nodeModule,
executorName,
context.root
);
const defaultProject = ws.calculateDefaultProjectName(
context.cwd,

View File

@ -46,7 +46,8 @@ import {
toNewFormat,
toOldFormat,
} from './angular-json';
import { normalizeExecutorSchema, Workspaces } from '../config/workspaces';
import { normalizeExecutorSchema } from '../command-line/run/executor-utils';
import { Workspaces } from '../config/workspaces';
import {
CustomHasher,
Executor,
@ -55,6 +56,11 @@ import {
TaskGraphExecutor,
} from '../config/misc-interfaces';
import { readPluginPackageJson } from '../utils/nx-plugin';
import {
getImplementationFactory,
resolveImplementation,
resolveSchema,
} from '../config/schema-utils';
export async function scheduleTarget(
root: string,
@ -106,7 +112,7 @@ export async function scheduleTarget(
readJsonFile<ExecutorsJson>(executorsFilePath).builders[builderName]
.description,
optionSchema: builderInfo.schema,
import: this.workspaces['resolveImplementation'].bind(this.workspaces)(
import: resolveImplementation(
executorConfig.implementation,
dirname(executorsFilePath)
),
@ -153,9 +159,7 @@ export async function scheduleTarget(
const { executorsFilePath, executorConfig, isNgCompat } =
this.readExecutorsJson(nodeModule, executor);
const executorsDir = dirname(executorsFilePath);
const schemaPath = this.workspaces['resolveSchema'].bind(
this.workspaces
)(executorConfig.schema, executorsDir);
const schemaPath = resolveSchema(executorConfig.schema, executorsDir);
const schema = normalizeExecutorSchema(readJsonFile(schemaPath));
const implementationFactory = this.getImplementationFactory<Executor>(
@ -195,10 +199,7 @@ export async function scheduleTarget(
implementation: string,
executorsDir: string
): () => T {
return this.workspaces['getImplementationFactory'].bind(this.workspaces)(
implementation,
executorsDir
);
return getImplementationFactory(implementation, executorsDir);
}
}

View File

@ -0,0 +1,128 @@
import { dirname, join } from 'path';
import { readPluginPackageJson } from '../../utils/nx-plugin';
import {
CustomHasher,
Executor,
ExecutorConfig,
ExecutorsJson,
TaskGraphExecutor,
} from '../../config/misc-interfaces';
import { readJsonFile } from '../../utils/fileutils';
import {
getImplementationFactory,
resolveSchema,
} from '../../config/schema-utils';
import { getNxRequirePaths } from '../../utils/installation-directory';
export function normalizeExecutorSchema(
schema: Partial<ExecutorConfig['schema']>
): ExecutorConfig['schema'] {
const version = (schema.version ??= 1);
return {
version,
outputCapture:
schema.outputCapture ?? version < 2 ? 'direct-nodejs' : 'pipe',
properties:
!schema.properties || typeof schema.properties !== 'object'
? {}
: schema.properties,
...schema,
};
}
export function getExecutorInformation(
nodeModule: string,
executor: string,
root: string
): ExecutorConfig & { isNgCompat: boolean; isNxExecutor: boolean } {
try {
const { executorsFilePath, executorConfig, isNgCompat } = readExecutorsJson(
nodeModule,
executor,
root
);
const executorsDir = dirname(executorsFilePath);
const schemaPath = resolveSchema(executorConfig.schema, executorsDir);
const schema = normalizeExecutorSchema(readJsonFile(schemaPath));
const implementationFactory = getImplementationFactory<Executor>(
executorConfig.implementation,
executorsDir
);
const batchImplementationFactory = executorConfig.batchImplementation
? getImplementationFactory<TaskGraphExecutor>(
executorConfig.batchImplementation,
executorsDir
)
: null;
const hasherFactory = executorConfig.hasher
? getImplementationFactory<CustomHasher>(
executorConfig.hasher,
executorsDir
)
: null;
return {
schema,
implementationFactory,
batchImplementationFactory,
hasherFactory,
isNgCompat,
isNxExecutor: !isNgCompat,
};
} catch (e) {
throw new Error(
`Unable to resolve ${nodeModule}:${executor}.\n${e.message}`
);
}
}
function readExecutorsJson(
nodeModule: string,
executor: string,
root: string
): {
executorsFilePath: string;
executorConfig: {
implementation: string;
batchImplementation?: string;
schema: string;
hasher?: string;
};
isNgCompat: boolean;
} {
const { json: packageJson, path: packageJsonPath } = readPluginPackageJson(
nodeModule,
root
? [root, __dirname, process.cwd(), ...getNxRequirePaths()]
: [__dirname, process.cwd(), ...getNxRequirePaths()]
);
const executorsFile = packageJson.executors ?? packageJson.builders;
if (!executorsFile) {
throw new Error(
`The "${nodeModule}" package does not support Nx executors.`
);
}
const executorsFilePath = require.resolve(
join(dirname(packageJsonPath), executorsFile)
);
const executorsJson = readJsonFile<ExecutorsJson>(executorsFilePath);
const executorConfig: {
implementation: string;
batchImplementation?: string;
schema: string;
hasher?: string;
} = executorsJson.executors?.[executor] || executorsJson.builders?.[executor];
if (!executorConfig) {
throw new Error(
`Cannot find executor '${executor}' in ${executorsFilePath}.`
);
}
const isNgCompat = !executorsJson.executors?.[executor];
return { executorsFilePath, executorConfig, isNgCompat };
}

View File

@ -32,6 +32,7 @@ import {
getLastValueFromAsyncIterableIterator,
isAsyncIterator,
} from '../../utils/async-iterator';
import { getExecutorInformation } from './executor-utils';
export interface Target {
project: string;
@ -131,9 +132,10 @@ async function parseExecutorAndTarget(
}
const [nodeModule, executor] = targetConfig.executor.split(':');
const { schema, implementationFactory } = ws.readExecutor(
const { schema, implementationFactory } = getExecutorInformation(
nodeModule,
executor
executor,
root
);
return { executor, implementationFactory, nodeModule, schema, targetConfig };
@ -195,7 +197,7 @@ async function runExecutorInternal<T extends { success: boolean }>(
isVerbose
);
if (ws.isNxExecutor(nodeModule, executor)) {
if (getExecutorInformation(nodeModule, executor, root).isNxExecutor) {
const implementation = implementationFactory() as Executor<any>;
const r = implementation(combinedOptions, {
root,

View File

@ -0,0 +1,73 @@
import { existsSync } from 'fs';
import { extname, join } from 'path';
import { registerPluginTSTranspiler } from '../utils/nx-plugin';
/**
* This function is used to get the implementation factory of an executor or generator.
* @param implementation path to the implementation
* @param directory path to the directory
* @returns a function that returns the implementation
*/
export function getImplementationFactory<T>(
implementation: string,
directory: string
): () => T {
const [implementationModulePath, implementationExportName] =
implementation.split('#');
return () => {
const modulePath = resolveImplementation(
implementationModulePath,
directory
);
if (extname(modulePath) === '.ts') {
registerPluginTSTranspiler();
}
const module = require(modulePath);
return implementationExportName
? module[implementationExportName]
: module.default ?? module;
};
}
/**
* This function is used to resolve the implementation of an executor or generator.
* @param implementationModulePath
* @param directory
* @returns path to the implementation
*/
export function resolveImplementation(
implementationModulePath: string,
directory: string
): string {
const validImplementations = ['', '.js', '.ts'].map(
(x) => implementationModulePath + x
);
for (const maybeImplementation of validImplementations) {
const maybeImplementationPath = join(directory, maybeImplementation);
if (existsSync(maybeImplementationPath)) {
return maybeImplementationPath;
}
try {
return require.resolve(maybeImplementation, {
paths: [directory],
});
} catch {}
}
throw new Error(
`Could not resolve "${implementationModulePath}" from "${directory}".`
);
}
export function resolveSchema(schemaPath: string, directory: string): string {
const maybeSchemaPath = join(directory, schemaPath);
if (existsSync(maybeSchemaPath)) {
return maybeSchemaPath;
}
return require.resolve(schemaPath, {
paths: [directory],
});
}

View File

@ -1,7 +1,7 @@
import { sync as globSync } from 'fast-glob';
import { existsSync, readFileSync } from 'fs';
import { existsSync } from 'fs';
import * as path from 'path';
import { basename, dirname, extname, join } from 'path';
import { basename, dirname, join } from 'path';
import { performance } from 'perf_hooks';
import { workspaceRoot } from '../utils/workspace-root';
import { readJsonFile, readYamlFile } from '../utils/fileutils';
@ -10,7 +10,6 @@ import {
loadNxPlugins,
loadNxPluginsSync,
readPluginPackageJson,
registerPluginTSTranspiler,
} from '../utils/nx-plugin';
import type { NxJsonConfiguration, TargetDefaults } from './nx-json';
@ -20,14 +19,9 @@ import {
TargetConfiguration,
} from './workspace-json-project-json';
import {
CustomHasher,
Executor,
ExecutorConfig,
ExecutorsJson,
Generator,
GeneratorsJson,
GeneratorsJsonEntry,
TaskGraphExecutor,
} from './misc-interfaces';
import { PackageJson } from '../utils/package-json';
import { output } from '../utils/output';
@ -42,6 +36,7 @@ import {
findProjectForPath,
normalizeProjectRoot,
} from '../project-graph/utils/find-project-for-path';
import { getImplementationFactory, resolveSchema } from './schema-utils';
export class Workspaces {
private cachedProjectsConfig: ProjectsConfigurations;
@ -169,61 +164,10 @@ export class Workspaces {
return projects;
}
isNxExecutor(nodeModule: string, executor: string) {
return !this.readExecutor(nodeModule, executor).isNgCompat;
}
isNxGenerator(collectionName: string, generatorName: string) {
return !this.readGenerator(collectionName, generatorName).isNgCompat;
}
readExecutor(
nodeModule: string,
executor: string
): ExecutorConfig & { isNgCompat: boolean } {
try {
const { executorsFilePath, executorConfig, isNgCompat } =
this.readExecutorsJson(nodeModule, executor);
const executorsDir = path.dirname(executorsFilePath);
const schemaPath = this.resolveSchema(
executorConfig.schema,
executorsDir
);
const schema = normalizeExecutorSchema(readJsonFile(schemaPath));
const implementationFactory = this.getImplementationFactory<Executor>(
executorConfig.implementation,
executorsDir
);
const batchImplementationFactory = executorConfig.batchImplementation
? this.getImplementationFactory<TaskGraphExecutor>(
executorConfig.batchImplementation,
executorsDir
)
: null;
const hasherFactory = executorConfig.hasher
? this.getImplementationFactory<CustomHasher>(
executorConfig.hasher,
executorsDir
)
: null;
return {
schema,
implementationFactory,
batchImplementationFactory,
hasherFactory,
isNgCompat,
};
} catch (e) {
throw new Error(
`Unable to resolve ${nodeModule}:${executor}.\n${e.message}`
);
}
}
readGenerator(
collectionName: string,
generatorName: string
@ -251,17 +195,14 @@ export class Workspaces {
generatorsJson.generators?.[normalizedGeneratorName] ||
generatorsJson.schematics?.[normalizedGeneratorName];
const isNgCompat = !generatorsJson.generators?.[normalizedGeneratorName];
const schemaPath = this.resolveSchema(
generatorConfig.schema,
generatorsDir
);
const schemaPath = resolveSchema(generatorConfig.schema, generatorsDir);
const schema = readJsonFile(schemaPath);
if (!schema.properties || typeof schema.properties !== 'object') {
schema.properties = {};
}
generatorConfig.implementation =
generatorConfig.implementation || generatorConfig.factory;
const implementationFactory = this.getImplementationFactory<Generator>(
const implementationFactory = getImplementationFactory<Generator>(
generatorConfig.implementation,
generatorsDir
);
@ -322,97 +263,6 @@ export class Workspaces {
}
}
private getImplementationFactory<T>(
implementation: string,
directory: string
): () => T {
const [implementationModulePath, implementationExportName] =
implementation.split('#');
return () => {
const modulePath = this.resolveImplementation(
implementationModulePath,
directory
);
if (extname(modulePath) === '.ts') {
registerPluginTSTranspiler();
}
const module = require(modulePath);
return implementationExportName
? module[implementationExportName]
: module.default ?? module;
};
}
private resolveSchema(schemaPath: string, directory: string): string {
const maybeSchemaPath = join(directory, schemaPath);
if (existsSync(maybeSchemaPath)) {
return maybeSchemaPath;
}
return require.resolve(schemaPath, {
paths: [directory],
});
}
private resolveImplementation(
implementationModulePath: string,
directory: string
): string {
const validImplementations = ['', '.js', '.ts'].map(
(x) => implementationModulePath + x
);
for (const maybeImplementation of validImplementations) {
const maybeImplementationPath = join(directory, maybeImplementation);
if (existsSync(maybeImplementationPath)) {
return maybeImplementationPath;
}
try {
return require.resolve(maybeImplementation, {
paths: [directory],
});
} catch {}
}
throw new Error(
`Could not resolve "${implementationModulePath}" from "${directory}".`
);
}
private readExecutorsJson(nodeModule: string, executor: string) {
const { json: packageJson, path: packageJsonPath } = readPluginPackageJson(
nodeModule,
this.resolvePaths()
);
const executorsFile = packageJson.executors ?? packageJson.builders;
if (!executorsFile) {
throw new Error(
`The "${nodeModule}" package does not support Nx executors.`
);
}
const executorsFilePath = require.resolve(
path.join(path.dirname(packageJsonPath), executorsFile)
);
const executorsJson = readJsonFile<ExecutorsJson>(executorsFilePath);
const executorConfig: {
implementation: string;
batchImplementation?: string;
schema: string;
hasher?: string;
} =
executorsJson.executors?.[executor] || executorsJson.builders?.[executor];
if (!executorConfig) {
throw new Error(
`Cannot find executor '${executor}' in ${executorsFilePath}.`
);
}
const isNgCompat = !executorsJson.executors?.[executor];
return { executorsFilePath, executorConfig, isNgCompat };
}
private readGeneratorsJson(
collectionName: string,
generator: string
@ -485,22 +335,6 @@ function findMatchingProjectInCwd(
return matchingProject;
}
export function normalizeExecutorSchema(
schema: Partial<ExecutorConfig['schema']>
): ExecutorConfig['schema'] {
const version = (schema.version ??= 1);
return {
version,
outputCapture:
schema.outputCapture ?? version < 2 ? 'direct-nodejs' : 'pipe',
properties:
!schema.properties || typeof schema.properties !== 'object'
? {}
: schema.properties,
...schema,
};
}
function findFullGeneratorName(
name: string,
generators: {

View File

@ -4,3 +4,4 @@
* These may not be available in certain version of Nx, so be sure to check them first.
*/
export { createTempNpmDirectory } from './utils/package-manager';
export { getExecutorInformation } from './command-line/run/executor-utils';

View File

@ -5,7 +5,6 @@ import {
CompleteTaskMessage,
BatchResults,
} from './batch-messages';
import { Workspaces } from '../../config/workspaces';
import { workspaceRoot } from '../../utils/workspace-root';
import { combineOptionsForExecutor } from '../../utils/params';
import { TaskGraph } from '../../config/task-graph';
@ -16,11 +15,11 @@ import {
} from '../../project-graph/project-graph';
import { readNxJson } from '../../config/configuration';
import { isAsyncIterator } from '../../utils/async-iterator';
import { getExecutorInformation } from '../../command-line/run/executor-utils';
function getBatchExecutor(executorName: string) {
const workspace = new Workspaces(workspaceRoot);
const [nodeModule, exportName] = executorName.split(':');
return workspace.readExecutor(nodeModule, exportName);
return getExecutorInformation(nodeModule, exportName, workspaceRoot);
}
async function runTasks(

View File

@ -3,6 +3,7 @@ import { Workspaces } from '../config/workspaces';
import { removeTasksFromTaskGraph } from './utils';
import { Task, TaskGraph } from '../config/task-graph';
import { DependencyType, ProjectGraph } from '../config/project-graph';
import * as executorUtils from '../command-line/run/executor-utils';
function createMockTask(id: string): Task {
const [project, target] = id.split(':');
@ -43,20 +44,20 @@ describe('TasksSchedule', () => {
roots: ['lib1:build', 'app2:build'],
};
const workspace: Partial<Workspaces> = {
readExecutor() {
return {
schema: {
version: 2,
properties: {},
},
implementationFactory: jest.fn(),
batchImplementationFactory: jest.fn(),
} as any;
},
readNxJson() {
return {};
},
};
jest.spyOn(executorUtils, 'getExecutorInformation').mockReturnValue({
schema: {
version: 2,
properties: {},
},
implementationFactory: jest.fn(),
batchImplementationFactory: jest.fn(),
isNgCompat: true,
isNxExecutor: true,
});
const projectGraph: ProjectGraph = {
nodes: {
@ -269,20 +270,20 @@ describe('TasksSchedule', () => {
roots: ['app1:test', 'app2:test', 'lib1:test'],
};
const workspace: Partial<Workspaces> = {
readExecutor() {
return {
schema: {
version: 2,
properties: {},
},
implementationFactory: jest.fn(),
batchImplementationFactory: jest.fn(),
} as any;
},
readNxJson() {
return {};
},
};
jest.spyOn(executorUtils, 'getExecutorInformation').mockReturnValue({
schema: {
version: 2,
properties: {},
},
implementationFactory: jest.fn(),
batchImplementationFactory: jest.fn(),
isNgCompat: true,
isNxExecutor: true,
});
const projectGraph: ProjectGraph = {
nodes: {

View File

@ -16,6 +16,7 @@ import { joinPathFragments } from '../utils/path';
import { isRelativePath } from '../utils/fileutils';
import { serializeOverridesIntoCommandLine } from '../utils/serialize-overrides-into-command-line';
import { splitByColons, splitTarget } from '../utils/split-target';
import { getExecutorInformation } from '../command-line/run/executor-utils';
export function getCommandAsString(execCommand: string, task: Task) {
const args = getPrintableCommandArgsForTask(task);
@ -250,7 +251,7 @@ export async function getExecutorForTask(
const executor = await getExecutorNameForTask(task, projectGraph);
const [nodeModule, executorName] = executor.split(':');
return workspace.readExecutor(nodeModule, executorName);
return getExecutorInformation(nodeModule, executorName, workspaceRoot);
}
export async function getCustomHasher(