fix(core): avoid launching default plugins twice (#29539)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
Default plugins are launched twice when loading plugins in a workspace
that has local plugins:
- Once to resolve the local plugin
- Once to be used as an actual plugin

## Expected Behavior
Default plugins are launched once and reused

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

Fixes #

---------

Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
This commit is contained in:
Craigory Coppola 2025-01-08 18:10:57 -05:00 committed by GitHub
parent fb318005f2
commit 0edd1102f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 546 additions and 474 deletions

View File

@ -7,7 +7,7 @@ import {
parseTargetString, parseTargetString,
} from '@nx/devkit'; } from '@nx/devkit';
import { addE2eCiTargetDefaults as _addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { addE2eCiTargetDefaults as _addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/internal-api'; import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/loaded-nx-plugin';
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
import { import {
ProjectConfigurationsError, ProjectConfigurationsError,

View File

@ -3,7 +3,10 @@ import { TempFs } from '../internal-testing-utils/temp-fs';
import { withEnvironmentVariables } from '../internal-testing-utils/with-environment'; import { withEnvironmentVariables } from '../internal-testing-utils/with-environment';
import { retrieveProjectConfigurations } from '../project-graph/utils/retrieve-workspace-files'; import { retrieveProjectConfigurations } from '../project-graph/utils/retrieve-workspace-files';
import { readNxJson } from './configuration'; import { readNxJson } from './configuration';
import { loadNxPlugins } from '../project-graph/plugins/internal-api'; import {
cleanupPlugins,
getPlugins,
} from '../project-graph/plugins/get-plugins';
describe('Workspaces', () => { describe('Workspaces', () => {
let fs: TempFs; let fs: TempFs;
@ -40,16 +43,13 @@ describe('Workspaces', () => {
NX_WORKSPACE_ROOT_PATH: fs.tempDir, NX_WORKSPACE_ROOT_PATH: fs.tempDir,
}, },
async () => { async () => {
const [plugins, cleanup] = await loadNxPlugins( const plugins = await getPlugins(fs.tempDir);
readNxJson(fs.tempDir).plugins,
fs.tempDir
);
const res = await retrieveProjectConfigurations( const res = await retrieveProjectConfigurations(
plugins, plugins,
fs.tempDir, fs.tempDir,
readNxJson(fs.tempDir) readNxJson(fs.tempDir)
); );
cleanup(); cleanupPlugins();
return res; return res;
} }
); );

View File

@ -30,7 +30,7 @@ import { notifyFileWatcherSockets } from './file-watching/file-watcher-sockets';
import { serverLogger } from './logger'; import { serverLogger } from './logger';
import { NxWorkspaceFilesExternals } from '../../native'; import { NxWorkspaceFilesExternals } from '../../native';
import { ConfigurationResult } from '../../project-graph/utils/project-configuration-utils'; import { ConfigurationResult } from '../../project-graph/utils/project-configuration-utils';
import { LoadedNxPlugin } from '../../project-graph/plugins/internal-api'; import type { LoadedNxPlugin } from '../../project-graph/plugins/loaded-nx-plugin';
import { import {
DaemonProjectGraphError, DaemonProjectGraphError,
ProjectConfigurationsError, ProjectConfigurationsError,

View File

@ -26,7 +26,7 @@ export {
findProjectForPath, findProjectForPath,
} from './project-graph/utils/find-project-for-path'; } from './project-graph/utils/find-project-for-path';
export { retrieveProjectConfigurations } from './project-graph/utils/retrieve-workspace-files'; export { retrieveProjectConfigurations } from './project-graph/utils/retrieve-workspace-files';
export { LoadedNxPlugin } from './project-graph/plugins/internal-api'; export { LoadedNxPlugin } from './project-graph/plugins/loaded-nx-plugin';
export * from './project-graph/error-types'; export * from './project-graph/error-types';
export { registerTsProject } from './plugins/js/utils/register'; export { registerTsProject } from './plugins/js/utils/register';
export { interpolate } from './tasks-runner/utils'; export { interpolate } from './tasks-runner/utils';

View File

@ -5,7 +5,6 @@ const tempFs = new TempFs('explicit-project-deps');
import { ProjectGraphProjectNode } from '../../../../config/project-graph'; import { ProjectGraphProjectNode } from '../../../../config/project-graph';
import { ProjectConfiguration } from '../../../../config/workspace-json-project-json'; import { ProjectConfiguration } from '../../../../config/workspace-json-project-json';
import { CreateDependenciesContext } from '../../../../project-graph/plugins'; import { CreateDependenciesContext } from '../../../../project-graph/plugins';
import { loadNxPlugins } from '../../../../project-graph/plugins/internal-api';
import { ProjectGraphBuilder } from '../../../../project-graph/project-graph-builder'; import { ProjectGraphBuilder } from '../../../../project-graph/project-graph-builder';
import { import {
retrieveProjectConfigurations, retrieveProjectConfigurations,
@ -14,6 +13,10 @@ import {
import { setupWorkspaceContext } from '../../../../utils/workspace-context'; import { setupWorkspaceContext } from '../../../../utils/workspace-context';
import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies'; import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies';
import { TargetProjectLocator } from './target-project-locator'; import { TargetProjectLocator } from './target-project-locator';
import {
cleanupPlugins,
getOnlyDefaultPlugins,
} from '../../../../project-graph/plugins/get-plugins';
// projectName => tsconfig import path // projectName => tsconfig import path
const dependencyProjectNamesToImportPaths = { const dependencyProjectNamesToImportPaths = {
@ -698,13 +701,13 @@ async function createContext(
setupWorkspaceContext(tempFs.tempDir); setupWorkspaceContext(tempFs.tempDir);
const [plugins, cleanup] = await loadNxPlugins([], tempFs.tempDir); const plugins = await getOnlyDefaultPlugins(tempFs.tempDir);
const { projects, projectRootMap } = await retrieveProjectConfigurations( const { projects, projectRootMap } = await retrieveProjectConfigurations(
plugins, plugins,
tempFs.tempDir, tempFs.tempDir,
nxJson nxJson
); );
cleanup(); cleanupPlugins();
const { fileMap } = await retrieveWorkspaceFiles( const { fileMap } = await retrieveWorkspaceFiles(
tempFs.tempDir, tempFs.tempDir,

View File

@ -12,7 +12,7 @@ import {
} from './nx-deps-cache'; } from './nx-deps-cache';
import { applyImplicitDependencies } from './utils/implicit-project-dependencies'; import { applyImplicitDependencies } from './utils/implicit-project-dependencies';
import { normalizeProjectNodes } from './utils/normalize-project-nodes'; import { normalizeProjectNodes } from './utils/normalize-project-nodes';
import { LoadedNxPlugin } from './plugins/internal-api'; import type { LoadedNxPlugin } from './plugins/loaded-nx-plugin';
import { import {
CreateDependenciesContext, CreateDependenciesContext,
CreateMetadataContext, CreateMetadataContext,

View File

@ -1,17 +1,33 @@
import { hashObject } from '../../hasher/file-hasher'; import { join } from 'node:path';
import { readNxJson } from '../../config/nx-json';
import { LoadedNxPlugin, loadNxPlugins } from './internal-api';
import { workspaceRoot } from '../../utils/workspace-root';
import { shouldMergeAngularProjects } from '../../adapter/angular-json';
import { PluginConfiguration, readNxJson } from '../../config/nx-json';
import { hashObject } from '../../hasher/file-hasher';
import { IS_WASM } from '../../native';
import { workspaceRoot } from '../../utils/workspace-root';
import { loadNxPluginInIsolation } from './isolation';
import { loadNxPlugin } from './in-process-loader';
import type { LoadedNxPlugin } from './loaded-nx-plugin';
import {
cleanupPluginTSTranspiler,
pluginTranspilerIsRegistered,
} from './transpiler';
/**
* Stuff for specified NX Plugins.
*/
let currentPluginsConfigurationHash: string; let currentPluginsConfigurationHash: string;
let loadedPlugins: LoadedNxPlugin[]; let loadedPlugins: LoadedNxPlugin[];
let pendingPluginsPromise: let pendingPluginsPromise:
| Promise<readonly [LoadedNxPlugin[], () => void]> | Promise<readonly [LoadedNxPlugin[], () => void]>
| undefined; | undefined;
let cleanup: () => void; let cleanup: () => void | undefined;
export async function getPlugins() { export async function getPlugins(
const pluginsConfiguration = readNxJson().plugins ?? []; root = workspaceRoot
): Promise<LoadedNxPlugin[]> {
const pluginsConfiguration = readNxJson(root).plugins ?? [];
const pluginsConfigurationHash = hashObject(pluginsConfiguration); const pluginsConfigurationHash = hashObject(pluginsConfiguration);
// If the plugins configuration has not changed, reuse the current plugins // If the plugins configuration has not changed, reuse the current plugins
@ -28,22 +44,29 @@ export async function getPlugins() {
cleanup(); cleanup();
} }
pendingPluginsPromise ??= loadNxPlugins(pluginsConfiguration, workspaceRoot); pendingPluginsPromise ??= loadSpecifiedNxPlugins(pluginsConfiguration, root);
currentPluginsConfigurationHash = pluginsConfigurationHash; currentPluginsConfigurationHash = pluginsConfigurationHash;
const [result, cleanupFn] = await pendingPluginsPromise; const [[result, cleanupFn], defaultPlugins] = await Promise.all([
pendingPluginsPromise,
getOnlyDefaultPlugins(root),
]);
cleanup = cleanupFn; cleanup = cleanupFn;
loadedPlugins = result; loadedPlugins = result.concat(defaultPlugins);
return result; return loadedPlugins;
} }
/**
* Stuff for default NX Plugins.
*/
let loadedDefaultPlugins: LoadedNxPlugin[]; let loadedDefaultPlugins: LoadedNxPlugin[];
let cleanupDefaultPlugins: () => void; let cleanupDefaultPlugins: () => void;
let pendingDefaultPluginPromise: let pendingDefaultPluginPromise:
| Promise<readonly [LoadedNxPlugin[], () => void]> | Promise<readonly [LoadedNxPlugin[], () => void]>
| undefined; | undefined;
export async function getOnlyDefaultPlugins() { export async function getOnlyDefaultPlugins(root = workspaceRoot) {
// If the plugins configuration has not changed, reuse the current plugins // If the plugins configuration has not changed, reuse the current plugins
if (loadedDefaultPlugins) { if (loadedDefaultPlugins) {
return loadedPlugins; return loadedPlugins;
@ -55,7 +78,7 @@ export async function getOnlyDefaultPlugins() {
cleanupDefaultPlugins(); cleanupDefaultPlugins();
} }
pendingDefaultPluginPromise ??= loadNxPlugins([], workspaceRoot); pendingDefaultPluginPromise ??= loadDefaultNxPlugins(workspaceRoot);
const [result, cleanupFn] = await pendingDefaultPluginPromise; const [result, cleanupFn] = await pendingDefaultPluginPromise;
cleanupDefaultPlugins = cleanupFn; cleanupDefaultPlugins = cleanupFn;
@ -66,6 +89,138 @@ export async function getOnlyDefaultPlugins() {
export function cleanupPlugins() { export function cleanupPlugins() {
pendingPluginsPromise = undefined; pendingPluginsPromise = undefined;
pendingDefaultPluginPromise = undefined; pendingDefaultPluginPromise = undefined;
cleanup(); cleanup?.();
cleanupDefaultPlugins(); cleanupDefaultPlugins?.();
}
/**
* Stuff for generic loading
*/
function isIsolationEnabled() {
// Explicitly enabled, regardless of further conditions
if (process.env.NX_ISOLATE_PLUGINS === 'true') {
return true;
}
if (
// Explicitly disabled
process.env.NX_ISOLATE_PLUGINS === 'false' ||
// Isolation is disabled on WASM builds currently.
IS_WASM
) {
return false;
}
// Default value
return true;
}
const loadingMethod = isIsolationEnabled()
? loadNxPluginInIsolation
: loadNxPlugin;
async function loadDefaultNxPlugins(root = workspaceRoot) {
performance.mark('loadDefaultNxPlugins:start');
const plugins = getDefaultPlugins(root);
const cleanupFunctions: Array<() => void> = [];
const ret = [
await Promise.all(
plugins.map(async (plugin) => {
performance.mark(`Load Nx Plugin: ${plugin} - start`);
const [loadedPluginPromise, cleanup] = await loadingMethod(
plugin,
root
);
cleanupFunctions.push(cleanup);
const res = await loadedPluginPromise;
performance.mark(`Load Nx Plugin: ${plugin} - end`);
performance.measure(
`Load Nx Plugin: ${plugin}`,
`Load Nx Plugin: ${plugin} - start`,
`Load Nx Plugin: ${plugin} - end`
);
return res;
})
),
() => {
for (const fn of cleanupFunctions) {
fn();
}
if (pluginTranspilerIsRegistered()) {
cleanupPluginTSTranspiler();
}
},
] as const;
performance.mark('loadDefaultNxPlugins:end');
performance.measure(
'loadDefaultNxPlugins',
'loadDefaultNxPlugins:start',
'loadDefaultNxPlugins:end'
);
return ret;
}
async function loadSpecifiedNxPlugins(
plugins: PluginConfiguration[],
root = workspaceRoot
): Promise<readonly [LoadedNxPlugin[], () => void]> {
performance.mark('loadSpecifiedNxPlugins:start');
plugins ??= [];
const cleanupFunctions: Array<() => void> = [];
const ret = [
await Promise.all(
plugins.map(async (plugin) => {
const pluginPath = typeof plugin === 'string' ? plugin : plugin.plugin;
performance.mark(`Load Nx Plugin: ${pluginPath} - start`);
const [loadedPluginPromise, cleanup] = await loadingMethod(
plugin,
root
);
cleanupFunctions.push(cleanup);
const res = await loadedPluginPromise;
performance.mark(`Load Nx Plugin: ${pluginPath} - end`);
performance.measure(
`Load Nx Plugin: ${pluginPath}`,
`Load Nx Plugin: ${pluginPath} - start`,
`Load Nx Plugin: ${pluginPath} - end`
);
return res;
})
),
() => {
for (const fn of cleanupFunctions) {
fn();
}
if (pluginTranspilerIsRegistered()) {
cleanupPluginTSTranspiler();
}
},
] as const;
performance.mark('loadSpecifiedNxPlugins:end');
performance.measure(
'loadSpecifiedNxPlugins',
'loadSpecifiedNxPlugins:start',
'loadSpecifiedNxPlugins:end'
);
return ret;
}
function getDefaultPlugins(root: string) {
return [
join(__dirname, '../../plugins/js'),
...(shouldMergeAngularProjects(root, false)
? [join(__dirname, '../../adapter/angular-json')]
: []),
join(__dirname, '../../plugins/package-json'),
join(__dirname, '../../plugins/project-json/build-nodes/project-json'),
];
} }

View File

@ -0,0 +1,85 @@
// This file contains methods and utilities that should **only** be used by the plugin worker.
import { ProjectConfiguration } from '../../config/workspace-json-project-json';
import { getNxRequirePaths } from '../../utils/installation-directory';
import {
PackageJson,
readModulePackageJsonWithoutFallbacks,
} from '../../utils/package-json';
import { readJsonFile } from '../../utils/fileutils';
import type { PluginConfiguration } from '../../config/nx-json';
import type { LoadedNxPlugin } from './loaded-nx-plugin';
import { LoadPluginError } from '../error-types';
import path = require('node:path/posix');
import { loadResolvedNxPluginAsync } from './load-resolved-plugin';
import { resolveLocalNxPlugin, resolveNxPlugin } from './resolve-plugin';
import {
pluginTranspilerIsRegistered,
registerPluginTSTranspiler,
} from './transpiler';
export function readPluginPackageJson(
pluginName: string,
projects: Record<string, ProjectConfiguration>,
paths = getNxRequirePaths()
): {
path: string;
json: PackageJson;
} {
try {
const result = readModulePackageJsonWithoutFallbacks(pluginName, paths);
return {
json: result.packageJson,
path: result.path,
};
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
const localPluginPath = resolveLocalNxPlugin(pluginName, projects);
if (localPluginPath) {
const localPluginPackageJson = path.join(
localPluginPath.path,
'package.json'
);
if (pluginTranspilerIsRegistered()) {
registerPluginTSTranspiler();
}
return {
path: localPluginPackageJson,
json: readJsonFile(localPluginPackageJson),
};
}
}
throw e;
}
}
export function loadNxPlugin(plugin: PluginConfiguration, root: string) {
return [
loadNxPluginAsync(plugin, getNxRequirePaths(root), root),
() => {},
] as const;
}
export async function loadNxPluginAsync(
pluginConfiguration: PluginConfiguration,
paths: string[],
root: string
): Promise<LoadedNxPlugin> {
const moduleName =
typeof pluginConfiguration === 'string'
? pluginConfiguration
: pluginConfiguration.plugin;
try {
const { pluginPath, name, shouldRegisterTSTranspiler } =
await resolveNxPlugin(moduleName, root, paths);
if (shouldRegisterTSTranspiler) {
registerPluginTSTranspiler();
}
return loadResolvedNxPluginAsync(pluginConfiguration, pluginPath, name);
} catch (e) {
throw new LoadPluginError(moduleName, e);
}
}

View File

@ -2,5 +2,6 @@ export * from './public-api';
// export * from './get-plugins'; // export * from './get-plugins';
export { readPluginPackageJson, registerPluginTSTranspiler } from './loader'; export { readPluginPackageJson } from './in-process-loader';
export { registerPluginTSTranspiler } from './transpiler';
export { createNodesFromFiles } from './utils'; export { createNodesFromFiles } from './utils';

View File

@ -1,224 +0,0 @@
// This file contains the bits and bobs of the internal API for loading and interacting with Nx plugins.
// For the public API, used by plugin authors, see `./public-api.ts`.
import { join } from 'path';
import { workspaceRoot } from '../../utils/workspace-root';
import { PluginConfiguration } from '../../config/nx-json';
import { shouldMergeAngularProjects } from '../../adapter/angular-json';
import {
CreateDependencies,
CreateDependenciesContext,
CreateMetadata,
CreateMetadataContext,
CreateNodesContextV2,
CreateNodesResult,
NxPluginV2,
ProjectsMetadata,
} from './public-api';
import { ProjectGraph } from '../../config/project-graph';
import { loadNxPluginInIsolation } from './isolation';
import { loadNxPlugin, unregisterPluginTSTranspiler } from './loader';
import { createNodesFromFiles } from './utils';
import {
AggregateCreateNodesError,
isAggregateCreateNodesError,
} from '../error-types';
import { IS_WASM } from '../../native';
import { RawProjectGraphDependency } from '../project-graph-builder';
export class LoadedNxPlugin {
readonly name: string;
readonly createNodes?: [
filePattern: string,
// The create nodes function takes all matched files instead of just one, and includes
// the result's context.
fn: (
matchedFiles: string[],
context: CreateNodesContextV2
) => Promise<
Array<readonly [plugin: string, file: string, result: CreateNodesResult]>
>
];
readonly createDependencies?: (
context: CreateDependenciesContext
) => Promise<RawProjectGraphDependency[]>;
readonly createMetadata?: (
graph: ProjectGraph,
context: CreateMetadataContext
) => Promise<ProjectsMetadata>;
readonly options?: unknown;
readonly include?: string[];
readonly exclude?: string[];
constructor(plugin: NxPluginV2, pluginDefinition: PluginConfiguration) {
this.name = plugin.name;
if (typeof pluginDefinition !== 'string') {
this.options = pluginDefinition.options;
this.include = pluginDefinition.include;
this.exclude = pluginDefinition.exclude;
}
if (plugin.createNodes && !plugin.createNodesV2) {
this.createNodes = [
plugin.createNodes[0],
(configFiles, context) =>
createNodesFromFiles(
plugin.createNodes[1],
configFiles,
this.options,
context
).then((results) => results.map((r) => [this.name, r[0], r[1]])),
];
}
if (plugin.createNodesV2) {
this.createNodes = [
plugin.createNodesV2[0],
async (configFiles, context) => {
const result = await plugin.createNodesV2[1](
configFiles,
this.options,
context
);
return result.map((r) => [this.name, r[0], r[1]]);
},
];
}
if (this.createNodes) {
const inner = this.createNodes[1];
this.createNodes[1] = async (...args) => {
performance.mark(`${plugin.name}:createNodes - start`);
try {
return await inner(...args);
} catch (e) {
if (isAggregateCreateNodesError(e)) {
throw e;
}
// The underlying plugin errored out. We can't know any partial results.
throw new AggregateCreateNodesError([[null, e]], []);
} finally {
performance.mark(`${plugin.name}:createNodes - end`);
performance.measure(
`${plugin.name}:createNodes`,
`${plugin.name}:createNodes - start`,
`${plugin.name}:createNodes - end`
);
}
};
}
if (plugin.createDependencies) {
this.createDependencies = async (context) =>
plugin.createDependencies(this.options, context);
}
if (plugin.createMetadata) {
this.createMetadata = async (graph, context) =>
plugin.createMetadata(graph, this.options, context);
}
}
}
export type CreateNodesResultWithContext = CreateNodesResult & {
file: string;
pluginName: string;
};
function isIsolationEnabled() {
// Explicitly enabled, regardless of further conditions
if (process.env.NX_ISOLATE_PLUGINS === 'true') {
return true;
}
if (
// Explicitly disabled
process.env.NX_ISOLATE_PLUGINS === 'false' ||
// Isolation is disabled on WASM builds currently.
IS_WASM
) {
return false;
}
// Default value
return true;
}
/**
* Use `getPlugins` instead.
* @deprecated Do not use this. Use `getPlugins` instead.
*/
export async function loadNxPlugins(
plugins: PluginConfiguration[],
root = workspaceRoot
): Promise<readonly [LoadedNxPlugin[], () => void]> {
performance.mark('loadNxPlugins:start');
const loadingMethod = isIsolationEnabled()
? loadNxPluginInIsolation
: loadNxPlugin;
plugins = await normalizePlugins(plugins, root);
const cleanupFunctions: Array<() => void> = [];
const ret = [
await Promise.all(
plugins.map(async (plugin) => {
const pluginPath = typeof plugin === 'string' ? plugin : plugin.plugin;
performance.mark(`Load Nx Plugin: ${pluginPath} - start`);
const [loadedPluginPromise, cleanup] = await loadingMethod(
plugin,
root
);
cleanupFunctions.push(cleanup);
const res = await loadedPluginPromise;
performance.mark(`Load Nx Plugin: ${pluginPath} - end`);
performance.measure(
`Load Nx Plugin: ${pluginPath}`,
`Load Nx Plugin: ${pluginPath} - start`,
`Load Nx Plugin: ${pluginPath} - end`
);
return res;
})
),
() => {
for (const fn of cleanupFunctions) {
fn();
}
if (unregisterPluginTSTranspiler) {
unregisterPluginTSTranspiler();
}
},
] as const;
performance.mark('loadNxPlugins:end');
performance.measure(
'loadNxPlugins',
'loadNxPlugins:start',
'loadNxPlugins:end'
);
return ret;
}
async function normalizePlugins(plugins: PluginConfiguration[], root: string) {
plugins ??= [];
return [
...plugins,
// Most of the nx core node plugins go on the end, s.t. it overwrites any other plugins
...(await getDefaultPlugins(root)),
];
}
export async function getDefaultPlugins(root: string) {
return [
join(__dirname, '../../plugins/js'),
...(shouldMergeAngularProjects(root, false)
? [join(__dirname, '../../adapter/angular-json')]
: []),
join(__dirname, '../../plugins/package-json'),
join(__dirname, '../../plugins/project-json/build-nodes/project-json'),
];
}

View File

@ -1,6 +1,6 @@
import { workspaceRoot } from '../../../utils/workspace-root'; import { workspaceRoot } from '../../../utils/workspace-root';
import { PluginConfiguration } from '../../../config/nx-json'; import type { PluginConfiguration } from '../../../config/nx-json';
import { LoadedNxPlugin } from '../internal-api'; import type { LoadedNxPlugin } from '../loaded-nx-plugin';
import { loadRemoteNxPlugin } from './plugin-pool'; import { loadRemoteNxPlugin } from './plugin-pool';
export async function loadNxPluginInIsolation( export async function loadNxPluginInIsolation(

View File

@ -5,7 +5,7 @@ import {
CreateMetadataContext, CreateMetadataContext,
CreateNodesContextV2, CreateNodesContextV2,
} from '../public-api'; } from '../public-api';
import { LoadedNxPlugin } from '../internal-api'; import type { LoadedNxPlugin } from '../loaded-nx-plugin';
import { Serializable } from 'child_process'; import { Serializable } from 'child_process';
import { Socket } from 'net'; import { Socket } from 'net';

View File

@ -7,7 +7,7 @@ import { PluginConfiguration } from '../../../config/nx-json';
// TODO (@AgentEnder): After scoped verbose logging is implemented, re-add verbose logs here. // TODO (@AgentEnder): After scoped verbose logging is implemented, re-add verbose logs here.
// import { logger } from '../../utils/logger'; // import { logger } from '../../utils/logger';
import { LoadedNxPlugin } from '../internal-api'; import type { LoadedNxPlugin } from '../loaded-nx-plugin';
import { getPluginOsSocketPath } from '../../../daemon/socket-utils'; import { getPluginOsSocketPath } from '../../../daemon/socket-utils';
import { consumeMessagesFromSocket } from '../../../utils/consume-messages-from-socket'; import { consumeMessagesFromSocket } from '../../../utils/consume-messages-from-socket';
@ -17,7 +17,7 @@ import {
sendMessageOverSocket, sendMessageOverSocket,
} from './messaging'; } from './messaging';
import { getNxRequirePaths } from '../../../utils/installation-directory'; import { getNxRequirePaths } from '../../../utils/installation-directory';
import { resolveNxPlugin } from '../loader'; import { resolveNxPlugin } from '../resolve-plugin';
const cleanupFunctions = new Set<() => void>(); const cleanupFunctions = new Set<() => void>();

View File

@ -4,7 +4,6 @@ import { consumeMessagesFromSocket } from '../../../utils/consume-messages-from-
import { createServer } from 'net'; import { createServer } from 'net';
import { unlinkSync } from 'fs'; import { unlinkSync } from 'fs';
import { registerPluginTSTranspiler } from '../loader';
if (process.env.NX_PERF_LOGGING === 'true') { if (process.env.NX_PERF_LOGGING === 'true') {
require('../../../utils/perf-logging'); require('../../../utils/perf-logging');
@ -53,7 +52,9 @@ const server = createServer((socket) => {
// Register the ts-transpiler if we are pointing to a // Register the ts-transpiler if we are pointing to a
// plain ts file that's not part of a plugin project // plain ts file that's not part of a plugin project
if (shouldRegisterTSTranspiler) { if (shouldRegisterTSTranspiler) {
registerPluginTSTranspiler(); (
require('../transpiler') as typeof import('../transpiler')
).registerPluginTSTranspiler();
} }
plugin = await loadResolvedNxPluginAsync( plugin = await loadResolvedNxPluginAsync(
pluginConfiguration, pluginConfiguration,

View File

@ -1,5 +1,5 @@
import type { PluginConfiguration } from '../../config/nx-json'; import type { PluginConfiguration } from '../../config/nx-json';
import { LoadedNxPlugin } from './internal-api'; import { LoadedNxPlugin } from './loaded-nx-plugin';
import { NxPlugin } from './public-api'; import { NxPlugin } from './public-api';
export async function loadResolvedNxPluginAsync( export async function loadResolvedNxPluginAsync(

View File

@ -0,0 +1,116 @@
import type { ProjectGraph } from '../../config/project-graph';
import type { PluginConfiguration } from '../../config/nx-json';
import {
AggregateCreateNodesError,
isAggregateCreateNodesError,
} from '../error-types';
import type { RawProjectGraphDependency } from '../project-graph-builder';
import type {
CreateDependenciesContext,
CreateMetadataContext,
CreateNodesContextV2,
CreateNodesResult,
NxPluginV2,
ProjectsMetadata,
} from './public-api';
import { createNodesFromFiles } from './utils';
export class LoadedNxPlugin {
readonly name: string;
readonly createNodes?: [
filePattern: string,
// The create nodes function takes all matched files instead of just one, and includes
// the result's context.
fn: (
matchedFiles: string[],
context: CreateNodesContextV2
) => Promise<
Array<readonly [plugin: string, file: string, result: CreateNodesResult]>
>
];
readonly createDependencies?: (
context: CreateDependenciesContext
) => Promise<RawProjectGraphDependency[]>;
readonly createMetadata?: (
graph: ProjectGraph,
context: CreateMetadataContext
) => Promise<ProjectsMetadata>;
readonly options?: unknown;
readonly include?: string[];
readonly exclude?: string[];
constructor(plugin: NxPluginV2, pluginDefinition: PluginConfiguration) {
this.name = plugin.name;
if (typeof pluginDefinition !== 'string') {
this.options = pluginDefinition.options;
this.include = pluginDefinition.include;
this.exclude = pluginDefinition.exclude;
}
if (plugin.createNodes && !plugin.createNodesV2) {
this.createNodes = [
plugin.createNodes[0],
(configFiles, context) =>
createNodesFromFiles(
plugin.createNodes[1],
configFiles,
this.options,
context
).then((results) => results.map((r) => [this.name, r[0], r[1]])),
];
}
if (plugin.createNodesV2) {
this.createNodes = [
plugin.createNodesV2[0],
async (configFiles, context) => {
const result = await plugin.createNodesV2[1](
configFiles,
this.options,
context
);
return result.map((r) => [this.name, r[0], r[1]]);
},
];
}
if (this.createNodes) {
const inner = this.createNodes[1];
this.createNodes[1] = async (...args) => {
performance.mark(`${plugin.name}:createNodes - start`);
try {
return await inner(...args);
} catch (e) {
if (isAggregateCreateNodesError(e)) {
throw e;
}
// The underlying plugin errored out. We can't know any partial results.
throw new AggregateCreateNodesError([[null, e]], []);
} finally {
performance.mark(`${plugin.name}:createNodes - end`);
performance.measure(
`${plugin.name}:createNodes`,
`${plugin.name}:createNodes - start`,
`${plugin.name}:createNodes - end`
);
}
};
}
if (plugin.createDependencies) {
this.createDependencies = async (context) =>
plugin.createDependencies(this.options, context);
}
if (plugin.createMetadata) {
this.createMetadata = async (graph, context) =>
plugin.createMetadata(graph, this.options, context);
}
}
}
export type CreateNodesResultWithContext = CreateNodesResult & {
file: string;
pluginName: string;
};

View File

@ -1,71 +1,59 @@
// This file contains methods and utilities that should **only** be used by the plugin worker. import * as path from 'node:path';
import { ProjectConfiguration } from '../../config/workspace-json-project-json';
import { join } from 'node:path/posix';
import { getNxRequirePaths } from '../../utils/installation-directory';
import {
PackageJson,
readModulePackageJsonWithoutFallbacks,
} from '../../utils/package-json';
import { readJsonFile } from '../../utils/fileutils';
import { workspaceRoot } from '../../utils/workspace-root';
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import {
registerTranspiler,
registerTsConfigPaths,
} from '../../plugins/js/utils/register';
import {
ProjectRootMappings,
findProjectForPath,
} from '../utils/find-project-for-path';
import { normalizePath } from '../../utils/path';
import { logger } from '../../utils/logger';
import type * as ts from 'typescript';
import { extname } from 'node:path';
import type { PluginConfiguration } from '../../config/nx-json';
import { retrieveProjectConfigurationsWithoutPluginInference } from '../utils/retrieve-workspace-files';
import { LoadedNxPlugin } from './internal-api';
import { LoadPluginError } from '../error-types';
import path = require('node:path/posix');
import { readTsConfig } from '../../plugins/js/utils/typescript';
import { loadResolvedNxPluginAsync } from './load-resolved-plugin';
import { getPackageEntryPointsToProjectMap } from '../../plugins/js/utils/packages'; import { getPackageEntryPointsToProjectMap } from '../../plugins/js/utils/packages';
import { readJsonFile } from '../../utils/fileutils';
import { logger } from '../../utils/logger';
import { normalizePath } from '../../utils/path';
import { workspaceRoot } from '../../utils/workspace-root';
import {
findProjectForPath,
ProjectRootMappings,
} from '../utils/find-project-for-path';
import { retrieveProjectConfigurationsWithoutPluginInference } from '../utils/retrieve-workspace-files';
export function readPluginPackageJson( import type { ProjectConfiguration } from '../../config/workspace-json-project-json';
pluginName: string,
projects: Record<string, ProjectConfiguration>, let projectsWithoutInference: Record<string, ProjectConfiguration>;
paths = getNxRequirePaths()
): { export async function resolveNxPlugin(
path: string; moduleName: string,
json: PackageJson; root: string,
} { paths: string[]
) {
try { try {
const result = readModulePackageJsonWithoutFallbacks(pluginName, paths); require.resolve(moduleName);
return { } catch {
json: result.packageJson, // If a plugin cannot be resolved, we will need projects to resolve it
path: result.path, projectsWithoutInference ??=
}; await retrieveProjectConfigurationsWithoutPluginInference(root);
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
const localPluginPath = resolveLocalNxPlugin(pluginName, projects);
if (localPluginPath) {
const localPluginPackageJson = path.join(
localPluginPath.path,
'package.json'
);
if (!unregisterPluginTSTranspiler) {
registerPluginTSTranspiler();
}
return {
path: localPluginPackageJson,
json: readJsonFile(localPluginPackageJson),
};
}
}
throw e;
} }
const { pluginPath, name, shouldRegisterTSTranspiler } = getPluginPathAndName(
moduleName,
paths,
projectsWithoutInference,
root
);
return { pluginPath, name, shouldRegisterTSTranspiler };
}
function readPluginMainFromProjectConfiguration(
plugin: ProjectConfiguration
): string | null {
const { main } =
Object.values(plugin.targets).find((x) =>
[
'@nx/js:tsc',
'@nrwl/js:tsc',
'@nx/js:swc',
'@nrwl/js:swc',
'@nx/node:package',
'@nrwl/node:package',
].includes(x.executor)
)?.options ||
plugin.targets?.build?.options ||
{};
return main;
} }
export function resolveLocalNxPlugin( export function resolveLocalNxPlugin(
@ -76,40 +64,45 @@ export function resolveLocalNxPlugin(
return lookupLocalPlugin(importPath, projects, root); return lookupLocalPlugin(importPath, projects, root);
} }
export let unregisterPluginTSTranspiler: (() => void) | null = null; export function getPluginPathAndName(
moduleName: string,
/** paths: string[],
* Register swc-node or ts-node if they are not currently registered projects: Record<string, ProjectConfiguration>,
* with some default settings which work well for Nx plugins. root: string
*/ ) {
export function registerPluginTSTranspiler() { let pluginPath: string;
// Get the first tsconfig that matches the allowed set let shouldRegisterTSTranspiler = false;
const tsConfigName = [ try {
join(workspaceRoot, 'tsconfig.base.json'), pluginPath = require.resolve(moduleName, {
join(workspaceRoot, 'tsconfig.json'), paths,
].find((x) => existsSync(x)); });
const extension = path.extname(pluginPath);
if (!tsConfigName) { shouldRegisterTSTranspiler = extension === '.ts';
return; } catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
const plugin = resolveLocalNxPlugin(moduleName, projects, root);
if (plugin) {
shouldRegisterTSTranspiler = true;
const main = readPluginMainFromProjectConfiguration(
plugin.projectConfig
);
pluginPath = main ? path.join(root, main) : plugin.path;
} else {
logger.error(`Plugin listed in \`nx.json\` not found: ${moduleName}`);
throw e;
}
} else {
throw e;
}
} }
const packageJsonPath = path.join(pluginPath, 'package.json');
const tsConfig: Partial<ts.ParsedCommandLine> = tsConfigName const { name } =
? readTsConfig(tsConfigName) !['.ts', '.js'].some((x) => path.extname(moduleName) === x) && // Not trying to point to a ts or js file
: {}; existsSync(packageJsonPath) // plugin has a package.json
const cleanupFns = [ ? readJsonFile(packageJsonPath) // read name from package.json
registerTsConfigPaths(tsConfigName), : { name: moduleName };
registerTranspiler( return { pluginPath, name, shouldRegisterTSTranspiler };
{
experimentalDecorators: true,
emitDecoratorMetadata: true,
...tsConfig.options,
},
tsConfig.raw
),
];
unregisterPluginTSTranspiler = () => {
cleanupFns.forEach((fn) => fn?.());
};
} }
function lookupLocalPlugin( function lookupLocalPlugin(
@ -184,115 +177,3 @@ function readTsConfigPaths(root: string = workspaceRoot) {
} }
return tsconfigPaths ?? {}; return tsconfigPaths ?? {};
} }
function readPluginMainFromProjectConfiguration(
plugin: ProjectConfiguration
): string | null {
const { main } =
Object.values(plugin.targets).find((x) =>
[
'@nx/js:tsc',
'@nrwl/js:tsc',
'@nx/js:swc',
'@nrwl/js:swc',
'@nx/node:package',
'@nrwl/node:package',
].includes(x.executor)
)?.options ||
plugin.targets?.build?.options ||
{};
return main;
}
export function getPluginPathAndName(
moduleName: string,
paths: string[],
projects: Record<string, ProjectConfiguration>,
root: string
) {
let pluginPath: string;
let shouldRegisterTSTranspiler = false;
try {
pluginPath = require.resolve(moduleName, {
paths,
});
const extension = path.extname(pluginPath);
shouldRegisterTSTranspiler = extension === '.ts';
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
const plugin = resolveLocalNxPlugin(moduleName, projects, root);
if (plugin) {
shouldRegisterTSTranspiler = true;
const main = readPluginMainFromProjectConfiguration(
plugin.projectConfig
);
pluginPath = main ? path.join(root, main) : plugin.path;
} else {
logger.error(`Plugin listed in \`nx.json\` not found: ${moduleName}`);
throw e;
}
} else {
throw e;
}
}
const packageJsonPath = path.join(pluginPath, 'package.json');
const { name } =
!['.ts', '.js'].some((x) => extname(moduleName) === x) && // Not trying to point to a ts or js file
existsSync(packageJsonPath) // plugin has a package.json
? readJsonFile(packageJsonPath) // read name from package.json
: { name: moduleName };
return { pluginPath, name, shouldRegisterTSTranspiler };
}
let projectsWithoutInference: Record<string, ProjectConfiguration>;
export function loadNxPlugin(plugin: PluginConfiguration, root: string) {
return [
loadNxPluginAsync(plugin, getNxRequirePaths(root), root),
() => {},
] as const;
}
export async function resolveNxPlugin(
moduleName: string,
root: string,
paths: string[]
) {
try {
require.resolve(moduleName);
} catch {
// If a plugin cannot be resolved, we will need projects to resolve it
projectsWithoutInference ??=
await retrieveProjectConfigurationsWithoutPluginInference(root);
}
const { pluginPath, name, shouldRegisterTSTranspiler } = getPluginPathAndName(
moduleName,
paths,
projectsWithoutInference,
root
);
return { pluginPath, name, shouldRegisterTSTranspiler };
}
export async function loadNxPluginAsync(
pluginConfiguration: PluginConfiguration,
paths: string[],
root: string
): Promise<LoadedNxPlugin> {
const moduleName =
typeof pluginConfiguration === 'string'
? pluginConfiguration
: pluginConfiguration.plugin;
try {
const { pluginPath, name, shouldRegisterTSTranspiler } =
await resolveNxPlugin(moduleName, root, paths);
if (shouldRegisterTSTranspiler) {
registerPluginTSTranspiler();
}
return loadResolvedNxPluginAsync(pluginConfiguration, pluginPath, name);
} catch (e) {
throw new LoadPluginError(moduleName, e);
}
}

View File

@ -0,0 +1,54 @@
import { existsSync } from 'node:fs';
import { join } from 'node:path/posix';
import { workspaceRoot } from '../../utils/workspace-root';
import {
registerTranspiler,
registerTsConfigPaths,
} from '../../plugins/js/utils/register';
import { readTsConfig } from '../../plugins/js/utils/typescript';
import type * as ts from 'typescript';
export let unregisterPluginTSTranspiler: (() => void) | null = null;
/**
* Register swc-node or ts-node if they are not currently registered
* with some default settings which work well for Nx plugins.
*/
export function registerPluginTSTranspiler() {
// Get the first tsconfig that matches the allowed set
const tsConfigName = [
join(workspaceRoot, 'tsconfig.base.json'),
join(workspaceRoot, 'tsconfig.json'),
].find((x) => existsSync(x));
if (!tsConfigName) {
return;
}
const tsConfig: Partial<ts.ParsedCommandLine> = tsConfigName
? readTsConfig(tsConfigName)
: {};
const cleanupFns = [
registerTsConfigPaths(tsConfigName),
registerTranspiler(
{
experimentalDecorators: true,
emitDecoratorMetadata: true,
...tsConfig.options,
},
tsConfig.raw
),
];
unregisterPluginTSTranspiler = () => {
cleanupFns.forEach((fn) => fn?.());
};
}
export function pluginTranspilerIsRegistered() {
return unregisterPluginTSTranspiler !== null;
}
export function cleanupPluginTSTranspiler() {
unregisterPluginTSTranspiler?.();
unregisterPluginTSTranspiler = null;
}

View File

@ -15,7 +15,7 @@ import {
readTargetDefaultsForTarget, readTargetDefaultsForTarget,
} from './project-configuration-utils'; } from './project-configuration-utils';
import { NxPluginV2 } from '../plugins'; import { NxPluginV2 } from '../plugins';
import { LoadedNxPlugin } from '../plugins/internal-api'; import { LoadedNxPlugin } from '../plugins/loaded-nx-plugin';
import { dirname } from 'path'; import { dirname } from 'path';
import { isProjectConfigurationsError } from '../error-types'; import { isProjectConfigurationsError } from '../error-types';

View File

@ -14,7 +14,7 @@ import { minimatch } from 'minimatch';
import { join } from 'path'; import { join } from 'path';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { LoadedNxPlugin } from '../plugins/internal-api'; import { LoadedNxPlugin } from '../plugins/loaded-nx-plugin';
import { import {
MergeNodesError, MergeNodesError,
ProjectConfigurationsError, ProjectConfigurationsError,

View File

@ -9,7 +9,7 @@ import {
ConfigurationResult, ConfigurationResult,
createProjectConfigurations, createProjectConfigurations,
} from './project-configuration-utils'; } from './project-configuration-utils';
import { LoadedNxPlugin } from '../plugins/internal-api'; import { LoadedNxPlugin } from '../plugins/loaded-nx-plugin';
import { import {
getNxWorkspaceFilesFromContext, getNxWorkspaceFilesFromContext,
globWithWorkspaceContext, globWithWorkspaceContext,

View File

@ -7,9 +7,9 @@ import { ProjectConfiguration } from '../../config/workspace-json-project-json';
import { readJsonFile } from '../fileutils'; import { readJsonFile } from '../fileutils';
import { getNxRequirePaths } from '../installation-directory'; import { getNxRequirePaths } from '../installation-directory';
import { readPluginPackageJson } from '../../project-graph/plugins'; import { readPluginPackageJson } from '../../project-graph/plugins';
import { loadNxPlugin } from '../../project-graph/plugins/loader'; import { loadNxPlugin } from '../../project-graph/plugins/in-process-loader';
import { PackageJson } from '../package-json'; import { PackageJson } from '../package-json';
import { LoadedNxPlugin } from '../../project-graph/plugins/internal-api'; import { LoadedNxPlugin } from '../../project-graph/plugins/loaded-nx-plugin';
export interface PluginCapabilities { export interface PluginCapabilities {
name: string; name: string;

View File

@ -6,7 +6,7 @@ import {
parseTargetString, parseTargetString,
} from '@nx/devkit'; } from '@nx/devkit';
import { addE2eCiTargetDefaults as _addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { addE2eCiTargetDefaults as _addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/internal-api'; import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/loaded-nx-plugin';
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
import { import {
ProjectConfigurationsError, ProjectConfigurationsError,

View File

@ -12,7 +12,7 @@ import {
import { tsquery } from '@phenomnomnominal/tsquery'; import { tsquery } from '@phenomnomnominal/tsquery';
import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults'; import addE2eCiTargetDefaults from './add-e2e-ci-target-defaults';
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/internal-api'; import { LoadedNxPlugin } from 'nx/src/project-graph/plugins/loaded-nx-plugin';
import { retrieveProjectConfigurations } from 'nx/src/project-graph/utils/retrieve-workspace-files'; import { retrieveProjectConfigurations } from 'nx/src/project-graph/utils/retrieve-workspace-files';
import { ProjectConfigurationsError } from 'nx/src/project-graph/error-types'; import { ProjectConfigurationsError } from 'nx/src/project-graph/error-types';
import { createNodesV2 as webpackCreateNodesV2 } from '@nx/webpack/src/plugins/plugin'; import { createNodesV2 as webpackCreateNodesV2 } from '@nx/webpack/src/plugins/plugin';