feat(core): re-enable running plugins in isolation (#22527)

This commit is contained in:
Craigory Coppola 2024-04-09 18:36:33 -04:00 committed by GitHub
parent caf663fd32
commit 7a7cbeca44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 1702 additions and 972 deletions

View File

@ -1,6 +1,6 @@
# Type alias: CreateNodes\<T\> # Type alias: CreateNodes\<T\>
Ƭ **CreateNodes**\<`T`\>: readonly [configFilePattern: string, createNodesFunction: CreateNodesFunction\<T\>] Ƭ **CreateNodes**\<`T`\>: readonly [projectFilePattern: string, createNodesFunction: CreateNodesFunction\<T\>]
A pair of file patterns and [CreateNodesFunction](../../devkit/documents/CreateNodesFunction) A pair of file patterns and [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)

View File

@ -0,0 +1,12 @@
# Type alias: ExpandedPluginConfiguration
Ƭ **ExpandedPluginConfiguration**: `Object`
#### Type declaration
| Name | Type |
| :--------- | :--------- |
| `exclude?` | `string`[] |
| `include?` | `string`[] |
| `options?` | `unknown` |
| `plugin` | `string` |

View File

@ -15,5 +15,5 @@ A plugin for Nx which creates nodes and dependencies for the [ProjectGraph](../.
| Name | Type | Description | | Name | Type | Description |
| :-------------------- | :------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | | :-------------------- | :------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- |
| `createDependencies?` | [`CreateDependencies`](../../devkit/documents/CreateDependencies)\<`TOptions`\> | Provides a function to analyze files to create dependencies for the [ProjectGraph](../../devkit/documents/ProjectGraph) | | `createDependencies?` | [`CreateDependencies`](../../devkit/documents/CreateDependencies)\<`TOptions`\> | Provides a function to analyze files to create dependencies for the [ProjectGraph](../../devkit/documents/ProjectGraph) |
| `createNodes?` | [`CreateNodes`](../../devkit/documents/CreateNodes) | Provides a file pattern and function that retrieves configuration info from those files. e.g. { '\*_/_.csproj': buildProjectsFromCsProjFile } | | `createNodes?` | [`CreateNodes`](../../devkit/documents/CreateNodes)\<`TOptions`\> | Provides a file pattern and function that retrieves configuration info from those files. e.g. { '\*_/_.csproj': buildProjectsFromCsProjFile } |
| `name` | `string` | - | | `name` | `string` | - |

View File

@ -1,3 +1,3 @@
# Type alias: PluginConfiguration # Type alias: PluginConfiguration
Ƭ **PluginConfiguration**: `string` \| \{ `exclude?`: `string`[] ; `include?`: `string`[] ; `options?`: `unknown` ; `plugin`: `string` } Ƭ **PluginConfiguration**: `string` \| [`ExpandedPluginConfiguration`](../../devkit/documents/ExpandedPluginConfiguration)

View File

@ -69,6 +69,7 @@ It only uses language primitives and immutable objects
- [CustomHasher](../../devkit/documents/CustomHasher) - [CustomHasher](../../devkit/documents/CustomHasher)
- [DynamicDependency](../../devkit/documents/DynamicDependency) - [DynamicDependency](../../devkit/documents/DynamicDependency)
- [Executor](../../devkit/documents/Executor) - [Executor](../../devkit/documents/Executor)
- [ExpandedPluginConfiguration](../../devkit/documents/ExpandedPluginConfiguration)
- [Generator](../../devkit/documents/Generator) - [Generator](../../devkit/documents/Generator)
- [GeneratorCallback](../../devkit/documents/GeneratorCallback) - [GeneratorCallback](../../devkit/documents/GeneratorCallback)
- [Hasher](../../devkit/documents/Hasher) - [Hasher](../../devkit/documents/Hasher)

View File

@ -5,10 +5,11 @@
#### Type declaration #### Type declaration
| Name | Type | | Name | Type |
| :------ | :-------------------------- | | :-------- | :-------------------------- |
| `debug` | (...`s`: `any`[]) => `void` | | `debug` | (...`s`: `any`[]) => `void` |
| `error` | (`s`: `any`) => `void` | | `error` | (`s`: `any`) => `void` |
| `fatal` | (...`s`: `any`[]) => `void` | | `fatal` | (...`s`: `any`[]) => `void` |
| `info` | (`s`: `any`) => `void` | | `info` | (`s`: `any`) => `void` |
| `log` | (...`s`: `any`[]) => `void` | | `log` | (...`s`: `any`[]) => `void` |
| `verbose` | (...`s`: `any`[]) => `void` |
| `warn` | (`s`: `any`) => `void` | | `warn` | (`s`: `any`) => `void` |

View File

@ -69,6 +69,7 @@ It only uses language primitives and immutable objects
- [CustomHasher](../../devkit/documents/CustomHasher) - [CustomHasher](../../devkit/documents/CustomHasher)
- [DynamicDependency](../../devkit/documents/DynamicDependency) - [DynamicDependency](../../devkit/documents/DynamicDependency)
- [Executor](../../devkit/documents/Executor) - [Executor](../../devkit/documents/Executor)
- [ExpandedPluginConfiguration](../../devkit/documents/ExpandedPluginConfiguration)
- [Generator](../../devkit/documents/Generator) - [Generator](../../devkit/documents/Generator)
- [GeneratorCallback](../../devkit/documents/GeneratorCallback) - [GeneratorCallback](../../devkit/documents/GeneratorCallback)
- [Hasher](../../devkit/documents/Hasher) - [Hasher](../../devkit/documents/Hasher)

View File

@ -1,23 +1,35 @@
// When plugins from root nx.json load through ts-jest, they can cause transpile errors such as `@nx/playwright/plugin.d.ts` containing an unexpected "export" keyword. import { TempFs } from '../../internal-testing-utils';
// Mock `loadNxPlugins` function to prevent them from loading.
jest.mock('nx/src/utils/nx-plugin', () => ({
loadNxPlugins: () => Promise.resolve([]),
}));
import { convertNxExecutor } from './convert-nx-executor'; import { convertNxExecutor } from './convert-nx-executor';
describe('Convert Nx Executor', () => { describe('Convert Nx Executor', () => {
let fs: TempFs;
beforeAll(async () => {
fs = new TempFs('convert-nx-executor');
// The tests in this file don't actually care about the files in the temp dir.
// The converted executor reads project configuration from the workspace root,
// which is set to the temp dir in the tests. If there are no files in the temp
// dir, the glob search currently hangs. So we create a dummy file to prevent that.
await fs.createFile('blah.json', JSON.stringify({}));
});
afterAll(() => {
fs.cleanup();
});
it('should convertNxExecutor to builder correctly and produce the same output', async () => { it('should convertNxExecutor to builder correctly and produce the same output', async () => {
// ARRANGE // ARRANGE
const { schema } = require('@angular-devkit/core'); const { schema } = require('@angular-devkit/core');
const { const {
TestingArchitectHost, TestingArchitectHost,
} = require('@angular-devkit/architect/testing'); // nx-ignore-next-line
} = require('@angular-devkit/architect/testing') as typeof import('@angular-devkit/architect/testing');
const { Architect } = require('@angular-devkit/architect'); const { Architect } = require('@angular-devkit/architect');
const registry = new schema.CoreSchemaRegistry(); const registry = new schema.CoreSchemaRegistry();
registry.addPostTransform(schema.transforms.addUndefinedDefaults); registry.addPostTransform(schema.transforms.addUndefinedDefaults);
const testArchitectHost = new TestingArchitectHost(); const testArchitectHost = new TestingArchitectHost();
testArchitectHost.workspaceRoot = fs.tempDir;
const architect = new Architect(testArchitectHost, registry); const architect = new Architect(testArchitectHost, registry);
const convertedExecutor = convertNxExecutor(echoExecutor); const convertedExecutor = convertNxExecutor(echoExecutor);

View File

@ -11,10 +11,10 @@ import { readNxJson } from '../src/config/nx-json';
import { setupWorkspaceContext } from '../src/utils/workspace-context'; import { setupWorkspaceContext } from '../src/utils/workspace-context';
(async () => { (async () => {
const start = new Date();
try { try {
setupWorkspaceContext(workspaceRoot); setupWorkspaceContext(workspaceRoot);
if (isMainNxPackage() && fileExists(join(workspaceRoot, 'nx.json'))) { if (isMainNxPackage() && fileExists(join(workspaceRoot, 'nx.json'))) {
const b = new Date();
assertSupportedPlatform(); assertSupportedPlatform();
try { try {
@ -35,15 +35,18 @@ import { setupWorkspaceContext } from '../src/utils/workspace-context';
}); });
}) })
); );
if (process.env.NX_VERBOSE_LOGGING === 'true') {
const a = new Date();
console.log(`Nx postinstall steps took ${a.getTime() - b.getTime()}ms`);
}
} }
} catch (e) { } catch (e) {
if (process.env.NX_VERBOSE_LOGGING === 'true') { if (process.env.NX_VERBOSE_LOGGING === 'true') {
console.log(e); console.log(e);
} }
} finally {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
const end = new Date();
console.log(
`Nx postinstall steps took ${end.getTime() - start.getTime()}ms`
);
}
} }
})(); })();

View File

@ -1,4 +1,4 @@
import type { NxPluginV2 } from '../src/utils/nx-plugin'; import type { NxPluginV2 } from '../src/project-graph/plugins';
import { workspaceRoot } from '../src/utils/workspace-root'; import { workspaceRoot } from '../src/utils/workspace-root';
import { createNodeFromPackageJson } from '../src/plugins/package-json-workspaces'; import { createNodeFromPackageJson } from '../src/plugins/package-json-workspaces';

View File

@ -2,7 +2,7 @@ import { existsSync } from 'fs';
import * as path from 'path'; import * as path from 'path';
import { readJsonFile } from '../utils/fileutils'; import { readJsonFile } from '../utils/fileutils';
import { ProjectsConfigurations } from '../config/workspace-json-project-json'; import { ProjectsConfigurations } from '../config/workspace-json-project-json';
import { NxPluginV2 } from '../utils/nx-plugin'; import { NxPluginV2 } from '../project-graph/plugins';
export const NX_ANGULAR_JSON_PLUGIN_NAME = 'nx-angular-json-plugin'; export const NX_ANGULAR_JSON_PLUGIN_NAME = 'nx-angular-json-plugin';
@ -16,6 +16,8 @@ export const NxAngularJsonPlugin: NxPluginV2 = {
], ],
}; };
export default NxAngularJsonPlugin;
export function shouldMergeAngularProjects( export function shouldMergeAngularProjects(
root: string, root: string,
includeProjectsFromAngularJson: boolean includeProjectsFromAngularJson: boolean

View File

@ -59,7 +59,7 @@ import {
ExecutorsJson, ExecutorsJson,
TaskGraphExecutor, TaskGraphExecutor,
} from '../config/misc-interfaces'; } from '../config/misc-interfaces';
import { readPluginPackageJson } from '../utils/nx-plugin'; import { readPluginPackageJson } from '../project-graph/plugins';
import { import {
getImplementationFactory, getImplementationFactory,
resolveImplementation, resolveImplementation,

View File

@ -10,7 +10,7 @@ import {
resolveSchema, resolveSchema,
} from '../../config/schema-utils'; } from '../../config/schema-utils';
import { readJsonFile } from '../../utils/fileutils'; import { readJsonFile } from '../../utils/fileutils';
import { readPluginPackageJson } from '../../utils/nx-plugin'; import { readPluginPackageJson } from '../../project-graph/plugins';
export function getGeneratorInformation( export function getGeneratorInformation(
collectionName: string, collectionName: string,

View File

@ -1,6 +1,6 @@
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { readPluginPackageJson } from '../../utils/nx-plugin'; import { readPluginPackageJson } from '../../project-graph/plugins';
import { import {
CustomHasher, CustomHasher,
Executor, Executor,

View File

@ -436,9 +436,9 @@ export interface NxJsonConfiguration<T = '*' | string[]> {
useInferencePlugins?: boolean; useInferencePlugins?: boolean;
} }
export type PluginConfiguration = export type PluginConfiguration = string | ExpandedPluginConfiguration;
| string
| { export type ExpandedPluginConfiguration = {
plugin: string; plugin: string;
options?: unknown; options?: unknown;
include?: string[]; include?: string[];

View File

@ -1,6 +1,6 @@
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { extname, join } from 'path'; import { extname, join } from 'path';
import { registerPluginTSTranspiler } from '../utils/nx-plugin'; import { registerPluginTSTranspiler } from '../project-graph/plugins';
/** /**
* This function is used to get the implementation factory of an executor or generator. * This function is used to get the implementation factory of an executor or generator.

View File

@ -1,22 +1,9 @@
import { toProjectName, Workspaces } from './workspaces'; import { toProjectName } from './workspaces';
import { TempFs } from '../internal-testing-utils/temp-fs'; 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';
const libConfig = (root, name?: string) => ({
name: name ?? toProjectName(`${root}/some-file`),
projectType: 'library',
root: `libs/${root}`,
sourceRoot: `libs/${root}/src`,
targets: {
'nx-release-publish': {
dependsOn: ['^nx-release-publish'],
executor: '@nx/js:release-publish',
options: {},
},
},
});
describe('Workspaces', () => { describe('Workspaces', () => {
let fs: TempFs; let fs: TempFs;
@ -48,10 +35,23 @@ describe('Workspaces', () => {
const { projects } = await withEnvironmentVariables( const { projects } = await withEnvironmentVariables(
{ {
NX_WORKSPACE_ROOT: fs.tempDir, NX_WORKSPACE_ROOT_PATH: fs.tempDir,
}, },
() => retrieveProjectConfigurations(fs.tempDir, readNxJson(fs.tempDir)) async () => {
const [plugins, cleanup] = await loadNxPlugins(
readNxJson(fs.tempDir).plugins,
fs.tempDir
); );
const res = retrieveProjectConfigurations(
plugins,
fs.tempDir,
readNxJson(fs.tempDir)
);
cleanup();
return res;
}
);
console.log(projects);
expect(projects['my-package']).toEqual({ expect(projects['my-package']).toEqual({
name: 'my-package', name: 'my-package',
root: 'packages/my-package', root: 'packages/my-package',

View File

@ -3,6 +3,8 @@ import { serializeResult } from '../socket-utils';
import { serverLogger } from './logger'; import { serverLogger } from './logger';
import { getCachedSerializedProjectGraphPromise } from './project-graph-incremental-recomputation'; import { getCachedSerializedProjectGraphPromise } from './project-graph-incremental-recomputation';
import { HandlerResult } from './server'; import { HandlerResult } from './server';
import { getPlugins } from './plugins';
import { readNxJson } from '../../config/nx-json';
export async function handleRequestProjectGraph(): Promise<HandlerResult> { export async function handleRequestProjectGraph(): Promise<HandlerResult> {
try { try {

View File

@ -0,0 +1,26 @@
import { readNxJson } from '../../config/nx-json';
import {
LoadedNxPlugin,
loadNxPlugins,
} from '../../project-graph/plugins/internal-api';
import { workspaceRoot } from '../../utils/workspace-root';
let loadedPlugins: Promise<LoadedNxPlugin[]>;
let cleanup: () => void;
export async function getPlugins() {
if (loadedPlugins) {
return loadedPlugins;
}
const pluginsConfiguration = readNxJson().plugins ?? [];
const [result, cleanupFn] = await loadNxPlugins(
pluginsConfiguration,
workspaceRoot
);
cleanup = cleanupFn;
return result;
}
export function cleanupPlugins() {
cleanup();
}

View File

@ -32,11 +32,11 @@ import { workspaceRoot } from '../../utils/workspace-root';
import { notifyFileWatcherSockets } from './file-watching/file-watcher-sockets'; 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 { import { ConfigurationResult } from '../../project-graph/utils/project-configuration-utils';
ConfigurationResult,
ProjectConfigurationsError,
} from '../../project-graph/utils/project-configuration-utils';
import { DaemonProjectGraphError } from '../daemon-project-graph-error'; import { DaemonProjectGraphError } from '../daemon-project-graph-error';
import { LoadedNxPlugin } from '../../project-graph/plugins/internal-api';
import { getPlugins } from './plugins';
import { ProjectConfigurationsError } from '../../project-graph/error-types';
interface SerializedProjectGraph { interface SerializedProjectGraph {
error: Error | null; error: Error | null;
@ -78,14 +78,15 @@ export async function getCachedSerializedProjectGraphPromise(): Promise<Serializ
// reset the wait time // reset the wait time
waitPeriod = 100; waitPeriod = 100;
await resetInternalStateIfNxDepsMissing(); await resetInternalStateIfNxDepsMissing();
const plugins = await getPlugins();
if (collectedUpdatedFiles.size == 0 && collectedDeletedFiles.size == 0) { if (collectedUpdatedFiles.size == 0 && collectedDeletedFiles.size == 0) {
if (!cachedSerializedProjectGraphPromise) { if (!cachedSerializedProjectGraphPromise) {
cachedSerializedProjectGraphPromise = cachedSerializedProjectGraphPromise =
processFilesAndCreateAndSerializeProjectGraph(); processFilesAndCreateAndSerializeProjectGraph(plugins);
} }
} else { } else {
cachedSerializedProjectGraphPromise = cachedSerializedProjectGraphPromise =
processFilesAndCreateAndSerializeProjectGraph(); processFilesAndCreateAndSerializeProjectGraph(plugins);
} }
return await cachedSerializedProjectGraphPromise; return await cachedSerializedProjectGraphPromise;
} catch (e) { } catch (e) {
@ -133,7 +134,7 @@ export function addUpdatedAndDeletedFiles(
} }
cachedSerializedProjectGraphPromise = cachedSerializedProjectGraphPromise =
processFilesAndCreateAndSerializeProjectGraph(); processFilesAndCreateAndSerializeProjectGraph(await getPlugins());
await cachedSerializedProjectGraphPromise; await cachedSerializedProjectGraphPromise;
if (createdFiles.length > 0) { if (createdFiles.length > 0) {
@ -209,7 +210,9 @@ async function processCollectedUpdatedAndDeletedFiles(
} }
} }
async function processFilesAndCreateAndSerializeProjectGraph(): Promise<SerializedProjectGraph> { async function processFilesAndCreateAndSerializeProjectGraph(
plugins: LoadedNxPlugin[]
): Promise<SerializedProjectGraph> {
try { try {
performance.mark('hash-watched-changes-start'); performance.mark('hash-watched-changes-start');
const updatedFiles = [...collectedUpdatedFiles.values()]; const updatedFiles = [...collectedUpdatedFiles.values()];
@ -227,14 +230,17 @@ async function processFilesAndCreateAndSerializeProjectGraph(): Promise<Serializ
serverLogger.requestLog([...updatedFiles.values()]); serverLogger.requestLog([...updatedFiles.values()]);
serverLogger.requestLog([...deletedFiles]); serverLogger.requestLog([...deletedFiles]);
const nxJson = readNxJson(workspaceRoot); const nxJson = readNxJson(workspaceRoot);
// Set this globally to allow plugins to know if they are being called from the project graph creation
global.NX_GRAPH_CREATION = true; global.NX_GRAPH_CREATION = true;
let graphNodes: ConfigurationResult; let graphNodes: ConfigurationResult;
let projectConfigurationsError; let projectConfigurationsError;
try { try {
graphNodes = await retrieveProjectConfigurations(workspaceRoot, nxJson); graphNodes = await retrieveProjectConfigurations(
plugins,
workspaceRoot,
nxJson
);
} catch (e) { } catch (e) {
if (e instanceof ProjectConfigurationsError) { if (e instanceof ProjectConfigurationsError) {
graphNodes = e.partialProjectConfigurationsResult; graphNodes = e.partialProjectConfigurationsResult;
@ -335,7 +341,8 @@ async function createAndSerializeProjectGraph({
fileMap, fileMap,
allWorkspaceFiles, allWorkspaceFiles,
rustReferences, rustReferences,
currentProjectFileMapCache || readFileMapCache() currentProjectFileMapCache || readFileMapCache(),
await getPlugins()
); );
currentProjectFileMapCache = projectFileMapCache; currentProjectFileMapCache = projectFileMapCache;

View File

@ -4,6 +4,7 @@ import { serverLogger } from './logger';
import { serializeResult } from '../socket-utils'; import { serializeResult } from '../socket-utils';
import { deleteDaemonJsonProcessCache } from '../cache'; import { deleteDaemonJsonProcessCache } from '../cache';
import type { Watcher } from '../../native'; import type { Watcher } from '../../native';
import { cleanupPlugins } from './plugins';
export const SERVER_INACTIVITY_TIMEOUT_MS = 10800000 as const; // 10800000 ms = 3 hours export const SERVER_INACTIVITY_TIMEOUT_MS = 10800000 as const; // 10800000 ms = 3 hours
@ -39,6 +40,7 @@ export async function handleServerProcessTermination({
try { try {
server.close(); server.close();
deleteDaemonJsonProcessCache(); deleteDaemonJsonProcessCache();
cleanupPlugins();
if (watcherInstance) { if (watcherInstance) {
await watcherInstance.stop(); await watcherInstance.stop();

View File

@ -47,16 +47,19 @@ export { workspaceLayout } from './config/configuration';
export type { export type {
NxPlugin, NxPlugin,
NxPluginV1,
NxPluginV2, NxPluginV2,
ProjectTargetConfigurator,
CreateNodes, CreateNodes,
CreateNodesFunction, CreateNodesFunction,
CreateNodesResult, CreateNodesResult,
CreateNodesContext, CreateNodesContext,
CreateDependencies, CreateDependencies,
CreateDependenciesContext, CreateDependenciesContext,
} from './utils/nx-plugin'; } from './project-graph/plugins';
export type {
NxPluginV1,
ProjectTargetConfigurator,
} from './utils/nx-plugin.deprecated';
/** /**
* @category Workspace * @category Workspace
@ -71,6 +74,7 @@ export type {
ImplicitJsonSubsetDependency, ImplicitJsonSubsetDependency,
NxJsonConfiguration, NxJsonConfiguration,
PluginConfiguration, PluginConfiguration,
ExpandedPluginConfiguration,
TargetDefaults, TargetDefaults,
NxAffectedConfig, NxAffectedConfig,
} from './config/nx-json'; } from './config/nx-json';

View File

@ -7,6 +7,7 @@ import { readNxJson } from '../../config/nx-json';
import { Executor, ExecutorContext } from '../../config/misc-interfaces'; import { Executor, ExecutorContext } from '../../config/misc-interfaces';
import { retrieveProjectConfigurations } from '../../project-graph/utils/retrieve-workspace-files'; import { retrieveProjectConfigurations } from '../../project-graph/utils/retrieve-workspace-files';
import { ProjectsConfigurations } from '../../config/workspace-json-project-json'; import { ProjectsConfigurations } from '../../config/workspace-json-project-json';
import { loadNxPlugins } from '../../project-graph/plugins/internal-api';
/** /**
* Convert an Nx Executor into an Angular Devkit Builder * Convert an Nx Executor into an Angular Devkit Builder
@ -17,15 +18,22 @@ export function convertNxExecutor(executor: Executor) {
const builderFunction = (options, builderContext) => { const builderFunction = (options, builderContext) => {
const promise = async () => { const promise = async () => {
const nxJsonConfiguration = readNxJson(builderContext.workspaceRoot); const nxJsonConfiguration = readNxJson(builderContext.workspaceRoot);
const [plugins, cleanup] = await loadNxPlugins(
nxJsonConfiguration.plugins,
builderContext.workspaceRoot
);
const projectsConfigurations: ProjectsConfigurations = { const projectsConfigurations: ProjectsConfigurations = {
version: 2, version: 2,
projects: ( projects: (
await retrieveProjectConfigurations( await retrieveProjectConfigurations(
plugins,
builderContext.workspaceRoot, builderContext.workspaceRoot,
nxJsonConfiguration nxJsonConfiguration
) )
).projects, ).projects,
}; };
cleanup();
const context: ExecutorContext = { const context: ExecutorContext = {
root: builderContext.workspaceRoot, root: builderContext.workspaceRoot,
projectName: builderContext.target.project, projectName: builderContext.target.project,

View File

@ -4,7 +4,7 @@ import { basename, join, relative } from 'path';
import { import {
buildProjectConfigurationFromPackageJson, buildProjectConfigurationFromPackageJson,
getGlobPatternsFromPackageManagerWorkspaces, getGlobPatternsFromPackageManagerWorkspaces,
getNxPackageJsonWorkspacesPlugin, createNodes as packageJsonWorkspacesCreateNodes,
} from '../../plugins/package-json-workspaces'; } from '../../plugins/package-json-workspaces';
import { import {
buildProjectFromProjectJson, buildProjectFromProjectJson,
@ -28,6 +28,7 @@ import { readJson, writeJson } from './json';
import { readNxJson } from './nx-json'; import { readNxJson } from './nx-json';
import type { Tree } from '../tree'; import type { Tree } from '../tree';
import { NxPlugin } from '../../project-graph/plugins';
export { readNxJson, updateNxJson } from './nx-json'; export { readNxJson, updateNxJson } from './nx-json';
@ -200,8 +201,8 @@ function readAndCombineAllProjectConfigurations(tree: Tree): {
), ),
]; ];
const projectGlobPatterns = configurationGlobs([ const projectGlobPatterns = configurationGlobs([
{ plugin: ProjectJsonProjectsPlugin }, ProjectJsonProjectsPlugin,
{ plugin: getNxPackageJsonWorkspacesPlugin(tree.root) }, { createNodes: packageJsonWorkspacesCreateNodes },
]); ]);
const globbedFiles = globWithWorkspaceContext(tree.root, projectGlobPatterns); const globbedFiles = globWithWorkspaceContext(tree.root, projectGlobPatterns);
const createdFiles = findCreatedProjectFiles(tree, patterns); const createdFiles = findCreatedProjectFiles(tree, patterns);

View File

@ -4,14 +4,15 @@ import { dirname } from 'path';
import { readJson, writeJson } from '../../generators/utils/json'; import { readJson, writeJson } from '../../generators/utils/json';
import { formatChangedFilesWithPrettierIfAvailable } from '../../generators/internal-utils/format-changed-files-with-prettier-if-available'; import { formatChangedFilesWithPrettierIfAvailable } from '../../generators/internal-utils/format-changed-files-with-prettier-if-available';
import { retrieveProjectConfigurationPaths } from '../../project-graph/utils/retrieve-workspace-files'; import { retrieveProjectConfigurationPaths } from '../../project-graph/utils/retrieve-workspace-files';
import { loadNxPlugins } from '../../utils/nx-plugin'; import { loadNxPlugins } from '../../project-graph/plugins/internal-api';
export default async function (tree: Tree) { export default async function (tree: Tree) {
const nxJson = readNxJson(tree); const nxJson = readNxJson(tree);
const projectFiles = await retrieveProjectConfigurationPaths( const [plugins, cleanup] = await loadNxPlugins(
tree.root, nxJson?.plugins ?? [],
await loadNxPlugins(nxJson?.plugins) tree.root
); );
const projectFiles = retrieveProjectConfigurationPaths(tree.root, plugins);
const projectJsons = projectFiles.filter((f) => f.endsWith('project.json')); const projectJsons = projectFiles.filter((f) => f.endsWith('project.json'));
for (let f of projectJsons) { for (let f of projectJsons) {
@ -22,6 +23,7 @@ export default async function (tree: Tree) {
} }
} }
await formatChangedFilesWithPrettierIfAvailable(tree); await formatChangedFilesWithPrettierIfAvailable(tree);
cleanup();
} }
function toProjectName(directory: string, nxJson: any): string { function toProjectName(directory: string, nxJson: any): string {

View File

@ -9,7 +9,7 @@ import {
CreateDependencies, CreateDependencies,
CreateDependenciesContext, CreateDependenciesContext,
CreateNodes, CreateNodes,
} from '../../utils/nx-plugin'; } from '../../project-graph/plugins';
import { import {
getLockFileDependencies, getLockFileDependencies,
getLockFileName, getLockFileName,

View File

@ -40,7 +40,7 @@ import { readJsonFile } from '../../../utils/fileutils';
import { import {
CreateDependenciesContext, CreateDependenciesContext,
CreateNodesContext, CreateNodesContext,
} from '../../../utils/nx-plugin'; } from '../../../project-graph/plugins';
const YARN_LOCK_FILE = 'yarn.lock'; const YARN_LOCK_FILE = 'yarn.lock';
const NPM_LOCK_FILE = 'package-lock.json'; const NPM_LOCK_FILE = 'package-lock.json';

View File

@ -8,7 +8,7 @@ import { pruneProjectGraph } from './project-graph-pruning';
import { vol } from 'memfs'; import { vol } from 'memfs';
import { ProjectGraph } from '../../../config/project-graph'; import { ProjectGraph } from '../../../config/project-graph';
import { ProjectGraphBuilder } from '../../../project-graph/project-graph-builder'; import { ProjectGraphBuilder } from '../../../project-graph/project-graph-builder';
import { CreateDependenciesContext } from '../../../utils/nx-plugin'; import { CreateDependenciesContext } from '../../../project-graph/plugins';
jest.mock('fs', () => { jest.mock('fs', () => {
const memFs = require('memfs').fs; const memFs = require('memfs').fs;

View File

@ -13,7 +13,7 @@ import {
ProjectGraphExternalNode, ProjectGraphExternalNode,
} from '../../../config/project-graph'; } from '../../../config/project-graph';
import { hashArray } from '../../../hasher/file-hasher'; import { hashArray } from '../../../hasher/file-hasher';
import { CreateDependenciesContext } from '../../../utils/nx-plugin'; import { CreateDependenciesContext } from '../../../project-graph/plugins';
/** /**
* NPM * NPM

View File

@ -11,7 +11,7 @@ import {
ProjectGraphBuilder, ProjectGraphBuilder,
RawProjectGraphDependency, RawProjectGraphDependency,
} from '../../../project-graph/project-graph-builder'; } from '../../../project-graph/project-graph-builder';
import { CreateDependenciesContext } from '../../../utils/nx-plugin'; import { CreateDependenciesContext } from '../../../project-graph/plugins';
jest.mock('fs', () => { jest.mock('fs', () => {
const memFs = require('memfs').fs; const memFs = require('memfs').fs;

View File

@ -25,7 +25,7 @@ import {
ProjectGraphExternalNode, ProjectGraphExternalNode,
} from '../../../config/project-graph'; } from '../../../config/project-graph';
import { hashArray } from '../../../hasher/file-hasher'; import { hashArray } from '../../../hasher/file-hasher';
import { CreateDependenciesContext } from '../../../utils/nx-plugin'; import { CreateDependenciesContext } from '../../../project-graph/plugins';
// we use key => node map to avoid duplicate work when parsing keys // we use key => node map to avoid duplicate work when parsing keys
let keyMap = new Map<string, ProjectGraphExternalNode>(); let keyMap = new Map<string, ProjectGraphExternalNode>();

View File

@ -9,7 +9,7 @@ import { vol } from 'memfs';
import { ProjectGraph } from '../../../config/project-graph'; import { ProjectGraph } from '../../../config/project-graph';
import { PackageJson } from '../../../utils/package-json'; import { PackageJson } from '../../../utils/package-json';
import { ProjectGraphBuilder } from '../../../project-graph/project-graph-builder'; import { ProjectGraphBuilder } from '../../../project-graph/project-graph-builder';
import { CreateDependenciesContext } from '../../../utils/nx-plugin'; import { CreateDependenciesContext } from '../../../project-graph/plugins';
jest.mock('fs', () => { jest.mock('fs', () => {
const memFs = require('memfs').fs; const memFs = require('memfs').fs;

View File

@ -14,7 +14,7 @@ import {
} from '../../../config/project-graph'; } from '../../../config/project-graph';
import { hashArray } from '../../../hasher/file-hasher'; import { hashArray } from '../../../hasher/file-hasher';
import { sortObjectByKeys } from '../../../utils/object-sort'; import { sortObjectByKeys } from '../../../utils/object-sort';
import { CreateDependenciesContext } from '../../../utils/nx-plugin'; import { CreateDependenciesContext } from '../../../project-graph/plugins';
/** /**
* Yarn * Yarn

View File

@ -1,6 +1,6 @@
import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies'; import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies';
import { buildExplicitPackageJsonDependencies } from './explicit-package-json-dependencies'; import { buildExplicitPackageJsonDependencies } from './explicit-package-json-dependencies';
import { CreateDependenciesContext } from '../../../../utils/nx-plugin'; import { CreateDependenciesContext } from '../../../../project-graph/plugins';
import { RawProjectGraphDependency } from '../../../../project-graph/project-graph-builder'; import { RawProjectGraphDependency } from '../../../../project-graph/project-graph-builder';
export function buildExplicitDependencies( export function buildExplicitDependencies(

View File

@ -6,7 +6,7 @@ import { buildExplicitPackageJsonDependencies } from './explicit-package-json-de
import { ProjectGraphProjectNode } from '../../../../config/project-graph'; import { ProjectGraphProjectNode } from '../../../../config/project-graph';
import { ProjectGraphBuilder } from '../../../../project-graph/project-graph-builder'; import { ProjectGraphBuilder } from '../../../../project-graph/project-graph-builder';
import { createFileMap } from '../../../../project-graph/file-map-utils'; import { createFileMap } from '../../../../project-graph/file-map-utils';
import { CreateDependenciesContext } from '../../../../utils/nx-plugin'; import { CreateDependenciesContext } from '../../../../project-graph/plugins';
import { getAllFileDataInContext } from '../../../../utils/workspace-context'; import { getAllFileDataInContext } from '../../../../utils/workspace-context';
describe('explicit package json dependencies', () => { describe('explicit package json dependencies', () => {

View File

@ -9,7 +9,7 @@ import {
} from '../../../../config/workspace-json-project-json'; } from '../../../../config/workspace-json-project-json';
import { NxJsonConfiguration } from '../../../../config/nx-json'; import { NxJsonConfiguration } from '../../../../config/nx-json';
import { PackageJson } from '../../../../utils/package-json'; import { PackageJson } from '../../../../utils/package-json';
import { CreateDependenciesContext } from '../../../../utils/nx-plugin'; import { CreateDependenciesContext } from '../../../../project-graph/plugins';
import { import {
RawProjectGraphDependency, RawProjectGraphDependency,
validateDependency, validateDependency,

View File

@ -1,15 +1,16 @@
import { TempFs } from '../../../../internal-testing-utils/temp-fs'; import { TempFs } from '../../../../internal-testing-utils/temp-fs';
const tempFs = new TempFs('explicit-project-deps'); const tempFs = new TempFs('explicit-project-deps');
import { ProjectGraphBuilder } from '../../../../project-graph/project-graph-builder'; import { ProjectGraphBuilder } from '../../../../project-graph/project-graph-builder';
import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies'; import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies';
import { import {
retrieveProjectConfigurationPaths,
retrieveProjectConfigurations, retrieveProjectConfigurations,
retrieveWorkspaceFiles, retrieveWorkspaceFiles,
} from '../../../../project-graph/utils/retrieve-workspace-files'; } from '../../../../project-graph/utils/retrieve-workspace-files';
import { CreateDependenciesContext } from '../../../../utils/nx-plugin'; import { CreateDependenciesContext } from '../../../../project-graph/plugins';
import { setupWorkspaceContext } from '../../../../utils/workspace-context'; import { setupWorkspaceContext } from '../../../../utils/workspace-context';
import { loadNxPlugins } from '../../../../project-graph/plugins/internal-api';
// projectName => tsconfig import path // projectName => tsconfig import path
const dependencyProjectNamesToImportPaths = { const dependencyProjectNamesToImportPaths = {
@ -564,10 +565,13 @@ async function createContext(
setupWorkspaceContext(tempFs.tempDir); setupWorkspaceContext(tempFs.tempDir);
const [plugins, cleanup] = await loadNxPlugins([], tempFs.tempDir);
const { projects, projectRootMap } = await retrieveProjectConfigurations( const { projects, projectRootMap } = await retrieveProjectConfigurations(
plugins,
tempFs.tempDir, tempFs.tempDir,
nxJson nxJson
); );
cleanup();
const { fileMap } = await retrieveWorkspaceFiles( const { fileMap } = await retrieveWorkspaceFiles(
tempFs.tempDir, tempFs.tempDir,

View File

@ -6,7 +6,7 @@ import {
import { join, relative } from 'path'; import { join, relative } from 'path';
import { workspaceRoot } from '../../../../utils/workspace-root'; import { workspaceRoot } from '../../../../utils/workspace-root';
import { normalizePath } from '../../../../utils/path'; import { normalizePath } from '../../../../utils/path';
import { CreateDependenciesContext } from '../../../../utils/nx-plugin'; import { CreateDependenciesContext } from '../../../../project-graph/plugins';
import { import {
RawProjectGraphDependency, RawProjectGraphDependency,
validateDependency, validateDependency,

View File

@ -8,23 +8,23 @@ import { toProjectName } from '../../config/workspaces';
import { readJsonFile, readYamlFile } from '../../utils/fileutils'; import { readJsonFile, readYamlFile } from '../../utils/fileutils';
import { combineGlobPatterns } from '../../utils/globs'; import { combineGlobPatterns } from '../../utils/globs';
import { NX_PREFIX } from '../../utils/logger'; import { NX_PREFIX } from '../../utils/logger';
import { NxPluginV2 } from '../../utils/nx-plugin';
import { output } from '../../utils/output'; import { output } from '../../utils/output';
import { import {
PackageJson, PackageJson,
readTargetsFromPackageJson, readTargetsFromPackageJson,
} from '../../utils/package-json'; } from '../../utils/package-json';
import { joinPathFragments } from '../../utils/path'; import { joinPathFragments } from '../../utils/path';
import { workspaceRoot } from '../../utils/workspace-root';
import { CreateNodes } from '../../project-graph/plugins';
export function getNxPackageJsonWorkspacesPlugin(root: string): NxPluginV2 { const readJson = (f) => readJsonFile(join(workspaceRoot, f));
const readJson = (f) => readJsonFile(join(root, f)); const patterns = getGlobPatternsFromPackageManagerWorkspaces(
const patterns = getGlobPatternsFromPackageManagerWorkspaces(root, readJson); workspaceRoot,
readJson
);
// If the user only specified a negative pattern, we should find all package.json
// files and only return those that don't match a negative pattern.
const negativePatterns = patterns.filter((p) => p.startsWith('!')); const negativePatterns = patterns.filter((p) => p.startsWith('!'));
let positivePatterns = patterns.filter((p) => !p.startsWith('!')); const positivePatterns = patterns.filter((p) => !p.startsWith('!'));
if ( if (
// There are some negative patterns // There are some negative patterns
negativePatterns.length > 0 && negativePatterns.length > 0 &&
@ -35,21 +35,16 @@ export function getNxPackageJsonWorkspacesPlugin(root: string): NxPluginV2 {
) { ) {
positivePatterns.push('**/package.json'); positivePatterns.push('**/package.json');
} }
export const createNodes: CreateNodes = [
return {
name: 'nx/core/package-json-workspaces',
createNodes: [
combineGlobPatterns(positivePatterns), combineGlobPatterns(positivePatterns),
(p) => { (p, _, { workspaceRoot }) => {
if (!negativePatterns.some((negative) => minimatch(p, negative))) { if (!negativePatterns.some((negative) => minimatch(p, negative))) {
return createNodeFromPackageJson(p, root); return createNodeFromPackageJson(p, workspaceRoot);
} }
// A negative pattern matched, so we should not create a node for this package.json // A negative pattern matched, so we should not create a node for this package.json
return {}; return {};
}, },
], ];
};
}
export function createNodeFromPackageJson(pkgJsonPath: string, root: string) { export function createNodeFromPackageJson(pkgJsonPath: string, root: string) {
const json: PackageJson = readJsonFile(join(root, pkgJsonPath)); const json: PackageJson = readJsonFile(join(root, pkgJsonPath));

View File

@ -1 +1,2 @@
export * from './create-nodes'; export * from './create-nodes';
export const name = 'nx/core/package-json-workspaces';

View File

@ -3,7 +3,7 @@ import * as memfs from 'memfs';
import '../../../internal-testing-utils/mock-fs'; import '../../../internal-testing-utils/mock-fs';
import { PackageJsonProjectsNextToProjectJsonPlugin } from './package-json-next-to-project-json'; import { PackageJsonProjectsNextToProjectJsonPlugin } from './package-json-next-to-project-json';
import { CreateNodesContext } from '../../../utils/nx-plugin'; import { CreateNodesContext } from '../../../project-graph/plugins';
const { createNodes } = PackageJsonProjectsNextToProjectJsonPlugin; const { createNodes } = PackageJsonProjectsNextToProjectJsonPlugin;
describe('nx project.json plugin', () => { describe('nx project.json plugin', () => {

View File

@ -1,6 +1,6 @@
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { NxPluginV2 } from '../../../utils/nx-plugin'; import { NxPluginV2 } from '../../../project-graph/plugins';
import { readJsonFile } from '../../../utils/fileutils'; import { readJsonFile } from '../../../utils/fileutils';
import { ProjectConfiguration } from '../../../config/workspace-json-project-json'; import { ProjectConfiguration } from '../../../config/workspace-json-project-json';
import { import {
@ -33,6 +33,8 @@ export const PackageJsonProjectsNextToProjectJsonPlugin: NxPluginV2 = {
], ],
}; };
export default PackageJsonProjectsNextToProjectJsonPlugin;
function createProjectFromPackageJsonNextToProjectJson( function createProjectFromPackageJsonNextToProjectJson(
projectJsonPath: string, projectJsonPath: string,
workspaceRoot: string workspaceRoot: string

View File

@ -3,7 +3,7 @@ import * as memfs from 'memfs';
import '../../../internal-testing-utils/mock-fs'; import '../../../internal-testing-utils/mock-fs';
import { ProjectJsonProjectsPlugin } from './project-json'; import { ProjectJsonProjectsPlugin } from './project-json';
import { CreateNodesContext } from '../../../utils/nx-plugin'; import { CreateNodesContext } from '../../../project-graph/plugins';
const { createNodes } = ProjectJsonProjectsPlugin; const { createNodes } = ProjectJsonProjectsPlugin;
describe('nx project.json plugin', () => { describe('nx project.json plugin', () => {

View File

@ -3,7 +3,7 @@ import { dirname, join } from 'node:path';
import { ProjectConfiguration } from '../../../config/workspace-json-project-json'; import { ProjectConfiguration } from '../../../config/workspace-json-project-json';
import { toProjectName } from '../../../config/workspaces'; import { toProjectName } from '../../../config/workspaces';
import { readJsonFile } from '../../../utils/fileutils'; import { readJsonFile } from '../../../utils/fileutils';
import { NxPluginV2 } from '../../../utils/nx-plugin'; import { NxPluginV2 } from '../../../project-graph/plugins';
export const ProjectJsonProjectsPlugin: NxPluginV2 = { export const ProjectJsonProjectsPlugin: NxPluginV2 = {
name: 'nx/core/project-json', name: 'nx/core/project-json',
@ -23,6 +23,8 @@ export const ProjectJsonProjectsPlugin: NxPluginV2 = {
], ],
}; };
export default ProjectJsonProjectsPlugin;
export function buildProjectFromProjectJson( export function buildProjectFromProjectJson(
json: Partial<ProjectConfiguration>, json: Partial<ProjectConfiguration>,
path: string path: string

View File

@ -0,0 +1,18 @@
/**
* This marks that a target provides information which should modify a target already registered
* on the project via other plugins. If the target has not already been registered, and this symbol is true,
* the information provided by it will be discarded.
*
* NOTE: This cannot be a symbol, as they are not serialized in JSON the communication
* between the plugin-worker and the main process.
*/
export const ONLY_MODIFIES_EXISTING_TARGET = 'NX_ONLY_MODIFIES_EXISTING_TARGET';
/**
* This is used to override the source file for the target defaults plugin.
* This allows the plugin to use the project files as the context, but point to nx.json as the source file.
*
* NOTE: This cannot be a symbol, as they are not serialized in JSON the communication
* between the plugin-worker and the main process.
*/
export const OVERRIDE_SOURCE_FILE = 'NX_OVERRIDE_SOURCE_FILE';

View File

@ -3,7 +3,7 @@ import * as memfs from 'memfs';
import '../../../src/internal-testing-utils/mock-fs'; import '../../../src/internal-testing-utils/mock-fs';
import { getTargetInfo, TargetDefaultsPlugin } from './target-defaults-plugin'; import { getTargetInfo, TargetDefaultsPlugin } from './target-defaults-plugin';
import { CreateNodesContext } from '../../utils/nx-plugin'; import { CreateNodesContext } from '../../project-graph/plugins';
const { const {
createNodes: [, createNodesFn], createNodes: [, createNodesFn],
} = TargetDefaultsPlugin; } = TargetDefaultsPlugin;

View File

@ -8,31 +8,13 @@ import {
} from '../../config/workspace-json-project-json'; } from '../../config/workspace-json-project-json';
import { readJsonFile } from '../../utils/fileutils'; import { readJsonFile } from '../../utils/fileutils';
import { combineGlobPatterns } from '../../utils/globs'; import { combineGlobPatterns } from '../../utils/globs';
import { NxPluginV2 } from '../../utils/nx-plugin'; import { NxPluginV2 } from '../../project-graph/plugins';
import { import {
PackageJson, PackageJson,
readTargetsFromPackageJson, readTargetsFromPackageJson,
} from '../../utils/package-json'; } from '../../utils/package-json';
import { getGlobPatternsFromPackageManagerWorkspaces } from '../package-json-workspaces'; import { getGlobPatternsFromPackageManagerWorkspaces } from '../package-json-workspaces';
import { ONLY_MODIFIES_EXISTING_TARGET, OVERRIDE_SOURCE_FILE } from './symbols';
/**
* This marks that a target provides information which should modify a target already registered
* on the project via other plugins. If the target has not already been registered, and this symbol is true,
* the information provided by it will be discarded.
*
* NOTE: This cannot be a symbol, as they are not serialized in JSON the communication
* between the plugin-worker and the main process.
*/
export const ONLY_MODIFIES_EXISTING_TARGET = 'NX_ONLY_MODIFIES_EXISTING_TARGET';
/**
* This is used to override the source file for the target defaults plugin.
* This allows the plugin to use the project files as the context, but point to nx.json as the source file.
*
* NOTE: This cannot be a symbol, as they are not serialized in JSON the communication
* between the plugin-worker and the main process.
*/
export const OVERRIDE_SOURCE_FILE = 'NX_OVERRIDE_SOURCE_FILE';
export const TargetDefaultsPlugin: NxPluginV2 = { export const TargetDefaultsPlugin: NxPluginV2 = {
name: 'nx/core/target-defaults', name: 'nx/core/target-defaults',
@ -127,6 +109,8 @@ export const TargetDefaultsPlugin: NxPluginV2 = {
], ],
}; };
export default TargetDefaultsPlugin;
function getExecutorToTargetMap( function getExecutorToTargetMap(
packageJsonTargets: Record<string, TargetConfiguration>, packageJsonTargets: Record<string, TargetConfiguration>,
projectJsonTargets: Record<string, TargetConfiguration> projectJsonTargets: Record<string, TargetConfiguration>

View File

@ -1,7 +1,7 @@
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 * as nxPlugin from '../../../utils/nx-plugin'; import * as nxPlugin from '../../../project-graph/plugins';
import { DeletedFileChange } from '../../file-utils'; import { DeletedFileChange } from '../../file-utils';
import { getTouchedProjectsFromProjectGlobChanges } from './project-glob-changes'; import { getTouchedProjectsFromProjectGlobChanges } from './project-glob-changes';

View File

@ -1,24 +1,16 @@
import { TouchedProjectLocator } from '../affected-project-graph-models'; import { TouchedProjectLocator } from '../affected-project-graph-models';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import { workspaceRoot } from '../../../utils/workspace-root'; import { workspaceRoot } from '../../../utils/workspace-root';
import { getNxRequirePaths } from '../../../utils/installation-directory';
import { join } from 'path'; import { join } from 'path';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { configurationGlobs } from '../../utils/retrieve-workspace-files'; import { configurationGlobs } from '../../utils/retrieve-workspace-files';
import { loadNxPlugins } from '../../../utils/nx-plugin'; import { loadNxPlugins } from '../../plugins/internal-api';
import { combineGlobPatterns } from '../../../utils/globs'; import { combineGlobPatterns } from '../../../utils/globs';
export const getTouchedProjectsFromProjectGlobChanges: TouchedProjectLocator = export const getTouchedProjectsFromProjectGlobChanges: TouchedProjectLocator =
async (touchedFiles, projectGraphNodes, nxJson): Promise<string[]> => { async (touchedFiles, projectGraphNodes, nxJson): Promise<string[]> => {
const globPattern = combineGlobPatterns( const [plugins] = await loadNxPlugins(nxJson?.plugins ?? [], workspaceRoot);
configurationGlobs( const globPattern = combineGlobPatterns(configurationGlobs(plugins));
await loadNxPlugins(
nxJson?.plugins,
getNxRequirePaths(workspaceRoot),
workspaceRoot
)
)
);
const touchedProjects = new Set<string>(); const touchedProjects = new Set<string>();
for (const touchedFile of touchedFiles) { for (const touchedFile of touchedFiles) {

View File

@ -13,12 +13,9 @@ 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 { import { LoadedNxPlugin } from './plugins/internal-api';
CreateDependenciesContext, import { isNxPluginV1, isNxPluginV2 } from './plugins/utils';
isNxPluginV1, import { CreateDependenciesContext } from './plugins';
isNxPluginV2,
loadNxPlugins,
} from '../utils/nx-plugin';
import { getRootTsConfigPath } from '../plugins/js/utils/typescript'; import { getRootTsConfigPath } from '../plugins/js/utils/typescript';
import { import {
FileMap, FileMap,
@ -32,9 +29,8 @@ import { ProjectConfiguration } from '../config/workspace-json-project-json';
import { readNxJson } from '../config/configuration'; import { readNxJson } from '../config/configuration';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { PackageJson } from '../utils/package-json'; import { PackageJson } from '../utils/package-json';
import { getNxRequirePaths } from '../utils/installation-directory';
import { output } from '../utils/output'; import { output } from '../utils/output';
import { ExternalObject, NxWorkspaceFilesExternals } from '../native'; import { NxWorkspaceFilesExternals } from '../native';
let storedFileMap: FileMap | null = null; let storedFileMap: FileMap | null = null;
let storedAllWorkspaceFiles: FileData[] | null = null; let storedAllWorkspaceFiles: FileData[] | null = null;
@ -69,7 +65,8 @@ export async function buildProjectGraphUsingProjectFileMap(
fileMap: FileMap, fileMap: FileMap,
allWorkspaceFiles: FileData[], allWorkspaceFiles: FileData[],
rustReferences: NxWorkspaceFilesExternals, rustReferences: NxWorkspaceFilesExternals,
fileMapCache: FileMapCache | null fileMapCache: FileMapCache | null,
plugins: LoadedNxPlugin[]
): Promise<{ ): Promise<{
projectGraph: ProjectGraph; projectGraph: ProjectGraph;
projectFileMapCache: FileMapCache; projectFileMapCache: FileMapCache;
@ -118,7 +115,8 @@ export async function buildProjectGraphUsingProjectFileMap(
externalNodes, externalNodes,
context, context,
cachedFileData, cachedFileData,
projectGraphVersion projectGraphVersion,
plugins
); );
const projectFileMapCache = createProjectFileMapCache( const projectFileMapCache = createProjectFileMapCache(
nxJson, nxJson,
@ -160,7 +158,8 @@ async function buildProjectGraphUsingContext(
knownExternalNodes: Record<string, ProjectGraphExternalNode>, knownExternalNodes: Record<string, ProjectGraphExternalNode>,
ctx: CreateDependenciesContext, ctx: CreateDependenciesContext,
cachedFileData: CachedFileData, cachedFileData: CachedFileData,
projectGraphVersion: string projectGraphVersion: string,
plugins: LoadedNxPlugin[]
) { ) {
performance.mark('build project graph:start'); performance.mark('build project graph:start');
@ -176,7 +175,11 @@ async function buildProjectGraphUsingContext(
let updatedGraph; let updatedGraph;
let error; let error;
try { try {
updatedGraph = await updateProjectGraphWithPlugins(ctx, initProjectGraph); updatedGraph = await updateProjectGraphWithPlugins(
ctx,
initProjectGraph,
plugins
);
} catch (e) { } catch (e) {
if (e instanceof CreateDependenciesError) { if (e instanceof CreateDependenciesError) {
updatedGraph = e.partialProjectGraph; updatedGraph = e.partialProjectGraph;
@ -248,17 +251,12 @@ function createContext(
async function updateProjectGraphWithPlugins( async function updateProjectGraphWithPlugins(
context: CreateDependenciesContext, context: CreateDependenciesContext,
initProjectGraph: ProjectGraph initProjectGraph: ProjectGraph,
plugins: LoadedNxPlugin[]
) { ) {
const plugins = await loadNxPlugins(
context.nxJsonConfiguration?.plugins,
getNxRequirePaths(),
context.workspaceRoot,
context.projects
);
let graph = initProjectGraph; let graph = initProjectGraph;
const errors: Array<ProcessDependenciesError | ProcessProjectGraphError> = []; const errors: Array<ProcessDependenciesError | ProcessProjectGraphError> = [];
for (const { plugin } of plugins) { for (const plugin of plugins) {
try { try {
if ( if (
isNxPluginV1(plugin) && isNxPluginV1(plugin) &&
@ -309,14 +307,14 @@ async function updateProjectGraphWithPlugins(
); );
const createDependencyPlugins = plugins.filter( const createDependencyPlugins = plugins.filter(
({ plugin }) => isNxPluginV2(plugin) && plugin.createDependencies (plugin) => isNxPluginV2(plugin) && plugin.createDependencies
); );
await Promise.all( await Promise.all(
createDependencyPlugins.map(async ({ plugin, options }) => { createDependencyPlugins.map(async (plugin) => {
performance.mark(`${plugin.name}:createDependencies - start`); performance.mark(`${plugin.name}:createDependencies - start`);
try { try {
const dependencies = await plugin.createDependencies(options, { const dependencies = await plugin.createDependencies({
...context, ...context,
}); });

View File

@ -0,0 +1,98 @@
import { CreateNodesResultWithContext } from './plugins/internal-api';
import { ConfigurationResult } from './utils/project-configuration-utils';
export class ProjectConfigurationsError extends Error {
constructor(
public readonly errors: Array<MergeNodesError | CreateNodesError>,
public readonly partialProjectConfigurationsResult: ConfigurationResult
) {
super('Failed to create project configurations');
this.name = this.constructor.name;
}
}
export class CreateNodesError extends Error {
file: string;
pluginName: string;
constructor({
file,
pluginName,
error,
}: {
file: string;
pluginName: string;
error: Error;
}) {
const msg = `The "${pluginName}" plugin threw an error while creating nodes from ${file}:`;
super(msg, { cause: error });
this.name = this.constructor.name;
this.file = file;
this.pluginName = pluginName;
this.stack = `${this.message}\n ${error.stack.split('\n').join('\n ')}`;
}
}
export class AggregateCreateNodesError extends Error {
constructor(
public readonly pluginName: string,
public readonly errors: Array<CreateNodesError>,
public readonly partialResults: Array<CreateNodesResultWithContext>
) {
super('Failed to create nodes');
this.name = this.constructor.name;
}
}
export class MergeNodesError extends Error {
file: string;
pluginName: string;
constructor({
file,
pluginName,
error,
}: {
file: string;
pluginName: string;
error: Error;
}) {
const msg = `The nodes created from ${file} by the "${pluginName}" could not be merged into the project graph:`;
super(msg, { cause: error });
this.name = this.constructor.name;
this.file = file;
this.pluginName = pluginName;
this.stack = `${this.message}\n ${error.stack.split('\n').join('\n ')}`;
}
}
export function isCreateNodesError(e: unknown): e is CreateNodesError {
return (
e instanceof CreateNodesError ||
(typeof e === 'object' &&
'name' in e &&
e?.name === CreateNodesError.prototype.name)
);
}
export function isAggregateCreateNodesError(
e: unknown
): e is AggregateCreateNodesError {
return (
e instanceof AggregateCreateNodesError ||
(typeof e === 'object' &&
'name' in e &&
e?.name === AggregateCreateNodesError.prototype.name)
);
}
export function isMergeNodesError(e: unknown): e is MergeNodesError {
return (
e instanceof MergeNodesError ||
(typeof e === 'object' &&
'name' in e &&
e?.name === MergeNodesError.prototype.name)
);
}

View File

@ -27,7 +27,7 @@ import { getDefaultPluginsSync } from '../utils/nx-plugin.deprecated';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import { CreateNodesResult } from '../devkit-exports'; import { CreateNodesResult } from '../devkit-exports';
import { PackageJsonProjectsNextToProjectJsonPlugin } from '../plugins/project-json/build-nodes/package-json-next-to-project-json'; import { PackageJsonProjectsNextToProjectJsonPlugin } from '../plugins/project-json/build-nodes/package-json-next-to-project-json';
import { LoadedNxPlugin } from '../utils/nx-plugin'; import { LoadedNxPlugin } from './plugins/internal-api';
export interface Change { export interface Change {
type: string; type: string;
@ -186,15 +186,15 @@ function getProjectsSyncNoInference(root: string, nxJson: NxJsonConfiguration) {
root, root,
getDefaultPluginsSync(root) getDefaultPluginsSync(root)
); );
const plugins: LoadedNxPlugin[] = [ const plugins = [
{ plugin: PackageJsonProjectsNextToProjectJsonPlugin }, PackageJsonProjectsNextToProjectJsonPlugin,
...getDefaultPluginsSync(root), ...getDefaultPluginsSync(root),
]; ];
const projectRootMap: Map<string, ProjectConfiguration> = new Map(); const projectRootMap: Map<string, ProjectConfiguration> = new Map();
// We iterate over plugins first - this ensures that plugins specified first take precedence. // We iterate over plugins first - this ensures that plugins specified first take precedence.
for (const { plugin, options } of plugins) { for (const plugin of plugins) {
const [pattern, createNodes] = plugin.createNodes ?? []; const [pattern, createNodes] = plugin.createNodes ?? [];
if (!pattern) { if (!pattern) {
continue; continue;
@ -204,11 +204,15 @@ function getProjectsSyncNoInference(root: string, nxJson: NxJsonConfiguration) {
); );
for (const file of matchingConfigFiles) { for (const file of matchingConfigFiles) {
if (minimatch(file, pattern, { dot: true })) { if (minimatch(file, pattern, { dot: true })) {
let r = createNodes(file, options, { let r = createNodes(
file,
{},
{
nxJsonConfiguration: nxJson, nxJsonConfiguration: nxJson,
workspaceRoot: root, workspaceRoot: root,
configFiles: matchingConfigFiles, configFiles: matchingConfigFiles,
}) as CreateNodesResult; }
) as CreateNodesResult;
for (const node in r.projects) { for (const node in r.projects) {
const project = { const project = {
root: node, root: node,

View File

@ -0,0 +1,3 @@
export * from './public-api';
export { readPluginPackageJson, registerPluginTSTranspiler } from './loader';

View File

@ -0,0 +1,144 @@
// 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 { NxPluginV1 } from '../../utils/nx-plugin.deprecated';
import { shouldMergeAngularProjects } from '../../adapter/angular-json';
import {
CreateDependencies,
CreateDependenciesContext,
CreateNodesContext,
CreateNodesResult,
NxPluginV2,
} from './public-api';
import { ProjectGraphProcessor } from '../../config/project-graph';
import { runCreateNodesInParallel } from './utils';
import { loadNxPluginInIsolation } from './isolation';
import { loadNxPlugin, unregisterPluginTSTranspiler } from './loader';
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: CreateNodesContext
) => Promise<CreateNodesResultWithContext[]>
];
readonly createDependencies?: (
context: CreateDependenciesContext
) => ReturnType<CreateDependencies>;
readonly processProjectGraph?: ProjectGraphProcessor;
readonly options?: unknown;
readonly include?: string[];
readonly exclude?: string[];
constructor(plugin: NormalizedPlugin, 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) {
this.createNodes = [
plugin.createNodes[0],
(files, context) =>
runCreateNodesInParallel(files, plugin, this.options, context),
];
}
if (plugin.createDependencies) {
this.createDependencies = (context) =>
plugin.createDependencies(this.options, context);
}
this.processProjectGraph = plugin.processProjectGraph;
}
}
export type CreateNodesResultWithContext = CreateNodesResult & {
file: string;
pluginName: string;
};
export type NormalizedPlugin = NxPluginV2 &
Pick<NxPluginV1, 'processProjectGraph'>;
// Short lived cache (cleared between cmd runs)
// holding resolved nx plugin objects.
// Allows loaded plugins to not be reloaded when
// referenced multiple times.
export const nxPluginCache: Map<
unknown,
[Promise<LoadedNxPlugin>, () => void]
> = new Map();
export async function loadNxPlugins(
plugins: PluginConfiguration[],
root = workspaceRoot
): Promise<[LoadedNxPlugin[], () => void]> {
const result: Promise<LoadedNxPlugin>[] = [];
const loadingMethod =
process.env.NX_ISOLATE_PLUGINS === 'true'
? loadNxPluginInIsolation
: loadNxPlugin;
plugins = await normalizePlugins(plugins, root);
const cleanupFunctions: Array<() => void> = [];
for (const plugin of plugins) {
const [loadedPluginPromise, cleanup] = loadingMethod(plugin, root);
result.push(loadedPluginPromise);
cleanupFunctions.push(cleanup);
}
return [
await Promise.all(result),
() => {
for (const fn of cleanupFunctions) {
fn();
}
if (unregisterPluginTSTranspiler) {
unregisterPluginTSTranspiler();
}
},
] as const;
}
async function normalizePlugins(plugins: PluginConfiguration[], root: string) {
plugins ??= [];
return [
// This plugin adds targets that we want to be able to overwrite
// in any user-land plugin, so it has to be first :).
join(
__dirname,
'../../plugins/project-json/build-nodes/package-json-next-to-project-json'
),
...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'),
join(__dirname, '../../plugins/target-defaults/target-defaults-plugin'),
...(shouldMergeAngularProjects(root, false)
? [join(__dirname, '../../adapter/angular-json')]
: []),
join(__dirname, '../../plugins/package-json-workspaces'),
join(__dirname, '../../plugins/project-json/build-nodes/project-json'),
];
}

View File

@ -0,0 +1,24 @@
import { workspaceRoot } from '../../../utils/workspace-root';
import { PluginConfiguration } from '../../../config/nx-json';
import { LoadedNxPlugin } from '../internal-api';
import { loadRemoteNxPlugin } from './plugin-pool';
const remotePluginCache = new Map<
string,
[Promise<LoadedNxPlugin>, () => void]
>();
export function loadNxPluginInIsolation(
plugin: PluginConfiguration,
root = workspaceRoot
): [Promise<LoadedNxPlugin>, () => void] {
const cacheKey = JSON.stringify(plugin);
if (remotePluginCache.has(cacheKey)) {
return remotePluginCache.get(cacheKey);
}
const [loadingPlugin, cleanup] = loadRemoteNxPlugin(plugin, root);
remotePluginCache.set(cacheKey, [loadingPlugin, cleanup]);
return [loadingPlugin, cleanup];
}

View File

@ -0,0 +1,153 @@
import {
ProjectGraph,
ProjectGraphProcessorContext,
} from '../../../config/project-graph';
import { PluginConfiguration } from '../../../config/nx-json';
import { CreateDependenciesContext, CreateNodesContext } from '../public-api';
import { LoadedNxPlugin } from '../internal-api';
export interface PluginWorkerLoadMessage {
type: 'load';
payload: {
plugin: PluginConfiguration;
root: string;
};
}
export interface PluginWorkerLoadResult {
type: 'load-result';
payload:
| {
name: string;
createNodesPattern: string;
hasCreateDependencies: boolean;
hasProcessProjectGraph: boolean;
success: true;
}
| {
success: false;
error: string;
};
}
export interface PluginWorkerCreateNodesMessage {
type: 'createNodes';
payload: {
configFiles: string[];
context: CreateNodesContext;
tx: string;
};
}
export interface PluginWorkerCreateNodesResult {
type: 'createNodesResult';
payload:
| {
success: true;
result: Awaited<ReturnType<LoadedNxPlugin['createNodes'][1]>>;
tx: string;
}
| {
success: false;
error: string;
tx: string;
};
}
export interface PluginCreateDependenciesMessage {
type: 'createDependencies';
payload: {
context: CreateDependenciesContext;
tx: string;
};
}
export interface PluginCreateDependenciesResult {
type: 'createDependenciesResult';
payload:
| {
dependencies: ReturnType<LoadedNxPlugin['createDependencies']>;
success: true;
tx: string;
}
| {
success: false;
error: string;
tx: string;
};
}
export interface PluginWorkerProcessProjectGraphMessage {
type: 'processProjectGraph';
payload: {
graph: ProjectGraph;
ctx: ProjectGraphProcessorContext;
tx: string;
};
}
export interface PluginWorkerProcessProjectGraphResult {
type: 'processProjectGraphResult';
payload:
| {
graph: ProjectGraph;
success: true;
tx: string;
}
| {
success: false;
error: string;
tx: string;
};
}
export type PluginWorkerMessage =
| PluginWorkerLoadMessage
| PluginWorkerCreateNodesMessage
| PluginCreateDependenciesMessage
| PluginWorkerProcessProjectGraphMessage;
export type PluginWorkerResult =
| PluginWorkerLoadResult
| PluginWorkerCreateNodesResult
| PluginCreateDependenciesResult
| PluginWorkerProcessProjectGraphResult;
type MaybePromise<T> = T | Promise<T>;
// The handler can return a message to be sent back to the process from which the message originated
type MessageHandlerReturn<T extends PluginWorkerMessage | PluginWorkerResult> =
T extends PluginWorkerResult
? MaybePromise<PluginWorkerMessage | void>
: MaybePromise<PluginWorkerResult | void>;
// Takes a message and a map of handlers and calls the appropriate handler
// type safe and requires all handlers to be handled
export async function consumeMessage<
T extends PluginWorkerMessage | PluginWorkerResult
>(
raw: string | T,
handlers: {
[K in T['type']]: (
// Extract restricts the type of payload to the payload of the message with the type K
payload: Extract<T, { type: K }>['payload']
) => MessageHandlerReturn<T>;
}
) {
const message: T = typeof raw === 'string' ? JSON.parse(raw) : raw;
const handler = handlers[message.type];
if (handler) {
const response = await handler(message.payload);
if (response) {
process.send!(createMessage(response));
}
} else {
throw new Error(`Unhandled message type: ${message.type}`);
}
}
export function createMessage(
message: PluginWorkerMessage | PluginWorkerResult
): string {
return JSON.stringify(message);
}

View File

@ -0,0 +1,250 @@
import { ChildProcess, fork } from 'child_process';
import path = require('path');
import { PluginConfiguration } from '../../../config/nx-json';
// TODO (@AgentEnder): After scoped verbose logging is implemented, re-add verbose logs here.
// import { logger } from '../../utils/logger';
import { LoadedNxPlugin, nxPluginCache } from '../internal-api';
import { PluginWorkerResult, consumeMessage, createMessage } from './messaging';
const cleanupFunctions = new Set<() => void>();
const pluginNames = new Map<ChildProcess, string>();
interface PendingPromise {
promise: Promise<unknown>;
resolver: (result: any) => void;
rejector: (err: any) => void;
}
export function loadRemoteNxPlugin(
plugin: PluginConfiguration,
root: string
): [Promise<LoadedNxPlugin>, () => void] {
// this should only really be true when running unit tests within
// the Nx repo. We still need to start the worker in this case,
// but its typescript.
const isWorkerTypescript = path.extname(__filename) === '.ts';
const workerPath = path.join(__dirname, 'plugin-worker');
const worker = fork(workerPath, [], {
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
env: {
...process.env,
...(isWorkerTypescript
? {
// Ensures that the worker uses the same tsconfig as the main process
TS_NODE_PROJECT: path.join(__dirname, '../../../tsconfig.lib.json'),
}
: {}),
},
execArgv: [
...process.execArgv,
// If the worker is typescript, we need to register ts-node
...(isWorkerTypescript ? ['-r', 'ts-node/register'] : []),
],
});
worker.send(createMessage({ type: 'load', payload: { plugin, root } }));
// logger.verbose(`[plugin-worker] started worker: ${worker.pid}`);
const pendingPromises = new Map<string, PendingPromise>();
const exitHandler = createWorkerExitHandler(worker, pendingPromises);
const cleanupFunction = () => {
worker.off('exit', exitHandler);
shutdownPluginWorker(worker, pendingPromises);
};
cleanupFunctions.add(cleanupFunction);
return [
new Promise<LoadedNxPlugin>((res, rej) => {
worker.on(
'message',
createWorkerHandler(worker, pendingPromises, res, rej)
);
worker.on('exit', exitHandler);
}),
() => {
cleanupFunction();
cleanupFunctions.delete(cleanupFunction);
},
] as const;
}
async function shutdownPluginWorker(
worker: ChildProcess,
pendingPromises: Map<string, PendingPromise>
) {
// Clears the plugin cache so no refs to the workers are held
nxPluginCache.clear();
// logger.verbose(`[plugin-pool] starting worker shutdown`);
// Other things may be interacting with the worker.
// Wait for all pending promises to be done before killing the worker
await Promise.all(
Array.from(pendingPromises.values()).map(({ promise }) => promise)
);
worker.kill('SIGINT');
}
/**
* Creates a message handler for the given worker.
* @param worker Instance of plugin-worker
* @param pending Set of pending promises
* @param onload Resolver for RemotePlugin promise
* @param onloadError Rejecter for RemotePlugin promise
* @returns Function to handle messages from the worker
*/
function createWorkerHandler(
worker: ChildProcess,
pending: Map<string, PendingPromise>,
onload: (plugin: LoadedNxPlugin) => void,
onloadError: (err?: unknown) => void
) {
let pluginName: string;
return function (message: string) {
const parsed = JSON.parse(message);
// logger.verbose(
// `[plugin-pool] received message: ${parsed.type} from ${
// pluginName ?? worker.pid
// }`
// );
consumeMessage<PluginWorkerResult>(parsed, {
'load-result': (result) => {
if (result.success) {
const { name, createNodesPattern } = result;
pluginName = name;
pluginNames.set(worker, pluginName);
onload({
name,
createNodes: createNodesPattern
? [
createNodesPattern,
(configFiles, ctx) => {
const tx = pluginName + ':createNodes:' + performance.now();
return registerPendingPromise(tx, pending, () => {
worker.send(
createMessage({
type: 'createNodes',
payload: { configFiles, context: ctx, tx },
})
);
});
},
]
: undefined,
createDependencies: result.hasCreateDependencies
? (ctx) => {
const tx =
pluginName + ':createDependencies:' + performance.now();
return registerPendingPromise(tx, pending, () => {
worker.send(
createMessage({
type: 'createDependencies',
payload: { context: ctx, tx },
})
);
});
}
: undefined,
processProjectGraph: result.hasProcessProjectGraph
? (graph, ctx) => {
const tx =
pluginName + ':processProjectGraph:' + performance.now();
return registerPendingPromise(tx, pending, () => {
worker.send(
createMessage({
type: 'processProjectGraph',
payload: { graph, ctx, tx },
})
);
});
}
: undefined,
});
} else if (result.success === false) {
onloadError(result.error);
}
},
createDependenciesResult: ({ tx, ...result }) => {
const { resolver, rejector } = pending.get(tx);
if (result.success) {
resolver(result.dependencies);
} else if (result.success === false) {
rejector(result.error);
}
},
createNodesResult: ({ tx, ...result }) => {
const { resolver, rejector } = pending.get(tx);
if (result.success) {
resolver(result.result);
} else if (result.success === false) {
rejector(result.error);
}
},
processProjectGraphResult: ({ tx, ...result }) => {
const { resolver, rejector } = pending.get(tx);
if (result.success) {
resolver(result.graph);
} else if (result.success === false) {
rejector(result.error);
}
},
});
};
}
function createWorkerExitHandler(
worker: ChildProcess,
pendingPromises: Map<string, PendingPromise>
) {
return () => {
for (const [_, pendingPromise] of pendingPromises) {
pendingPromise.rejector(
new Error(
`Plugin worker ${
pluginNames.get(worker) ?? worker.pid
} exited unexpectedly with code ${worker.exitCode}`
)
);
}
};
}
process.on('exit', () => {
for (const fn of cleanupFunctions) {
fn();
}
});
function registerPendingPromise(
tx: string,
pending: Map<string, PendingPromise>,
callback: () => void
): Promise<any> {
let resolver, rejector;
const promise = new Promise((res, rej) => {
resolver = res;
rejector = rej;
callback();
}).finally(() => {
pending.delete(tx);
});
pending.set(tx, {
promise,
resolver,
rejector,
});
return promise;
}

View File

@ -0,0 +1,84 @@
import { consumeMessage, PluginWorkerMessage } from './messaging';
import { LoadedNxPlugin } from '../internal-api';
import { loadNxPlugin } from '../loader';
import { runCreateNodesInParallel } from '../utils';
global.NX_GRAPH_CREATION = true;
let plugin: LoadedNxPlugin;
process.on('message', async (message: string) => {
consumeMessage<PluginWorkerMessage>(message, {
load: async ({ plugin: pluginConfiguration, root }) => {
process.chdir(root);
try {
const [promise] = loadNxPlugin(pluginConfiguration, root);
plugin = await promise;
return {
type: 'load-result',
payload: {
name: plugin.name,
createNodesPattern: plugin.createNodes?.[0],
hasCreateDependencies:
'createDependencies' in plugin && !!plugin.createDependencies,
hasProcessProjectGraph:
'processProjectGraph' in plugin && !!plugin.processProjectGraph,
success: true,
},
};
} catch (e) {
return {
type: 'load-result',
payload: {
success: false,
error: `Could not load plugin ${plugin} \n ${
e instanceof Error ? e.stack : ''
}`,
},
};
}
},
createNodes: async ({ configFiles, context, tx }) => {
try {
const result = await plugin.createNodes[1](configFiles, context);
return {
type: 'createNodesResult',
payload: { result, success: true, tx },
};
} catch (e) {
return {
type: 'createNodesResult',
payload: { success: false, error: e.stack, tx },
};
}
},
createDependencies: async ({ context, tx }) => {
try {
const result = await plugin.createDependencies(context);
return {
type: 'createDependenciesResult',
payload: { dependencies: result, success: true, tx },
};
} catch (e) {
return {
type: 'createDependenciesResult',
payload: { success: false, error: e.stack, tx },
};
}
},
processProjectGraph: async ({ graph, ctx, tx }) => {
try {
const result = await plugin.processProjectGraph(graph, ctx);
return {
type: 'processProjectGraphResult',
payload: { graph: result, success: true, tx },
};
} catch (e) {
return {
type: 'processProjectGraphResult',
payload: { success: false, error: e.stack, tx },
};
}
},
});
});

View File

@ -0,0 +1,294 @@
// This file contains methods and utilities that should **only** be used by the plugin worker.
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 { readTsConfig } from '../../utils/typescript';
import {
registerTranspiler,
registerTsConfigPaths,
} from '../../plugins/js/utils/register';
import {
createProjectRootMappingsFromProjectConfigurations,
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 { NxPlugin } from './public-api';
import path = require('node:path/posix');
import {
ExpandedPluginConfiguration,
PluginConfiguration,
} from '../../config/nx-json';
import { retrieveProjectConfigurationsWithoutPluginInference } from '../utils/retrieve-workspace-files';
import { normalizeNxPlugin } from './utils';
import { LoadedNxPlugin } from './internal-api';
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'
);
return {
path: localPluginPackageJson,
json: readJsonFile(localPluginPackageJson),
};
}
}
throw e;
}
}
export function resolveLocalNxPlugin(
importPath: string,
projects: Record<string, ProjectConfiguration>,
root = workspaceRoot
): { path: string; projectConfig: ProjectConfiguration } | null {
return lookupLocalPlugin(importPath, projects, root);
}
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,
}),
];
unregisterPluginTSTranspiler = () => {
cleanupFns.forEach((fn) => fn?.());
};
}
function lookupLocalPlugin(
importPath: string,
projects: Record<string, ProjectConfiguration>,
root = workspaceRoot
) {
const plugin = findNxProjectForImportPath(importPath, projects, root);
if (!plugin) {
return null;
}
const projectConfig: ProjectConfiguration = projects[plugin];
return { path: path.join(root, projectConfig.root), projectConfig };
}
function findNxProjectForImportPath(
importPath: string,
projects: Record<string, ProjectConfiguration>,
root = workspaceRoot
): string | null {
const tsConfigPaths: Record<string, string[]> = readTsConfigPaths(root);
const possiblePaths = tsConfigPaths[importPath]?.map((p) =>
normalizePath(path.relative(root, path.join(root, p)))
);
if (possiblePaths?.length) {
const projectRootMappings =
createProjectRootMappingsFromProjectConfigurations(projects);
for (const tsConfigPath of possiblePaths) {
const nxProject = findProjectForPath(tsConfigPath, projectRootMappings);
if (nxProject) {
return nxProject;
}
}
logger.verbose(
'Unable to find local plugin',
possiblePaths,
projectRootMappings
);
throw new Error(
'Unable to resolve local plugin with import path ' + importPath
);
}
}
let tsconfigPaths: Record<string, string[]>;
function readTsConfigPaths(root: string = workspaceRoot) {
if (!tsconfigPaths) {
const tsconfigPath: string | null = ['tsconfig.base.json', 'tsconfig.json']
.map((x) => path.join(root, x))
.filter((x) => existsSync(x))[0];
if (!tsconfigPath) {
throw new Error('unable to find tsconfig.base.json or tsconfig.json');
}
const { compilerOptions } = readJsonFile(tsconfigPath);
tsconfigPaths = compilerOptions?.paths;
}
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 registerTSTranspiler = false;
try {
pluginPath = require.resolve(moduleName, {
paths,
});
const extension = path.extname(pluginPath);
registerTSTranspiler = extension === '.ts';
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
const plugin = resolveLocalNxPlugin(moduleName, projects, root);
if (plugin) {
registerTSTranspiler = 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');
// Register the ts-transpiler if we are pointing to a
// plain ts file that's not part of a plugin project
if (registerTSTranspiler) {
registerPluginTSTranspiler();
}
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 };
}
let projectsWithoutInference: Record<string, ProjectConfiguration>;
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> {
try {
require.resolve(
typeof pluginConfiguration === 'string'
? pluginConfiguration
: pluginConfiguration.plugin
);
} catch {
// If a plugin cannot be resolved, we will need projects to resolve it
projectsWithoutInference ??=
await retrieveProjectConfigurationsWithoutPluginInference(root);
}
const moduleName =
typeof pluginConfiguration === 'string'
? pluginConfiguration
: pluginConfiguration.plugin;
performance.mark(`Load Nx Plugin: ${moduleName} - start`);
let { pluginPath, name } = await getPluginPathAndName(
moduleName,
paths,
projectsWithoutInference,
root
);
const plugin = normalizeNxPlugin(await importPluginModule(pluginPath));
plugin.name ??= name;
performance.mark(`Load Nx Plugin: ${moduleName} - end`);
performance.measure(
`Load Nx Plugin: ${moduleName}`,
`Load Nx Plugin: ${moduleName} - start`,
`Load Nx Plugin: ${moduleName} - end`
);
return new LoadedNxPlugin(plugin, pluginConfiguration);
}
async function importPluginModule(pluginPath: string): Promise<NxPlugin> {
const m = await import(pluginPath);
if (
m.default &&
('createNodes' in m.default || 'createDependencies' in m.default)
) {
return m.default;
}
return m;
}

View File

@ -0,0 +1,123 @@
// This file represents the public API for plugins which live in nx.json's plugins array.
// For methods to interact with plugins from within Nx, see `./internal-api.ts`.
import { NxPluginV1 } from '../../utils/nx-plugin.deprecated';
import {
FileMap,
ProjectGraph,
ProjectGraphExternalNode,
} from '../../config/project-graph';
import { ProjectConfiguration } from '../../config/workspace-json-project-json';
import { NxJsonConfiguration } from '../../config/nx-json';
import { RawProjectGraphDependency } from '../project-graph-builder';
/**
* Context for {@link CreateNodesFunction}
*/
export interface CreateNodesContext {
readonly nxJsonConfiguration: NxJsonConfiguration;
readonly workspaceRoot: string;
/**
* The subset of configuration files which match the createNodes pattern
*/
readonly configFiles: string[];
}
/**
* A function which parses a configuration file into a set of nodes.
* Used for creating nodes for the {@link ProjectGraph}
*/
export type CreateNodesFunction<T = unknown> = (
projectConfigurationFile: string,
options: T | undefined,
context: CreateNodesContext
) => CreateNodesResult | Promise<CreateNodesResult>;
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export interface CreateNodesResult {
/**
* A map of project root -> project configuration
*/
projects?: Record<string, Optional<ProjectConfiguration, 'root'>>;
/**
* A map of external node name -> external node. External nodes do not have a root, so the key is their name.
*/
externalNodes?: Record<string, ProjectGraphExternalNode>;
}
/**
* A pair of file patterns and {@link CreateNodesFunction}
*/
export type CreateNodes<T = unknown> = readonly [
projectFilePattern: string,
createNodesFunction: CreateNodesFunction<T>
];
/**
* Context for {@link CreateDependencies}
*/
export interface CreateDependenciesContext {
/**
* The external nodes that have been added to the graph.
*/
readonly externalNodes: ProjectGraph['externalNodes'];
/**
* The configuration of each project in the workspace.
*/
readonly projects: Record<string, ProjectConfiguration>;
/**
* The `nx.json` configuration from the workspace
*/
readonly nxJsonConfiguration: NxJsonConfiguration;
/**
* All files in the workspace
*/
readonly fileMap: FileMap;
/**
* Files changes since last invocation
*/
readonly filesToProcess: FileMap;
readonly workspaceRoot: string;
}
/**
* A function which parses files in the workspace to create dependencies in the {@link ProjectGraph}
* Use {@link validateDependency} to validate dependencies
*/
export type CreateDependencies<T = unknown> = (
options: T | undefined,
context: CreateDependenciesContext
) => RawProjectGraphDependency[] | Promise<RawProjectGraphDependency[]>;
/**
* A plugin for Nx which creates nodes and dependencies for the {@link ProjectGraph}
*/
export type NxPluginV2<TOptions = unknown> = {
name: string;
/**
* Provides a file pattern and function that retrieves configuration info from
* those files. e.g. { '**\/*.csproj': buildProjectsFromCsProjFile }
*/
createNodes?: CreateNodes<TOptions>;
// Todo(@AgentEnder): This shouldn't be a full processor, since its only responsible for defining edges between projects. What do we want the API to be?
/**
* Provides a function to analyze files to create dependencies for the {@link ProjectGraph}
*/
createDependencies?: CreateDependencies<TOptions>;
};
/**
* A plugin for Nx
*/
export type NxPlugin = NxPluginV1 | NxPluginV2;

View File

@ -0,0 +1,123 @@
import { dirname } from 'node:path';
import { toProjectName } from '../../config/workspaces';
import { combineGlobPatterns } from '../../utils/globs';
import type { NxPluginV1 } from '../../utils/nx-plugin.deprecated';
import type {
CreateNodesResultWithContext,
LoadedNxPlugin,
NormalizedPlugin,
} from './internal-api';
import type { CreateNodesContext, NxPlugin, NxPluginV2 } from './public-api';
import { AggregateCreateNodesError, CreateNodesError } from '../error-types';
export function isNxPluginV2(plugin: NxPlugin): plugin is NxPluginV2 {
return 'createNodes' in plugin || 'createDependencies' in plugin;
}
export function isNxPluginV1(
plugin: NxPlugin | LoadedNxPlugin
): plugin is NxPluginV1 {
return 'processProjectGraph' in plugin || 'projectFilePatterns' in plugin;
}
export function normalizeNxPlugin(plugin: NxPlugin): NormalizedPlugin {
if (isNxPluginV2(plugin)) {
return plugin;
}
if (isNxPluginV1(plugin) && plugin.projectFilePatterns) {
return {
...plugin,
createNodes: [
`*/**/${combineGlobPatterns(plugin.projectFilePatterns)}`,
(configFilePath) => {
const root = dirname(configFilePath);
return {
projects: {
[root]: {
name: toProjectName(configFilePath),
targets: plugin.registerProjectTargets?.(configFilePath),
},
},
};
},
],
};
}
return plugin;
}
export async function runCreateNodesInParallel(
configFiles: string[],
plugin: NormalizedPlugin,
options: unknown,
context: CreateNodesContext
): Promise<CreateNodesResultWithContext[]> {
performance.mark(`${plugin.name}:createNodes - start`);
const promises: Array<
CreateNodesResultWithContext | Promise<CreateNodesResultWithContext>
> = configFiles.map((file) => {
performance.mark(`${plugin.name}:createNodes:${file} - start`);
// Result is either static or a promise, using Promise.resolve lets us
// handle both cases with same logic
const value = Promise.resolve(
plugin.createNodes[1](file, options, context)
);
return value
.catch((e) => {
performance.mark(`${plugin.name}:createNodes:${file} - end`);
return new CreateNodesError({
error: e,
pluginName: plugin.name,
file,
});
})
.then((r) => {
performance.mark(`${plugin.name}:createNodes:${file} - end`);
performance.measure(
`${plugin.name}:createNodes:${file}`,
`${plugin.name}:createNodes:${file} - start`,
`${plugin.name}:createNodes:${file} - end`
);
return { ...r, pluginName: plugin.name, file };
});
});
const results = await Promise.all(promises).then((results) => {
performance.mark(`${plugin.name}:createNodes - end`);
performance.measure(
`${plugin.name}:createNodes`,
`${plugin.name}:createNodes - start`,
`${plugin.name}:createNodes - end`
);
return results;
});
const [errors, successful] = partition<
CreateNodesError,
CreateNodesResultWithContext
>(results, (r): r is CreateNodesError => r instanceof CreateNodesError);
if (errors.length > 0) {
throw new AggregateCreateNodesError(plugin.name, errors, successful);
}
return results;
}
function partition<T, T2 = T>(
arr: Array<T | T2>,
test: (item: T | T2) => item is T
): [T[], T2[]] {
const pass: T[] = [];
const fail: T2[] = [];
for (const item of arr) {
if (test(item)) {
pass.push(item);
} else {
fail.push(item as any as T2);
}
}
return [pass, fail];
}

View File

@ -15,7 +15,7 @@ import {
ProjectGraphProjectNode, ProjectGraphProjectNode,
} from '../config/project-graph'; } from '../config/project-graph';
import { ProjectConfiguration } from '../config/workspace-json-project-json'; import { ProjectConfiguration } from '../config/workspace-json-project-json';
import { CreateDependenciesContext } from '../utils/nx-plugin'; import { CreateDependenciesContext } from './plugins';
import { getFileMap } from './build-project-graph'; import { getFileMap } from './build-project-graph';
/** /**

View File

@ -26,15 +26,17 @@ import {
retrieveWorkspaceFiles, retrieveWorkspaceFiles,
} from './utils/retrieve-workspace-files'; } from './utils/retrieve-workspace-files';
import { readNxJson } from '../config/nx-json'; import { readNxJson } from '../config/nx-json';
import { unregisterPluginTSTranspiler } from '../utils/nx-plugin';
import { import {
ConfigurationResult, ConfigurationResult,
ConfigurationSourceMaps, ConfigurationSourceMaps,
} from './utils/project-configuration-utils';
import {
CreateNodesError, CreateNodesError,
MergeNodesError, MergeNodesError,
ProjectConfigurationsError, ProjectConfigurationsError,
} from './utils/project-configuration-utils'; } from './error-types';
import { DaemonProjectGraphError } from '../daemon/daemon-project-graph-error'; import { DaemonProjectGraphError } from '../daemon/daemon-project-graph-error';
import { loadNxPlugins, LoadedNxPlugin } from './plugins/internal-api';
/** /**
* Synchronously reads the latest cached copy of the workspace's ProjectGraph. * Synchronously reads the latest cached copy of the workspace's ProjectGraph.
@ -95,15 +97,16 @@ export function readProjectsConfigurationFromProjectGraph(
} }
export async function buildProjectGraphAndSourceMapsWithoutDaemon() { export async function buildProjectGraphAndSourceMapsWithoutDaemon() {
// Set this globally to allow plugins to know if they are being called from the project graph creation
global.NX_GRAPH_CREATION = true; global.NX_GRAPH_CREATION = true;
const nxJson = readNxJson(); const nxJson = readNxJson();
performance.mark('retrieve-project-configurations:start'); performance.mark('retrieve-project-configurations:start');
let configurationResult: ConfigurationResult; let configurationResult: ConfigurationResult;
let projectConfigurationsError: ProjectConfigurationsError; let projectConfigurationsError: ProjectConfigurationsError;
const [plugins, cleanup] = await loadNxPlugins(nxJson.plugins);
try { try {
configurationResult = await retrieveProjectConfigurations( configurationResult = await retrieveProjectConfigurations(
plugins,
workspaceRoot, workspaceRoot,
nxJson nxJson
); );
@ -137,7 +140,8 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() {
fileMap, fileMap,
allWorkspaceFiles, allWorkspaceFiles,
rustReferences, rustReferences,
cacheEnabled ? readFileMapCache() : null cacheEnabled ? readFileMapCache() : null,
plugins
); );
} catch (e) { } catch (e) {
if (e instanceof CreateDependenciesError) { if (e instanceof CreateDependenciesError) {
@ -149,12 +153,13 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() {
} else { } else {
throw e; throw e;
} }
} finally {
cleanup();
} }
const { projectGraph, projectFileMapCache } = projectGraphResult; const { projectGraph, projectFileMapCache } = projectGraphResult;
performance.mark('build-project-graph-using-project-file-map:end'); performance.mark('build-project-graph-using-project-file-map:end');
unregisterPluginTSTranspiler();
delete global.NX_GRAPH_CREATION; delete global.NX_GRAPH_CREATION;
const errors = [ const errors = [

View File

@ -5,9 +5,8 @@ import {
TargetConfiguration, TargetConfiguration,
} from '../../config/workspace-json-project-json'; } from '../../config/workspace-json-project-json';
import { findMatchingProjects } from '../../utils/find-matching-projects'; import { findMatchingProjects } from '../../utils/find-matching-projects';
import { NX_PREFIX } from '../../utils/logger';
import { resolveNxTokensInOptions } from '../utils/project-configuration-utils'; import { resolveNxTokensInOptions } from '../utils/project-configuration-utils';
import { CreateDependenciesContext } from '../../utils/nx-plugin'; import { CreateDependenciesContext } from '../plugins';
export async function normalizeProjectNodes( export async function normalizeProjectNodes(
ctx: CreateDependenciesContext, ctx: CreateDependenciesContext,

View File

@ -1,4 +1,4 @@
import { ONLY_MODIFIES_EXISTING_TARGET } from '../../plugins/target-defaults/target-defaults-plugin'; import { ONLY_MODIFIES_EXISTING_TARGET } from '../../plugins/target-defaults/symbols';
import { import {
ProjectConfiguration, ProjectConfiguration,
TargetConfiguration, TargetConfiguration,

View File

@ -7,17 +7,26 @@ import {
TargetMetadata, TargetMetadata,
} from '../../config/workspace-json-project-json'; } from '../../config/workspace-json-project-json';
import { NX_PREFIX } from '../../utils/logger'; import { NX_PREFIX } from '../../utils/logger';
import { CreateNodesResult, LoadedNxPlugin } from '../../utils/nx-plugin';
import { readJsonFile } from '../../utils/fileutils'; import { readJsonFile } from '../../utils/fileutils';
import { workspaceRoot } from '../../utils/workspace-root'; import { workspaceRoot } from '../../utils/workspace-root';
import { import {
ONLY_MODIFIES_EXISTING_TARGET, ONLY_MODIFIES_EXISTING_TARGET,
OVERRIDE_SOURCE_FILE, OVERRIDE_SOURCE_FILE,
} from '../../plugins/target-defaults/target-defaults-plugin'; } from '../../plugins/target-defaults/symbols';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import { join } from 'path'; import { join } from 'path';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import {
CreateNodesResultWithContext,
LoadedNxPlugin,
} from '../plugins/internal-api';
import {
CreateNodesError,
MergeNodesError,
ProjectConfigurationsError,
isAggregateCreateNodesError,
} from '../error-types';
export type SourceInformation = [file: string, plugin: string]; export type SourceInformation = [file: string, plugin: string];
export type ConfigurationSourceMaps = Record< export type ConfigurationSourceMaps = Record<
@ -294,10 +303,6 @@ export type ConfigurationResult = {
projectRootMap: Record<string, string>; projectRootMap: Record<string, string>;
sourceMaps: ConfigurationSourceMaps; sourceMaps: ConfigurationSourceMaps;
}; };
type CreateNodesResultWithContext = CreateNodesResult & {
file: string;
pluginName: string;
};
/** /**
* Transforms a list of project paths into a map of project configurations. * Transforms a list of project paths into a map of project configurations.
@ -307,10 +312,10 @@ type CreateNodesResultWithContext = CreateNodesResult & {
* @param workspaceFiles A list of non-ignored workspace files * @param workspaceFiles A list of non-ignored workspace files
* @param plugins The plugins that should be used to infer project configuration * @param plugins The plugins that should be used to infer project configuration
*/ */
export function createProjectConfigurations( export async function createProjectConfigurations(
root: string = workspaceRoot, root: string = workspaceRoot,
nxJson: NxJsonConfiguration, nxJson: NxJsonConfiguration,
workspaceFiles: string[], // making this parameter allows devkit to pick up newly created projects projectFiles: string[], // making this parameter allows devkit to pick up newly created projects
plugins: LoadedNxPlugin[] plugins: LoadedNxPlugin[]
): Promise<ConfigurationResult> { ): Promise<ConfigurationResult> {
performance.mark('build-project-configs:start'); performance.mark('build-project-configs:start');
@ -319,20 +324,21 @@ export function createProjectConfigurations(
const errors: Array<CreateNodesError | MergeNodesError> = []; const errors: Array<CreateNodesError | MergeNodesError> = [];
// We iterate over plugins first - this ensures that plugins specified first take precedence. // We iterate over plugins first - this ensures that plugins specified first take precedence.
for (const { plugin, options, include, exclude } of plugins) { for (const {
const [pattern, createNodes] = plugin.createNodes ?? []; name: pluginName,
const pluginResults: Array< createNodes: createNodesTuple,
CreateNodesResultWithContext | Promise<CreateNodesResultWithContext> include,
> = []; exclude,
} of plugins) {
const [pattern, createNodes] = createNodesTuple ?? [];
performance.mark(`${plugin.name}:createNodes - start`);
if (!pattern) { if (!pattern) {
continue; continue;
} }
const matchingConfigFiles: string[] = []; const matchingConfigFiles: string[] = [];
for (const file of workspaceFiles) { for (const file of projectFiles) {
if (minimatch(file, pattern, { dot: true })) { if (minimatch(file, pattern, { dot: true })) {
if (include) { if (include) {
const included = include.some((includedPattern) => const included = include.some((includedPattern) =>
@ -355,76 +361,20 @@ export function createProjectConfigurations(
matchingConfigFiles.push(file); matchingConfigFiles.push(file);
} }
} }
for (const file of matchingConfigFiles) { let r = createNodes(matchingConfigFiles, {
performance.mark(`${plugin.name}:createNodes:${file} - start`);
try {
let r = createNodes(file, options, {
nxJsonConfiguration: nxJson, nxJsonConfiguration: nxJson,
workspaceRoot: root, workspaceRoot: root,
configFiles: matchingConfigFiles, configFiles: matchingConfigFiles,
}); }).catch((e) => {
if (isAggregateCreateNodesError(e)) {
if (r instanceof Promise) { errors.push(...e.errors);
pluginResults.push( return e.partialResults;
r
.catch((error) => {
performance.mark(`${plugin.name}:createNodes:${file} - end`);
errors.push(
new CreateNodesError({
file,
pluginName: plugin.name,
error,
})
);
return {
projects: {},
};
})
.then((r) => {
performance.mark(`${plugin.name}:createNodes:${file} - end`);
performance.measure(
`${plugin.name}:createNodes:${file}`,
`${plugin.name}:createNodes:${file} - start`,
`${plugin.name}:createNodes:${file} - end`
);
return { ...r, file, pluginName: plugin.name };
})
);
} else { } else {
performance.mark(`${plugin.name}:createNodes:${file} - end`); throw e;
performance.measure( }
`${plugin.name}:createNodes:${file}`,
`${plugin.name}:createNodes:${file} - start`,
`${plugin.name}:createNodes:${file} - end`
);
pluginResults.push({
...r,
file,
pluginName: plugin.name,
}); });
}
} catch (error) {
errors.push(
new CreateNodesError({
file,
pluginName: plugin.name,
error,
})
);
}
}
results.push( results.push(r);
Promise.all(pluginResults).then((results) => {
performance.mark(`${plugin.name}:createNodes - end`);
performance.measure(
`${plugin.name}:createNodes`,
`${plugin.name}:createNodes - start`,
`${plugin.name}:createNodes - end`
);
return results;
})
);
} }
return Promise.all(results).then((results) => { return Promise.all(results).then((results) => {
@ -557,62 +507,6 @@ export function readProjectConfigurationsFromRootMap(
return projects; return projects;
} }
export class ProjectConfigurationsError extends Error {
constructor(
public readonly errors: Array<MergeNodesError | CreateNodesError>,
public readonly partialProjectConfigurationsResult: ConfigurationResult
) {
super('Failed to create project configurations');
this.name = this.constructor.name;
}
}
export class CreateNodesError extends Error {
file: string;
pluginName: string;
constructor({
file,
pluginName,
error,
}: {
file: string;
pluginName: string;
error: Error;
}) {
const msg = `The "${pluginName}" plugin threw an error while creating nodes from ${file}:`;
super(msg, { cause: error });
this.name = this.constructor.name;
this.file = file;
this.pluginName = pluginName;
this.stack = `${this.message}\n ${error.stack.split('\n').join('\n ')}`;
}
}
export class MergeNodesError extends Error {
file: string;
pluginName: string;
constructor({
file,
pluginName,
error,
}: {
file: string;
pluginName: string;
error: Error;
}) {
const msg = `The nodes created from ${file} by the "${pluginName}" could not be merged into the project graph:`;
super(msg, { cause: error });
this.name = this.constructor.name;
this.file = file;
this.pluginName = pluginName;
this.stack = `${this.message}\n ${error.stack.split('\n').join('\n ')}`;
}
}
/** /**
* Merges two targets. * Merges two targets.
* *

View File

@ -1,4 +1,3 @@
import { getDefaultPlugins } from '../../utils/nx-plugin';
import { TempFs } from '../../internal-testing-utils/temp-fs'; import { TempFs } from '../../internal-testing-utils/temp-fs';
import { retrieveProjectConfigurationPaths } from './retrieve-workspace-files'; import { retrieveProjectConfigurationPaths } from './retrieve-workspace-files';
@ -26,10 +25,18 @@ describe('retrieveProjectConfigurationPaths', () => {
}) })
); );
const configPaths = await retrieveProjectConfigurationPaths( const configPaths = retrieveProjectConfigurationPaths(fs.tempDir, [
fs.tempDir, {
await getDefaultPlugins(fs.tempDir) createNodes: [
); '{project.json,**/project.json}',
() => {
return {
projects: {},
};
},
],
},
]);
expect(configPaths).not.toContain('not-projects/project.json'); expect(configPaths).not.toContain('not-projects/project.json');
expect(configPaths).toContain('projects/project.json'); expect(configPaths).toContain('projects/project.json');

View File

@ -1,28 +1,22 @@
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { getNxRequirePaths } from '../../utils/installation-directory';
import { ProjectConfiguration } from '../../config/workspace-json-project-json'; import { ProjectConfiguration } from '../../config/workspace-json-project-json';
import { import {
NX_ANGULAR_JSON_PLUGIN_NAME, NX_ANGULAR_JSON_PLUGIN_NAME,
NxAngularJsonPlugin,
shouldMergeAngularProjects, shouldMergeAngularProjects,
} from '../../adapter/angular-json'; } from '../../adapter/angular-json';
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json'; import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
import { getNxPackageJsonWorkspacesPlugin } from '../../plugins/package-json-workspaces';
import { import {
createProjectConfigurations, createProjectConfigurations,
ConfigurationResult, ConfigurationResult,
} from './project-configuration-utils'; } from './project-configuration-utils';
import { import { LoadedNxPlugin, loadNxPlugins } from '../plugins/internal-api';
getDefaultPlugins,
LoadedNxPlugin,
loadNxPlugins,
} from '../../utils/nx-plugin';
import { ProjectJsonProjectsPlugin } from '../../plugins/project-json/build-nodes/project-json';
import { import {
getNxWorkspaceFilesFromContext, getNxWorkspaceFilesFromContext,
globWithWorkspaceContext, globWithWorkspaceContext,
} from '../../utils/workspace-context'; } from '../../utils/workspace-context';
import { buildAllWorkspaceFiles } from './build-all-workspace-files'; import { buildAllWorkspaceFiles } from './build-all-workspace-files';
import { join } from 'path';
import { NxPlugin } from '../plugins';
/** /**
* Walks the workspace directory to create the `projectFileMap`, `ProjectConfigurations` and `allWorkspaceFiles` * Walks the workspace directory to create the `projectFileMap`, `ProjectConfigurations` and `allWorkspaceFiles`
@ -65,41 +59,45 @@ export async function retrieveWorkspaceFiles(
/** /**
* Walk through the workspace and return `ProjectConfigurations`. Only use this if the projectFileMap is not needed. * Walk through the workspace and return `ProjectConfigurations`. Only use this if the projectFileMap is not needed.
*
* @param workspaceRoot
* @param nxJson
*/ */
export async function retrieveProjectConfigurations( export async function retrieveProjectConfigurations(
plugins: LoadedNxPlugin[],
workspaceRoot: string, workspaceRoot: string,
nxJson: NxJsonConfiguration nxJson: NxJsonConfiguration
): Promise<ConfigurationResult> { ): Promise<ConfigurationResult> {
const plugins = await loadNxPlugins( const projects = await _retrieveProjectConfigurations(
nxJson?.plugins ?? [], workspaceRoot,
getNxRequirePaths(workspaceRoot), nxJson,
workspaceRoot plugins
); );
return projects;
return _retrieveProjectConfigurations(workspaceRoot, nxJson, plugins);
} }
export async function retrieveProjectConfigurationsWithAngularProjects( export async function retrieveProjectConfigurationsWithAngularProjects(
workspaceRoot: string, workspaceRoot: string,
nxJson: NxJsonConfiguration nxJson: NxJsonConfiguration
): Promise<ConfigurationResult> { ): Promise<ConfigurationResult> {
const plugins = await loadNxPlugins( const pluginsToLoad = nxJson?.plugins ?? [];
nxJson?.plugins ?? [],
getNxRequirePaths(workspaceRoot),
workspaceRoot
);
if ( if (
shouldMergeAngularProjects(workspaceRoot, true) && shouldMergeAngularProjects(workspaceRoot, true) &&
!plugins.some((p) => p.plugin.name === NX_ANGULAR_JSON_PLUGIN_NAME) !pluginsToLoad.some(
(p) =>
p === NX_ANGULAR_JSON_PLUGIN_NAME ||
(typeof p === 'object' && p.plugin === NX_ANGULAR_JSON_PLUGIN_NAME)
)
) { ) {
plugins.push({ plugin: NxAngularJsonPlugin }); pluginsToLoad.push(join(__dirname, '../../adapter/angular-json'));
} }
return _retrieveProjectConfigurations(workspaceRoot, nxJson, plugins); const [plugins, cleanup] = await loadNxPlugins(
nxJson?.plugins ?? [],
workspaceRoot
);
const res = _retrieveProjectConfigurations(workspaceRoot, nxJson, plugins);
cleanup();
return res;
} }
function _retrieveProjectConfigurations( function _retrieveProjectConfigurations(
@ -120,7 +118,7 @@ function _retrieveProjectConfigurations(
export function retrieveProjectConfigurationPaths( export function retrieveProjectConfigurationPaths(
root: string, root: string,
plugins: LoadedNxPlugin[] plugins: Array<{ createNodes?: readonly [string, ...unknown[]] } & unknown>
): string[] { ): string[] {
const projectGlobPatterns = configurationGlobs(plugins); const projectGlobPatterns = configurationGlobs(plugins);
return globWithWorkspaceContext(root, projectGlobPatterns); return globWithWorkspaceContext(root, projectGlobPatterns);
@ -136,7 +134,7 @@ export async function retrieveProjectConfigurationsWithoutPluginInference(
root: string root: string
): Promise<Record<string, ProjectConfiguration>> { ): Promise<Record<string, ProjectConfiguration>> {
const nxJson = readNxJson(root); const nxJson = readNxJson(root);
const plugins = await getDefaultPlugins(root); const [plugins, cleanup] = await loadNxPlugins([]); // only load default plugins
const projectGlobPatterns = retrieveProjectConfigurationPaths(root, plugins); const projectGlobPatterns = retrieveProjectConfigurationPaths(root, plugins);
const cacheKey = root + ',' + projectGlobPatterns.join(','); const cacheKey = root + ',' + projectGlobPatterns.join(',');
@ -150,21 +148,22 @@ export async function retrieveProjectConfigurationsWithoutPluginInference(
root, root,
nxJson, nxJson,
projectFiles, projectFiles,
[ plugins
{ plugin: getNxPackageJsonWorkspacesPlugin(root) },
{ plugin: ProjectJsonProjectsPlugin },
]
); );
projectsWithoutPluginCache.set(cacheKey, projects); projectsWithoutPluginCache.set(cacheKey, projects);
cleanup();
return projects; return projects;
} }
export function configurationGlobs(plugins: LoadedNxPlugin[]): string[] { export function configurationGlobs(
plugins: Array<{ createNodes?: readonly [string, ...unknown[]] }>
): string[] {
const globPatterns = []; const globPatterns = [];
for (const { plugin } of plugins) { for (const plugin of plugins) {
if (plugin.createNodes) { if ('createNodes' in plugin && plugin.createNodes) {
globPatterns.push(plugin.createNodes[0]); globPatterns.push(plugin.createNodes[0]);
} }
} }

View File

@ -31,6 +31,11 @@ export const logger = {
fatal: (...s) => { fatal: (...s) => {
console.error(...s); console.error(...s);
}, },
verbose: (...s) => {
if (process.env.NX_VERBOSE_LOGGING) {
console.log(...s);
}
},
}; };
export function stripIndent(str: string): string { export function stripIndent(str: string): string {

View File

@ -1,10 +1,11 @@
import { shouldMergeAngularProjects } from '../adapter/angular-json'; import { shouldMergeAngularProjects } from '../adapter/angular-json';
import { ProjectGraphProcessor } from '../config/project-graph'; import { ProjectGraphProcessor } from '../config/project-graph';
import { TargetConfiguration } from '../config/workspace-json-project-json'; import { TargetConfiguration } from '../config/workspace-json-project-json';
import { ProjectJsonProjectsPlugin } from '../plugins/project-json/build-nodes/project-json'; import ProjectJsonProjectsPlugin from '../plugins/project-json/build-nodes/project-json';
import { TargetDefaultsPlugin } from '../plugins/target-defaults/target-defaults-plugin'; import TargetDefaultsPlugin from '../plugins/target-defaults/target-defaults-plugin';
import { getNxPackageJsonWorkspacesPlugin } from '../plugins/package-json-workspaces'; import * as PackageJsonWorkspacesPlugin from '../plugins/package-json-workspaces';
import { LoadedNxPlugin, NxPluginV2 } from './nx-plugin'; import { NxPluginV2 } from '../project-graph/plugins';
import { LoadedNxPlugin } from '../project-graph/plugins/internal-api';
/** /**
* @deprecated Add targets to the projects in a {@link CreateNodes} function instead. This will be removed in Nx 19 * @deprecated Add targets to the projects in a {@link CreateNodes} function instead. This will be removed in Nx 19
@ -39,18 +40,16 @@ export type NxPluginV1 = {
/** /**
* @todo(@agentender) v19: Remove this fn when we remove readWorkspaceConfig * @todo(@agentender) v19: Remove this fn when we remove readWorkspaceConfig
*/ */
export function getDefaultPluginsSync(root: string): LoadedNxPlugin[] { export function getDefaultPluginsSync(root: string): NxPluginV2[] {
const plugins: NxPluginV2[] = [ const plugins: NxPluginV2[] = [
require('../plugins/js'), require('../plugins/js'),
...(shouldMergeAngularProjects(root, false) ...(shouldMergeAngularProjects(root, false)
? [require('../adapter/angular-json').NxAngularJsonPlugin] ? [require('../adapter/angular-json').NxAngularJsonPlugin]
: []), : []),
TargetDefaultsPlugin, TargetDefaultsPlugin,
getNxPackageJsonWorkspacesPlugin(root), PackageJsonWorkspacesPlugin,
ProjectJsonProjectsPlugin, ProjectJsonProjectsPlugin,
]; ];
return plugins.map((p) => ({ return plugins;
plugin: p,
}));
} }

View File

@ -1,556 +0,0 @@
import { existsSync } from 'fs';
import * as path from 'path';
import {
FileMap,
ProjectGraph,
ProjectGraphExternalNode,
} from '../config/project-graph';
import { toProjectName } from '../config/workspaces';
import { workspaceRoot } from './workspace-root';
import { readJsonFile } from '../utils/fileutils';
import {
PackageJson,
readModulePackageJsonWithoutFallbacks,
} from './package-json';
import {
registerTranspiler,
registerTsConfigPaths,
} from '../plugins/js/utils/register';
import { ProjectConfiguration } from '../config/workspace-json-project-json';
import { logger } from './logger';
import {
createProjectRootMappingsFromProjectConfigurations,
findProjectForPath,
} from '../project-graph/utils/find-project-for-path';
import { normalizePath } from './path';
import { dirname, join } from 'path';
import { getNxRequirePaths } from './installation-directory';
import { readTsConfig } from '../plugins/js/utils/typescript';
import {
NxJsonConfiguration,
PluginConfiguration,
readNxJson,
} from '../config/nx-json';
import type * as ts from 'typescript';
import { NxPluginV1 } from './nx-plugin.deprecated';
import { RawProjectGraphDependency } from '../project-graph/project-graph-builder';
import { combineGlobPatterns } from './globs';
import { shouldMergeAngularProjects } from '../adapter/angular-json';
import { getNxPackageJsonWorkspacesPlugin } from '../plugins/package-json-workspaces';
import { ProjectJsonProjectsPlugin } from '../plugins/project-json/build-nodes/project-json';
import { PackageJsonProjectsNextToProjectJsonPlugin } from '../plugins/project-json/build-nodes/package-json-next-to-project-json';
import { retrieveProjectConfigurationsWithoutPluginInference } from '../project-graph/utils/retrieve-workspace-files';
import { TargetDefaultsPlugin } from '../plugins/target-defaults/target-defaults-plugin';
/**
* Context for {@link CreateNodesFunction}
*/
export interface CreateNodesContext {
readonly nxJsonConfiguration: NxJsonConfiguration;
readonly workspaceRoot: string;
/**
* The subset of configuration files which match the createNodes pattern
*/
readonly configFiles: string[];
}
/**
* A function which parses a configuration file into a set of nodes.
* Used for creating nodes for the {@link ProjectGraph}
*/
export type CreateNodesFunction<T = unknown> = (
projectConfigurationFile: string,
options: T | undefined,
context: CreateNodesContext
) => CreateNodesResult | Promise<CreateNodesResult>;
export interface CreateNodesResult {
/**
* A map of project root -> project configuration
*/
projects?: Record<string, Optional<ProjectConfiguration, 'root'>>;
/**
* A map of external node name -> external node. External nodes do not have a root, so the key is their name.
*/
externalNodes?: Record<string, ProjectGraphExternalNode>;
}
/**
* A pair of file patterns and {@link CreateNodesFunction}
*/
export type CreateNodes<T = unknown> = readonly [
configFilePattern: string,
createNodesFunction: CreateNodesFunction<T>
];
/**
* Context for {@link CreateDependencies}
*/
export interface CreateDependenciesContext {
/**
* The external nodes that have been added to the graph.
*/
readonly externalNodes: ProjectGraph['externalNodes'];
/**
* The configuration of each project in the workspace.
*/
readonly projects: Record<string, ProjectConfiguration>;
/**
* The `nx.json` configuration from the workspace
*/
readonly nxJsonConfiguration: NxJsonConfiguration;
/**
* All files in the workspace
*/
readonly fileMap: FileMap;
/**
* Files changes since last invocation
*/
readonly filesToProcess: FileMap;
readonly workspaceRoot: string;
}
/**
* A function which parses files in the workspace to create dependencies in the {@link ProjectGraph}
* Use {@link validateDependency} to validate dependencies
*/
export type CreateDependencies<T = unknown> = (
options: T | undefined,
context: CreateDependenciesContext
) => RawProjectGraphDependency[] | Promise<RawProjectGraphDependency[]>;
/**
* A plugin for Nx which creates nodes and dependencies for the {@link ProjectGraph}
*/
export type NxPluginV2<TOptions = unknown> = {
name: string;
/**
* Provides a file pattern and function that retrieves configuration info from
* those files. e.g. { '**\/*.csproj': buildProjectsFromCsProjFile }
*/
createNodes?: CreateNodes;
// Todo(@AgentEnder): This shouldn't be a full processor, since its only responsible for defining edges between projects. What do we want the API to be?
/**
* Provides a function to analyze files to create dependencies for the {@link ProjectGraph}
*/
createDependencies?: CreateDependencies<TOptions>;
};
export * from './nx-plugin.deprecated';
/**
* A plugin for Nx
*/
export type NxPlugin = NxPluginV1 | NxPluginV2;
export type LoadedNxPlugin = {
plugin: NxPluginV2 & Pick<NxPluginV1, 'processProjectGraph'>;
options?: unknown;
include?: string[];
exclude?: string[];
};
// Short lived cache (cleared between cmd runs)
// holding resolved nx plugin objects.
// Allows loadNxPlugins to be called multiple times w/o
// executing resolution mulitple times.
export const nxPluginCache: Map<string, LoadedNxPlugin['plugin']> = new Map();
export function getPluginPathAndName(
moduleName: string,
paths: string[],
projects: Record<string, ProjectConfiguration>,
root: string
) {
let pluginPath: string;
try {
pluginPath = require.resolve(moduleName, {
paths,
});
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
const plugin = resolveLocalNxPlugin(
moduleName,
readNxJson(root),
projects,
root
);
if (plugin) {
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 extension = path.extname(pluginPath);
// Register the ts-transpiler if we are pointing to a
// plain ts file that's not part of a plugin project
if (extension === '.ts' && !tsNodeAndPathsUnregisterCallback) {
registerPluginTSTranspiler();
}
const { name } =
!['.ts', '.js'].some((x) => x === extension) && // 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 };
}
export async function loadNxPluginAsync(
pluginConfiguration: PluginConfiguration,
paths: string[],
projects: Record<string, ProjectConfiguration>,
root: string
): Promise<LoadedNxPlugin> {
const { plugin: moduleName, options } =
typeof pluginConfiguration === 'object'
? pluginConfiguration
: { plugin: pluginConfiguration, options: undefined };
let pluginModule = nxPluginCache.get(moduleName);
const include =
typeof pluginConfiguration === 'object'
? pluginConfiguration.include
: undefined;
const exclude =
typeof pluginConfiguration === 'object'
? pluginConfiguration.exclude
: undefined;
if (pluginModule) {
return {
plugin: pluginModule,
options,
include,
exclude,
};
}
performance.mark(`Load Nx Plugin: ${moduleName} - start`);
let { pluginPath, name } = await getPluginPathAndName(
moduleName,
paths,
projects,
root
);
const plugin = ensurePluginIsV2(
(await import(pluginPath)) as LoadedNxPlugin['plugin']
);
plugin.name ??= name;
nxPluginCache.set(moduleName, plugin);
performance.mark(`Load Nx Plugin: ${moduleName} - end`);
performance.measure(
`Load Nx Plugin: ${moduleName}`,
`Load Nx Plugin: ${moduleName} - start`,
`Load Nx Plugin: ${moduleName} - end`
);
return {
plugin,
options,
include,
exclude,
};
}
export async function loadNxPlugins(
plugins: PluginConfiguration[],
paths = getNxRequirePaths(),
root = workspaceRoot,
projects?: Record<string, ProjectConfiguration>
): Promise<LoadedNxPlugin[]> {
const result: LoadedNxPlugin[] = [
{ plugin: PackageJsonProjectsNextToProjectJsonPlugin },
];
plugins ??= [];
// When loading plugins for `createNodes`, we don't know what projects exist yet.
// Try resolving plugins
for (const plugin of plugins) {
try {
require.resolve(typeof plugin === 'string' ? plugin : plugin.plugin);
} catch {
// If a plugin cannot be resolved, we will need projects to resolve it
projects ??= await retrieveProjectConfigurationsWithoutPluginInference(
root
);
break;
}
}
for (const plugin of plugins) {
result.push(await loadNxPluginAsync(plugin, paths, projects, root));
}
// We push the nx core node plugins onto the end, s.t. it overwrites any other plugins
result.push(...(await getDefaultPlugins(root)));
return result;
}
export function ensurePluginIsV2(plugin: NxPlugin): NxPluginV2 {
if (isNxPluginV2(plugin)) {
return plugin;
}
if (isNxPluginV1(plugin) && plugin.projectFilePatterns) {
return {
...plugin,
createNodes: [
`*/**/${combineGlobPatterns(plugin.projectFilePatterns)}`,
(configFilePath) => {
const root = dirname(configFilePath);
return {
projects: {
[root]: {
name: toProjectName(configFilePath),
root,
targets: plugin.registerProjectTargets?.(configFilePath),
},
},
};
},
],
};
}
return plugin;
}
export function isNxPluginV2(plugin: NxPlugin): plugin is NxPluginV2 {
return 'createNodes' in plugin || 'createDependencies' in plugin;
}
export function isNxPluginV1(plugin: NxPlugin): plugin is NxPluginV1 {
return 'processProjectGraph' in plugin || 'projectFilePatterns' in plugin;
}
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 nxJson = readNxJson();
const localPluginPath = resolveLocalNxPlugin(
pluginName,
nxJson,
projects
);
if (localPluginPath) {
const localPluginPackageJson = path.join(
localPluginPath.path,
'package.json'
);
return {
path: localPluginPackageJson,
json: readJsonFile(localPluginPackageJson),
};
}
}
throw e;
}
}
/**
* Builds a plugin package and returns the path to output
* @param importPath What is the import path that refers to a potential plugin?
* @returns The path to the built plugin, or null if it doesn't exist
*/
const localPluginCache: Record<
string,
{ path: string; projectConfig: ProjectConfiguration }
> = {};
export function resolveLocalNxPlugin(
importPath: string,
nxJsonConfiguration: NxJsonConfiguration,
projects: Record<string, ProjectConfiguration>,
root = workspaceRoot
): { path: string; projectConfig: ProjectConfiguration } | null {
localPluginCache[importPath] ??= lookupLocalPlugin(
importPath,
nxJsonConfiguration,
projects,
root
);
return localPluginCache[importPath];
}
let tsNodeAndPathsUnregisterCallback: (() => void) | undefined = undefined;
/**
* 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() {
if (!tsNodeAndPathsUnregisterCallback) {
// nx-ignore-next-line
const ts: typeof import('typescript') = require('typescript');
// Get the first tsconfig that matches the allowed set
const tsConfigName = [
join(workspaceRoot, 'tsconfig.base.json'),
join(workspaceRoot, 'tsconfig.json'),
].find((x) => existsSync(x));
const tsConfig: Partial<ts.ParsedCommandLine> = tsConfigName
? readTsConfig(tsConfigName)
: {};
const unregisterTsConfigPaths = registerTsConfigPaths(tsConfigName);
const unregisterTranspiler = registerTranspiler({
experimentalDecorators: true,
emitDecoratorMetadata: true,
...tsConfig.options,
});
tsNodeAndPathsUnregisterCallback = () => {
unregisterTsConfigPaths();
unregisterTranspiler();
};
}
}
/**
* Unregister the ts-node transpiler if it is registered
*/
export function unregisterPluginTSTranspiler() {
if (tsNodeAndPathsUnregisterCallback) {
tsNodeAndPathsUnregisterCallback();
tsNodeAndPathsUnregisterCallback = undefined;
}
}
function lookupLocalPlugin(
importPath: string,
nxJsonConfiguration: NxJsonConfiguration,
projects: Record<string, ProjectConfiguration>,
root = workspaceRoot
) {
const plugin = findNxProjectForImportPath(importPath, projects, root);
if (!plugin) {
return null;
}
if (!tsNodeAndPathsUnregisterCallback) {
registerPluginTSTranspiler();
}
const projectConfig: ProjectConfiguration = projects[plugin];
return { path: path.join(root, projectConfig.root), projectConfig };
}
function findNxProjectForImportPath(
importPath: string,
projects: Record<string, ProjectConfiguration>,
root = workspaceRoot
): string | null {
const tsConfigPaths: Record<string, string[]> = readTsConfigPaths(root);
const possiblePaths = tsConfigPaths[importPath]?.map((p) =>
normalizePath(path.relative(root, path.join(root, p)))
);
if (possiblePaths?.length) {
const projectRootMappings =
createProjectRootMappingsFromProjectConfigurations(projects);
for (const tsConfigPath of possiblePaths) {
const nxProject = findProjectForPath(tsConfigPath, projectRootMappings);
if (nxProject) {
return nxProject;
}
}
if (process.env.NX_VERBOSE_LOGGING) {
console.log(
'Unable to find local plugin',
possiblePaths,
projectRootMappings
);
}
throw new Error(
'Unable to resolve local plugin with import path ' + importPath
);
}
}
let tsconfigPaths: Record<string, string[]>;
function readTsConfigPaths(root: string = workspaceRoot) {
if (!tsconfigPaths) {
const tsconfigPath: string | null = ['tsconfig.base.json', 'tsconfig.json']
.map((x) => path.join(root, x))
.filter((x) => existsSync(x))[0];
if (!tsconfigPath) {
throw new Error('unable to find tsconfig.base.json or tsconfig.json');
}
const { compilerOptions } = readJsonFile(tsconfigPath);
tsconfigPaths = compilerOptions?.paths;
}
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 async function getDefaultPlugins(
root: string
): Promise<LoadedNxPlugin[]> {
const plugins: NxPluginV2[] = [
await import('../plugins/js'),
TargetDefaultsPlugin,
...(shouldMergeAngularProjects(root, false)
? [
await import('../adapter/angular-json').then(
(m) => m.NxAngularJsonPlugin
),
]
: []),
getNxPackageJsonWorkspacesPlugin(root),
ProjectJsonProjectsPlugin,
];
return plugins.map((p) => ({
plugin: p,
}));
}
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

View File

@ -1,19 +1,18 @@
import { workspaceRoot } from '../workspace-root';
import * as chalk from 'chalk'; import * as chalk from 'chalk';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { output } from '../output';
import type { PluginCapabilities } from './models';
import { hasElements } from './shared';
import { readJsonFile } from '../fileutils';
import { getPackageManagerCommand } from '../package-manager';
import {
loadNxPluginAsync,
NxPlugin,
readPluginPackageJson,
} from '../nx-plugin';
import { getNxRequirePaths } from '../installation-directory';
import { PackageJson } from '../package-json';
import { ProjectConfiguration } from '../../config/workspace-json-project-json'; import { ProjectConfiguration } from '../../config/workspace-json-project-json';
import { NxPlugin, readPluginPackageJson } from '../../project-graph/plugins';
import { loadNxPlugin } from '../../project-graph/plugins/loader';
import { readJsonFile } from '../fileutils';
import { getNxRequirePaths } from '../installation-directory';
import { output } from '../output';
import { PackageJson } from '../package-json';
import { getPackageManagerCommand } from '../package-manager';
import { workspaceRoot } from '../workspace-root';
import { hasElements } from './shared';
import type { PluginCapabilities } from './models';
function tryGetCollection<T extends object>( function tryGetCollection<T extends object>(
packageJsonPath: string, packageJsonPath: string,
@ -46,7 +45,7 @@ export async function getPluginCapabilities(
getNxRequirePaths(workspaceRoot) getNxRequirePaths(workspaceRoot)
); );
const pluginModule = includeRuntimeCapabilities const pluginModule = includeRuntimeCapabilities
? await tryGetModule(packageJson, workspaceRoot, projects) ? await tryGetModule(packageJson, workspaceRoot)
: ({} as Record<string, unknown>); : ({} as Record<string, unknown>);
return { return {
name: pluginName, name: pluginName,
@ -99,26 +98,24 @@ export async function getPluginCapabilities(
async function tryGetModule( async function tryGetModule(
packageJson: PackageJson, packageJson: PackageJson,
workspaceRoot: string, workspaceRoot: string
projects: Record<string, ProjectConfiguration>
): Promise<NxPlugin | null> { ): Promise<NxPlugin | null> {
try { try {
return packageJson.generators ?? if (
packageJson.generators ??
packageJson.executors ?? packageJson.executors ??
packageJson['nx-migrations'] ?? packageJson['nx-migrations'] ??
packageJson['schematics'] ?? packageJson['schematics'] ??
packageJson['builders'] packageJson['builders']
? ( ) {
await loadNxPluginAsync( const [pluginPromise] = loadNxPlugin(packageJson.name, workspaceRoot);
packageJson.name, const plugin = await pluginPromise;
getNxRequirePaths(workspaceRoot), return plugin;
projects, } else {
workspaceRoot return {
)
).plugin
: ({
name: packageJson.name, name: packageJson.name,
} as NxPlugin); };
}
} catch { } catch {
return null; return null;
} }

View File

@ -38,6 +38,7 @@ describe('normalizeRollupExecutorOptions', () => {
); );
expect(result.rollupConfig).toHaveLength(1); expect(result.rollupConfig).toHaveLength(1);
expect(result.rollupConfig[0]).toMatch('react'); expect(result.rollupConfig[0]).toMatch('react');
// This fails if the nx repo has been cloned in `/root/...`
expect(result.rollupConfig[0]).not.toMatch(root); expect(result.rollupConfig[0]).not.toMatch(root);
}); });