diff --git a/docs/generated/devkit/CreateNodes.md b/docs/generated/devkit/CreateNodes.md index 766261c911cab..ed46857f91b7c 100644 --- a/docs/generated/devkit/CreateNodes.md +++ b/docs/generated/devkit/CreateNodes.md @@ -1,6 +1,6 @@ # Type alias: CreateNodes\ -Ƭ **CreateNodes**\<`T`\>: readonly [configFilePattern: string, createNodesFunction: CreateNodesFunction\] +Ƭ **CreateNodes**\<`T`\>: readonly [projectFilePattern: string, createNodesFunction: CreateNodesFunction\] A pair of file patterns and [CreateNodesFunction](../../devkit/documents/CreateNodesFunction) diff --git a/docs/generated/devkit/ExpandedPluginConfiguration.md b/docs/generated/devkit/ExpandedPluginConfiguration.md new file mode 100644 index 0000000000000..58ad5aafc1ae5 --- /dev/null +++ b/docs/generated/devkit/ExpandedPluginConfiguration.md @@ -0,0 +1,12 @@ +# Type alias: ExpandedPluginConfiguration + +Ƭ **ExpandedPluginConfiguration**: `Object` + +#### Type declaration + +| Name | Type | +| :--------- | :--------- | +| `exclude?` | `string`[] | +| `include?` | `string`[] | +| `options?` | `unknown` | +| `plugin` | `string` | diff --git a/docs/generated/devkit/NxPluginV2.md b/docs/generated/devkit/NxPluginV2.md index 52eb73cf5ffa8..98d4a37fa4c1d 100644 --- a/docs/generated/devkit/NxPluginV2.md +++ b/docs/generated/devkit/NxPluginV2.md @@ -15,5 +15,5 @@ A plugin for Nx which creates nodes and dependencies for the [ProjectGraph](../. | Name | Type | Description | | :-------------------- | :------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | | `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` | - | diff --git a/docs/generated/devkit/PluginConfiguration.md b/docs/generated/devkit/PluginConfiguration.md index 64b7b8baf8acc..fd55cbdc88092 100644 --- a/docs/generated/devkit/PluginConfiguration.md +++ b/docs/generated/devkit/PluginConfiguration.md @@ -1,3 +1,3 @@ # Type alias: PluginConfiguration -Ƭ **PluginConfiguration**: `string` \| \{ `exclude?`: `string`[] ; `include?`: `string`[] ; `options?`: `unknown` ; `plugin`: `string` } +Ƭ **PluginConfiguration**: `string` \| [`ExpandedPluginConfiguration`](../../devkit/documents/ExpandedPluginConfiguration) diff --git a/docs/generated/devkit/README.md b/docs/generated/devkit/README.md index 25f19c02c7193..af2f0bfb1ead1 100644 --- a/docs/generated/devkit/README.md +++ b/docs/generated/devkit/README.md @@ -69,6 +69,7 @@ It only uses language primitives and immutable objects - [CustomHasher](../../devkit/documents/CustomHasher) - [DynamicDependency](../../devkit/documents/DynamicDependency) - [Executor](../../devkit/documents/Executor) +- [ExpandedPluginConfiguration](../../devkit/documents/ExpandedPluginConfiguration) - [Generator](../../devkit/documents/Generator) - [GeneratorCallback](../../devkit/documents/GeneratorCallback) - [Hasher](../../devkit/documents/Hasher) diff --git a/docs/generated/devkit/logger.md b/docs/generated/devkit/logger.md index 6ef004c9e6645..c2cf2a082d1eb 100644 --- a/docs/generated/devkit/logger.md +++ b/docs/generated/devkit/logger.md @@ -4,11 +4,12 @@ #### Type declaration -| Name | Type | -| :------ | :-------------------------- | -| `debug` | (...`s`: `any`[]) => `void` | -| `error` | (`s`: `any`) => `void` | -| `fatal` | (...`s`: `any`[]) => `void` | -| `info` | (`s`: `any`) => `void` | -| `log` | (...`s`: `any`[]) => `void` | -| `warn` | (`s`: `any`) => `void` | +| Name | Type | +| :-------- | :-------------------------- | +| `debug` | (...`s`: `any`[]) => `void` | +| `error` | (`s`: `any`) => `void` | +| `fatal` | (...`s`: `any`[]) => `void` | +| `info` | (`s`: `any`) => `void` | +| `log` | (...`s`: `any`[]) => `void` | +| `verbose` | (...`s`: `any`[]) => `void` | +| `warn` | (`s`: `any`) => `void` | diff --git a/docs/generated/packages/devkit/documents/nx_devkit.md b/docs/generated/packages/devkit/documents/nx_devkit.md index 25f19c02c7193..af2f0bfb1ead1 100644 --- a/docs/generated/packages/devkit/documents/nx_devkit.md +++ b/docs/generated/packages/devkit/documents/nx_devkit.md @@ -69,6 +69,7 @@ It only uses language primitives and immutable objects - [CustomHasher](../../devkit/documents/CustomHasher) - [DynamicDependency](../../devkit/documents/DynamicDependency) - [Executor](../../devkit/documents/Executor) +- [ExpandedPluginConfiguration](../../devkit/documents/ExpandedPluginConfiguration) - [Generator](../../devkit/documents/Generator) - [GeneratorCallback](../../devkit/documents/GeneratorCallback) - [Hasher](../../devkit/documents/Hasher) diff --git a/packages/devkit/src/utils/convert-nx-executor.spec.ts b/packages/devkit/src/utils/convert-nx-executor.spec.ts index 2f74b54b9f605..c61f00ac7fcef 100644 --- a/packages/devkit/src/utils/convert-nx-executor.spec.ts +++ b/packages/devkit/src/utils/convert-nx-executor.spec.ts @@ -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. -// Mock `loadNxPlugins` function to prevent them from loading. -jest.mock('nx/src/utils/nx-plugin', () => ({ - loadNxPlugins: () => Promise.resolve([]), -})); - +import { TempFs } from '../../internal-testing-utils'; import { convertNxExecutor } from './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 () => { // ARRANGE const { schema } = require('@angular-devkit/core'); const { 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 registry = new schema.CoreSchemaRegistry(); registry.addPostTransform(schema.transforms.addUndefinedDefaults); const testArchitectHost = new TestingArchitectHost(); + testArchitectHost.workspaceRoot = fs.tempDir; const architect = new Architect(testArchitectHost, registry); const convertedExecutor = convertNxExecutor(echoExecutor); diff --git a/packages/nx/bin/post-install.ts b/packages/nx/bin/post-install.ts index eb1d0200f9ceb..06dbac66591b0 100644 --- a/packages/nx/bin/post-install.ts +++ b/packages/nx/bin/post-install.ts @@ -11,10 +11,10 @@ import { readNxJson } from '../src/config/nx-json'; import { setupWorkspaceContext } from '../src/utils/workspace-context'; (async () => { + const start = new Date(); try { setupWorkspaceContext(workspaceRoot); if (isMainNxPackage() && fileExists(join(workspaceRoot, 'nx.json'))) { - const b = new Date(); assertSupportedPlatform(); 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) { if (process.env.NX_VERBOSE_LOGGING === 'true') { 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` + ); + } } })(); diff --git a/packages/nx/plugins/package-json.ts b/packages/nx/plugins/package-json.ts index 9ba453f652994..936ca3e8970a4 100644 --- a/packages/nx/plugins/package-json.ts +++ b/packages/nx/plugins/package-json.ts @@ -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 { createNodeFromPackageJson } from '../src/plugins/package-json-workspaces'; diff --git a/packages/nx/src/adapter/angular-json.ts b/packages/nx/src/adapter/angular-json.ts index b509554da8874..585cd66c1e623 100644 --- a/packages/nx/src/adapter/angular-json.ts +++ b/packages/nx/src/adapter/angular-json.ts @@ -2,7 +2,7 @@ import { existsSync } from 'fs'; import * as path from 'path'; import { readJsonFile } from '../utils/fileutils'; 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'; @@ -16,6 +16,8 @@ export const NxAngularJsonPlugin: NxPluginV2 = { ], }; +export default NxAngularJsonPlugin; + export function shouldMergeAngularProjects( root: string, includeProjectsFromAngularJson: boolean diff --git a/packages/nx/src/adapter/ngcli-adapter.ts b/packages/nx/src/adapter/ngcli-adapter.ts index 1f1d137ad3e28..a0445d98c9287 100644 --- a/packages/nx/src/adapter/ngcli-adapter.ts +++ b/packages/nx/src/adapter/ngcli-adapter.ts @@ -59,7 +59,7 @@ import { ExecutorsJson, TaskGraphExecutor, } from '../config/misc-interfaces'; -import { readPluginPackageJson } from '../utils/nx-plugin'; +import { readPluginPackageJson } from '../project-graph/plugins'; import { getImplementationFactory, resolveImplementation, diff --git a/packages/nx/src/command-line/generate/generator-utils.ts b/packages/nx/src/command-line/generate/generator-utils.ts index 1fffe47dcbb00..20d57f4eff259 100644 --- a/packages/nx/src/command-line/generate/generator-utils.ts +++ b/packages/nx/src/command-line/generate/generator-utils.ts @@ -10,7 +10,7 @@ import { resolveSchema, } from '../../config/schema-utils'; import { readJsonFile } from '../../utils/fileutils'; -import { readPluginPackageJson } from '../../utils/nx-plugin'; +import { readPluginPackageJson } from '../../project-graph/plugins'; export function getGeneratorInformation( collectionName: string, diff --git a/packages/nx/src/command-line/run/executor-utils.ts b/packages/nx/src/command-line/run/executor-utils.ts index e12f7e752cd83..ecbac0c0f8bea 100644 --- a/packages/nx/src/command-line/run/executor-utils.ts +++ b/packages/nx/src/command-line/run/executor-utils.ts @@ -1,6 +1,6 @@ import { dirname, join } from 'path'; -import { readPluginPackageJson } from '../../utils/nx-plugin'; +import { readPluginPackageJson } from '../../project-graph/plugins'; import { CustomHasher, Executor, diff --git a/packages/nx/src/config/nx-json.ts b/packages/nx/src/config/nx-json.ts index 7a4e7f7497084..83c7d2bac22d6 100644 --- a/packages/nx/src/config/nx-json.ts +++ b/packages/nx/src/config/nx-json.ts @@ -436,14 +436,14 @@ export interface NxJsonConfiguration { useInferencePlugins?: boolean; } -export type PluginConfiguration = - | string - | { - plugin: string; - options?: unknown; - include?: string[]; - exclude?: string[]; - }; +export type PluginConfiguration = string | ExpandedPluginConfiguration; + +export type ExpandedPluginConfiguration = { + plugin: string; + options?: unknown; + include?: string[]; + exclude?: string[]; +}; export function readNxJson(root: string = workspaceRoot): NxJsonConfiguration { const nxJson = join(root, 'nx.json'); diff --git a/packages/nx/src/config/schema-utils.ts b/packages/nx/src/config/schema-utils.ts index 6c92129a643d7..30d7d083130f0 100644 --- a/packages/nx/src/config/schema-utils.ts +++ b/packages/nx/src/config/schema-utils.ts @@ -1,6 +1,6 @@ import { existsSync } from 'fs'; 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. diff --git a/packages/nx/src/config/workspaces.spec.ts b/packages/nx/src/config/workspaces.spec.ts index 81d3d1a183171..f7145e5fc5b60 100644 --- a/packages/nx/src/config/workspaces.spec.ts +++ b/packages/nx/src/config/workspaces.spec.ts @@ -1,22 +1,9 @@ -import { toProjectName, Workspaces } from './workspaces'; +import { toProjectName } from './workspaces'; import { TempFs } from '../internal-testing-utils/temp-fs'; import { withEnvironmentVariables } from '../internal-testing-utils/with-environment'; import { retrieveProjectConfigurations } from '../project-graph/utils/retrieve-workspace-files'; import { readNxJson } from './configuration'; - -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: {}, - }, - }, -}); +import { loadNxPlugins } from '../project-graph/plugins/internal-api'; describe('Workspaces', () => { let fs: TempFs; @@ -48,10 +35,23 @@ describe('Workspaces', () => { 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({ name: 'my-package', root: 'packages/my-package', diff --git a/packages/nx/src/daemon/server/handle-request-project-graph.ts b/packages/nx/src/daemon/server/handle-request-project-graph.ts index 449d57a693805..1dc4507383c82 100644 --- a/packages/nx/src/daemon/server/handle-request-project-graph.ts +++ b/packages/nx/src/daemon/server/handle-request-project-graph.ts @@ -3,6 +3,8 @@ import { serializeResult } from '../socket-utils'; import { serverLogger } from './logger'; import { getCachedSerializedProjectGraphPromise } from './project-graph-incremental-recomputation'; import { HandlerResult } from './server'; +import { getPlugins } from './plugins'; +import { readNxJson } from '../../config/nx-json'; export async function handleRequestProjectGraph(): Promise { try { diff --git a/packages/nx/src/daemon/server/plugins.ts b/packages/nx/src/daemon/server/plugins.ts new file mode 100644 index 0000000000000..38f4b598678e8 --- /dev/null +++ b/packages/nx/src/daemon/server/plugins.ts @@ -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; +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(); +} diff --git a/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts b/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts index f87a6b892f28c..da1285d192041 100644 --- a/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts +++ b/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts @@ -32,11 +32,11 @@ import { workspaceRoot } from '../../utils/workspace-root'; import { notifyFileWatcherSockets } from './file-watching/file-watcher-sockets'; import { serverLogger } from './logger'; import { NxWorkspaceFilesExternals } from '../../native'; -import { - ConfigurationResult, - ProjectConfigurationsError, -} from '../../project-graph/utils/project-configuration-utils'; +import { ConfigurationResult } from '../../project-graph/utils/project-configuration-utils'; 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 { error: Error | null; @@ -78,14 +78,15 @@ export async function getCachedSerializedProjectGraphPromise(): Promise 0) { @@ -209,7 +210,9 @@ async function processCollectedUpdatedAndDeletedFiles( } } -async function processFilesAndCreateAndSerializeProjectGraph(): Promise { +async function processFilesAndCreateAndSerializeProjectGraph( + plugins: LoadedNxPlugin[] +): Promise { try { performance.mark('hash-watched-changes-start'); const updatedFiles = [...collectedUpdatedFiles.values()]; @@ -227,14 +230,17 @@ async function processFilesAndCreateAndSerializeProjectGraph(): Promise { const promise = async () => { const nxJsonConfiguration = readNxJson(builderContext.workspaceRoot); + + const [plugins, cleanup] = await loadNxPlugins( + nxJsonConfiguration.plugins, + builderContext.workspaceRoot + ); const projectsConfigurations: ProjectsConfigurations = { version: 2, projects: ( await retrieveProjectConfigurations( + plugins, builderContext.workspaceRoot, nxJsonConfiguration ) ).projects, }; + cleanup(); const context: ExecutorContext = { root: builderContext.workspaceRoot, projectName: builderContext.target.project, diff --git a/packages/nx/src/generators/utils/project-configuration.ts b/packages/nx/src/generators/utils/project-configuration.ts index 172f71abcd120..8cb7b51fa6f29 100644 --- a/packages/nx/src/generators/utils/project-configuration.ts +++ b/packages/nx/src/generators/utils/project-configuration.ts @@ -4,7 +4,7 @@ import { basename, join, relative } from 'path'; import { buildProjectConfigurationFromPackageJson, getGlobPatternsFromPackageManagerWorkspaces, - getNxPackageJsonWorkspacesPlugin, + createNodes as packageJsonWorkspacesCreateNodes, } from '../../plugins/package-json-workspaces'; import { buildProjectFromProjectJson, @@ -28,6 +28,7 @@ import { readJson, writeJson } from './json'; import { readNxJson } from './nx-json'; import type { Tree } from '../tree'; +import { NxPlugin } from '../../project-graph/plugins'; export { readNxJson, updateNxJson } from './nx-json'; @@ -200,8 +201,8 @@ function readAndCombineAllProjectConfigurations(tree: Tree): { ), ]; const projectGlobPatterns = configurationGlobs([ - { plugin: ProjectJsonProjectsPlugin }, - { plugin: getNxPackageJsonWorkspacesPlugin(tree.root) }, + ProjectJsonProjectsPlugin, + { createNodes: packageJsonWorkspacesCreateNodes }, ]); const globbedFiles = globWithWorkspaceContext(tree.root, projectGlobPatterns); const createdFiles = findCreatedProjectFiles(tree, patterns); diff --git a/packages/nx/src/migrations/update-15-1-0/set-project-names.ts b/packages/nx/src/migrations/update-15-1-0/set-project-names.ts index 24c9e426ac331..f61e283fa3064 100644 --- a/packages/nx/src/migrations/update-15-1-0/set-project-names.ts +++ b/packages/nx/src/migrations/update-15-1-0/set-project-names.ts @@ -4,14 +4,15 @@ import { dirname } from 'path'; import { readJson, writeJson } from '../../generators/utils/json'; import { formatChangedFilesWithPrettierIfAvailable } from '../../generators/internal-utils/format-changed-files-with-prettier-if-available'; 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) { const nxJson = readNxJson(tree); - const projectFiles = await retrieveProjectConfigurationPaths( - tree.root, - await loadNxPlugins(nxJson?.plugins) + const [plugins, cleanup] = await loadNxPlugins( + nxJson?.plugins ?? [], + tree.root ); + const projectFiles = retrieveProjectConfigurationPaths(tree.root, plugins); const projectJsons = projectFiles.filter((f) => f.endsWith('project.json')); for (let f of projectJsons) { @@ -22,6 +23,7 @@ export default async function (tree: Tree) { } } await formatChangedFilesWithPrettierIfAvailable(tree); + cleanup(); } function toProjectName(directory: string, nxJson: any): string { diff --git a/packages/nx/src/plugins/js/index.ts b/packages/nx/src/plugins/js/index.ts index 738013b4d40a3..74409efd7249c 100644 --- a/packages/nx/src/plugins/js/index.ts +++ b/packages/nx/src/plugins/js/index.ts @@ -9,7 +9,7 @@ import { CreateDependencies, CreateDependenciesContext, CreateNodes, -} from '../../utils/nx-plugin'; +} from '../../project-graph/plugins'; import { getLockFileDependencies, getLockFileName, diff --git a/packages/nx/src/plugins/js/lock-file/lock-file.ts b/packages/nx/src/plugins/js/lock-file/lock-file.ts index 21687585aff12..f2b1d162b7ee6 100644 --- a/packages/nx/src/plugins/js/lock-file/lock-file.ts +++ b/packages/nx/src/plugins/js/lock-file/lock-file.ts @@ -40,7 +40,7 @@ import { readJsonFile } from '../../../utils/fileutils'; import { CreateDependenciesContext, CreateNodesContext, -} from '../../../utils/nx-plugin'; +} from '../../../project-graph/plugins'; const YARN_LOCK_FILE = 'yarn.lock'; const NPM_LOCK_FILE = 'package-lock.json'; diff --git a/packages/nx/src/plugins/js/lock-file/npm-parser.spec.ts b/packages/nx/src/plugins/js/lock-file/npm-parser.spec.ts index 2ebb0dbb3a8b7..817e100c023d7 100644 --- a/packages/nx/src/plugins/js/lock-file/npm-parser.spec.ts +++ b/packages/nx/src/plugins/js/lock-file/npm-parser.spec.ts @@ -8,7 +8,7 @@ import { pruneProjectGraph } from './project-graph-pruning'; import { vol } from 'memfs'; import { ProjectGraph } from '../../../config/project-graph'; import { ProjectGraphBuilder } from '../../../project-graph/project-graph-builder'; -import { CreateDependenciesContext } from '../../../utils/nx-plugin'; +import { CreateDependenciesContext } from '../../../project-graph/plugins'; jest.mock('fs', () => { const memFs = require('memfs').fs; diff --git a/packages/nx/src/plugins/js/lock-file/npm-parser.ts b/packages/nx/src/plugins/js/lock-file/npm-parser.ts index c048ce515c44b..00ce0290631a9 100644 --- a/packages/nx/src/plugins/js/lock-file/npm-parser.ts +++ b/packages/nx/src/plugins/js/lock-file/npm-parser.ts @@ -13,7 +13,7 @@ import { ProjectGraphExternalNode, } from '../../../config/project-graph'; import { hashArray } from '../../../hasher/file-hasher'; -import { CreateDependenciesContext } from '../../../utils/nx-plugin'; +import { CreateDependenciesContext } from '../../../project-graph/plugins'; /** * NPM diff --git a/packages/nx/src/plugins/js/lock-file/pnpm-parser.spec.ts b/packages/nx/src/plugins/js/lock-file/pnpm-parser.spec.ts index bdbcea297d715..52ee056631895 100644 --- a/packages/nx/src/plugins/js/lock-file/pnpm-parser.spec.ts +++ b/packages/nx/src/plugins/js/lock-file/pnpm-parser.spec.ts @@ -11,7 +11,7 @@ import { ProjectGraphBuilder, RawProjectGraphDependency, } from '../../../project-graph/project-graph-builder'; -import { CreateDependenciesContext } from '../../../utils/nx-plugin'; +import { CreateDependenciesContext } from '../../../project-graph/plugins'; jest.mock('fs', () => { const memFs = require('memfs').fs; diff --git a/packages/nx/src/plugins/js/lock-file/pnpm-parser.ts b/packages/nx/src/plugins/js/lock-file/pnpm-parser.ts index 8f6a6692ccaf3..068fddaae4e5e 100644 --- a/packages/nx/src/plugins/js/lock-file/pnpm-parser.ts +++ b/packages/nx/src/plugins/js/lock-file/pnpm-parser.ts @@ -25,7 +25,7 @@ import { ProjectGraphExternalNode, } from '../../../config/project-graph'; 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 let keyMap = new Map(); diff --git a/packages/nx/src/plugins/js/lock-file/yarn-parser.spec.ts b/packages/nx/src/plugins/js/lock-file/yarn-parser.spec.ts index 41e83ac85c542..2e920bb5c12ea 100644 --- a/packages/nx/src/plugins/js/lock-file/yarn-parser.spec.ts +++ b/packages/nx/src/plugins/js/lock-file/yarn-parser.spec.ts @@ -9,7 +9,7 @@ import { vol } from 'memfs'; import { ProjectGraph } from '../../../config/project-graph'; import { PackageJson } from '../../../utils/package-json'; import { ProjectGraphBuilder } from '../../../project-graph/project-graph-builder'; -import { CreateDependenciesContext } from '../../../utils/nx-plugin'; +import { CreateDependenciesContext } from '../../../project-graph/plugins'; jest.mock('fs', () => { const memFs = require('memfs').fs; diff --git a/packages/nx/src/plugins/js/lock-file/yarn-parser.ts b/packages/nx/src/plugins/js/lock-file/yarn-parser.ts index 7098220352778..d96f3e5125335 100644 --- a/packages/nx/src/plugins/js/lock-file/yarn-parser.ts +++ b/packages/nx/src/plugins/js/lock-file/yarn-parser.ts @@ -14,7 +14,7 @@ import { } from '../../../config/project-graph'; import { hashArray } from '../../../hasher/file-hasher'; import { sortObjectByKeys } from '../../../utils/object-sort'; -import { CreateDependenciesContext } from '../../../utils/nx-plugin'; +import { CreateDependenciesContext } from '../../../project-graph/plugins'; /** * Yarn diff --git a/packages/nx/src/plugins/js/project-graph/build-dependencies/build-dependencies.ts b/packages/nx/src/plugins/js/project-graph/build-dependencies/build-dependencies.ts index 09d8da40396e6..87530bc624ca8 100644 --- a/packages/nx/src/plugins/js/project-graph/build-dependencies/build-dependencies.ts +++ b/packages/nx/src/plugins/js/project-graph/build-dependencies/build-dependencies.ts @@ -1,6 +1,6 @@ import { buildExplicitTypeScriptDependencies } from './explicit-project-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'; export function buildExplicitDependencies( diff --git a/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-package-json-dependencies.spec.ts b/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-package-json-dependencies.spec.ts index 5cf832fb6f26e..bd253f3c7ce91 100644 --- a/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-package-json-dependencies.spec.ts +++ b/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-package-json-dependencies.spec.ts @@ -6,7 +6,7 @@ import { buildExplicitPackageJsonDependencies } from './explicit-package-json-de import { ProjectGraphProjectNode } from '../../../../config/project-graph'; import { ProjectGraphBuilder } from '../../../../project-graph/project-graph-builder'; 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'; describe('explicit package json dependencies', () => { diff --git a/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-package-json-dependencies.ts b/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-package-json-dependencies.ts index 58ba45c8843b8..3724c4498a4a3 100644 --- a/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-package-json-dependencies.ts +++ b/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-package-json-dependencies.ts @@ -9,7 +9,7 @@ import { } from '../../../../config/workspace-json-project-json'; import { NxJsonConfiguration } from '../../../../config/nx-json'; import { PackageJson } from '../../../../utils/package-json'; -import { CreateDependenciesContext } from '../../../../utils/nx-plugin'; +import { CreateDependenciesContext } from '../../../../project-graph/plugins'; import { RawProjectGraphDependency, validateDependency, diff --git a/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-project-dependencies.spec.ts b/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-project-dependencies.spec.ts index 0cf2c22dd9d0b..d6b5e5a0f77d5 100644 --- a/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-project-dependencies.spec.ts +++ b/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-project-dependencies.spec.ts @@ -1,15 +1,16 @@ import { TempFs } from '../../../../internal-testing-utils/temp-fs'; + const tempFs = new TempFs('explicit-project-deps'); import { ProjectGraphBuilder } from '../../../../project-graph/project-graph-builder'; import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies'; import { - retrieveProjectConfigurationPaths, retrieveProjectConfigurations, retrieveWorkspaceFiles, } 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 { loadNxPlugins } from '../../../../project-graph/plugins/internal-api'; // projectName => tsconfig import path const dependencyProjectNamesToImportPaths = { @@ -564,10 +565,13 @@ async function createContext( setupWorkspaceContext(tempFs.tempDir); + const [plugins, cleanup] = await loadNxPlugins([], tempFs.tempDir); const { projects, projectRootMap } = await retrieveProjectConfigurations( + plugins, tempFs.tempDir, nxJson ); + cleanup(); const { fileMap } = await retrieveWorkspaceFiles( tempFs.tempDir, diff --git a/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-project-dependencies.ts b/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-project-dependencies.ts index 948e917fd09b0..cffc478a3a685 100644 --- a/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-project-dependencies.ts +++ b/packages/nx/src/plugins/js/project-graph/build-dependencies/explicit-project-dependencies.ts @@ -6,7 +6,7 @@ import { import { join, relative } from 'path'; import { workspaceRoot } from '../../../../utils/workspace-root'; import { normalizePath } from '../../../../utils/path'; -import { CreateDependenciesContext } from '../../../../utils/nx-plugin'; +import { CreateDependenciesContext } from '../../../../project-graph/plugins'; import { RawProjectGraphDependency, validateDependency, diff --git a/packages/nx/src/plugins/package-json-workspaces/create-nodes.ts b/packages/nx/src/plugins/package-json-workspaces/create-nodes.ts index d1547b5deb09f..cff3d81eeedad 100644 --- a/packages/nx/src/plugins/package-json-workspaces/create-nodes.ts +++ b/packages/nx/src/plugins/package-json-workspaces/create-nodes.ts @@ -8,48 +8,43 @@ import { toProjectName } from '../../config/workspaces'; import { readJsonFile, readYamlFile } from '../../utils/fileutils'; import { combineGlobPatterns } from '../../utils/globs'; import { NX_PREFIX } from '../../utils/logger'; -import { NxPluginV2 } from '../../utils/nx-plugin'; import { output } from '../../utils/output'; import { PackageJson, readTargetsFromPackageJson, } from '../../utils/package-json'; import { joinPathFragments } from '../../utils/path'; - -export function getNxPackageJsonWorkspacesPlugin(root: string): NxPluginV2 { - const readJson = (f) => readJsonFile(join(root, f)); - const patterns = getGlobPatternsFromPackageManagerWorkspaces(root, 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('!')); - let positivePatterns = patterns.filter((p) => !p.startsWith('!')); - - if ( - // There are some negative patterns - negativePatterns.length > 0 && - // No positive patterns - (positivePatterns.length === 0 || - // Or only a single positive pattern that is the default coming from root package - (positivePatterns.length === 1 && positivePatterns[0] === 'package.json')) - ) { - positivePatterns.push('**/package.json'); - } - - return { - name: 'nx/core/package-json-workspaces', - createNodes: [ - combineGlobPatterns(positivePatterns), - (p) => { - if (!negativePatterns.some((negative) => minimatch(p, negative))) { - return createNodeFromPackageJson(p, root); - } - // A negative pattern matched, so we should not create a node for this package.json - return {}; - }, - ], - }; +import { workspaceRoot } from '../../utils/workspace-root'; +import { CreateNodes } from '../../project-graph/plugins'; + +const readJson = (f) => readJsonFile(join(workspaceRoot, f)); +const patterns = getGlobPatternsFromPackageManagerWorkspaces( + workspaceRoot, + readJson +); + +const negativePatterns = patterns.filter((p) => p.startsWith('!')); +const positivePatterns = patterns.filter((p) => !p.startsWith('!')); +if ( + // There are some negative patterns + negativePatterns.length > 0 && + // No positive patterns + (positivePatterns.length === 0 || + // Or only a single positive pattern that is the default coming from root package + (positivePatterns.length === 1 && positivePatterns[0] === 'package.json')) +) { + positivePatterns.push('**/package.json'); } +export const createNodes: CreateNodes = [ + combineGlobPatterns(positivePatterns), + (p, _, { workspaceRoot }) => { + if (!negativePatterns.some((negative) => minimatch(p, negative))) { + return createNodeFromPackageJson(p, workspaceRoot); + } + // A negative pattern matched, so we should not create a node for this package.json + return {}; + }, +]; export function createNodeFromPackageJson(pkgJsonPath: string, root: string) { const json: PackageJson = readJsonFile(join(root, pkgJsonPath)); diff --git a/packages/nx/src/plugins/package-json-workspaces/index.ts b/packages/nx/src/plugins/package-json-workspaces/index.ts index e675dd81f1475..7ac34ae661e87 100644 --- a/packages/nx/src/plugins/package-json-workspaces/index.ts +++ b/packages/nx/src/plugins/package-json-workspaces/index.ts @@ -1 +1,2 @@ export * from './create-nodes'; +export const name = 'nx/core/package-json-workspaces'; diff --git a/packages/nx/src/plugins/project-json/build-nodes/package-json-next-to-project-json.spec.ts b/packages/nx/src/plugins/project-json/build-nodes/package-json-next-to-project-json.spec.ts index c4367d0066a0a..cc55fa3f7f968 100644 --- a/packages/nx/src/plugins/project-json/build-nodes/package-json-next-to-project-json.spec.ts +++ b/packages/nx/src/plugins/project-json/build-nodes/package-json-next-to-project-json.spec.ts @@ -3,7 +3,7 @@ import * as memfs from 'memfs'; import '../../../internal-testing-utils/mock-fs'; import { PackageJsonProjectsNextToProjectJsonPlugin } from './package-json-next-to-project-json'; -import { CreateNodesContext } from '../../../utils/nx-plugin'; +import { CreateNodesContext } from '../../../project-graph/plugins'; const { createNodes } = PackageJsonProjectsNextToProjectJsonPlugin; describe('nx project.json plugin', () => { diff --git a/packages/nx/src/plugins/project-json/build-nodes/package-json-next-to-project-json.ts b/packages/nx/src/plugins/project-json/build-nodes/package-json-next-to-project-json.ts index bb241cf307a2b..19421aea54b1f 100644 --- a/packages/nx/src/plugins/project-json/build-nodes/package-json-next-to-project-json.ts +++ b/packages/nx/src/plugins/project-json/build-nodes/package-json-next-to-project-json.ts @@ -1,6 +1,6 @@ import { dirname, join } from 'path'; import { existsSync } from 'fs'; -import { NxPluginV2 } from '../../../utils/nx-plugin'; +import { NxPluginV2 } from '../../../project-graph/plugins'; import { readJsonFile } from '../../../utils/fileutils'; import { ProjectConfiguration } from '../../../config/workspace-json-project-json'; import { @@ -33,6 +33,8 @@ export const PackageJsonProjectsNextToProjectJsonPlugin: NxPluginV2 = { ], }; +export default PackageJsonProjectsNextToProjectJsonPlugin; + function createProjectFromPackageJsonNextToProjectJson( projectJsonPath: string, workspaceRoot: string diff --git a/packages/nx/src/plugins/project-json/build-nodes/project-json.spec.ts b/packages/nx/src/plugins/project-json/build-nodes/project-json.spec.ts index 9d6addb0dbcee..9a2141cc660f7 100644 --- a/packages/nx/src/plugins/project-json/build-nodes/project-json.spec.ts +++ b/packages/nx/src/plugins/project-json/build-nodes/project-json.spec.ts @@ -3,7 +3,7 @@ import * as memfs from 'memfs'; import '../../../internal-testing-utils/mock-fs'; import { ProjectJsonProjectsPlugin } from './project-json'; -import { CreateNodesContext } from '../../../utils/nx-plugin'; +import { CreateNodesContext } from '../../../project-graph/plugins'; const { createNodes } = ProjectJsonProjectsPlugin; describe('nx project.json plugin', () => { diff --git a/packages/nx/src/plugins/project-json/build-nodes/project-json.ts b/packages/nx/src/plugins/project-json/build-nodes/project-json.ts index 90048a133f8b6..9dfc44fcbc262 100644 --- a/packages/nx/src/plugins/project-json/build-nodes/project-json.ts +++ b/packages/nx/src/plugins/project-json/build-nodes/project-json.ts @@ -3,7 +3,7 @@ import { dirname, join } from 'node:path'; import { ProjectConfiguration } from '../../../config/workspace-json-project-json'; import { toProjectName } from '../../../config/workspaces'; import { readJsonFile } from '../../../utils/fileutils'; -import { NxPluginV2 } from '../../../utils/nx-plugin'; +import { NxPluginV2 } from '../../../project-graph/plugins'; export const ProjectJsonProjectsPlugin: NxPluginV2 = { name: 'nx/core/project-json', @@ -23,6 +23,8 @@ export const ProjectJsonProjectsPlugin: NxPluginV2 = { ], }; +export default ProjectJsonProjectsPlugin; + export function buildProjectFromProjectJson( json: Partial, path: string diff --git a/packages/nx/src/plugins/target-defaults/symbols.ts b/packages/nx/src/plugins/target-defaults/symbols.ts new file mode 100644 index 0000000000000..e42ced0cc9c8e --- /dev/null +++ b/packages/nx/src/plugins/target-defaults/symbols.ts @@ -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'; diff --git a/packages/nx/src/plugins/target-defaults/target-defaults-plugin.spec.ts b/packages/nx/src/plugins/target-defaults/target-defaults-plugin.spec.ts index 3bc0c97404225..8dd59a38c9e37 100644 --- a/packages/nx/src/plugins/target-defaults/target-defaults-plugin.spec.ts +++ b/packages/nx/src/plugins/target-defaults/target-defaults-plugin.spec.ts @@ -3,7 +3,7 @@ import * as memfs from 'memfs'; import '../../../src/internal-testing-utils/mock-fs'; import { getTargetInfo, TargetDefaultsPlugin } from './target-defaults-plugin'; -import { CreateNodesContext } from '../../utils/nx-plugin'; +import { CreateNodesContext } from '../../project-graph/plugins'; const { createNodes: [, createNodesFn], } = TargetDefaultsPlugin; diff --git a/packages/nx/src/plugins/target-defaults/target-defaults-plugin.ts b/packages/nx/src/plugins/target-defaults/target-defaults-plugin.ts index e16c81e9abf6e..f383e2c477fd8 100644 --- a/packages/nx/src/plugins/target-defaults/target-defaults-plugin.ts +++ b/packages/nx/src/plugins/target-defaults/target-defaults-plugin.ts @@ -8,31 +8,13 @@ import { } from '../../config/workspace-json-project-json'; import { readJsonFile } from '../../utils/fileutils'; import { combineGlobPatterns } from '../../utils/globs'; -import { NxPluginV2 } from '../../utils/nx-plugin'; +import { NxPluginV2 } from '../../project-graph/plugins'; import { PackageJson, readTargetsFromPackageJson, } from '../../utils/package-json'; import { getGlobPatternsFromPackageManagerWorkspaces } from '../package-json-workspaces'; - -/** - * 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'; +import { ONLY_MODIFIES_EXISTING_TARGET, OVERRIDE_SOURCE_FILE } from './symbols'; export const TargetDefaultsPlugin: NxPluginV2 = { name: 'nx/core/target-defaults', @@ -127,6 +109,8 @@ export const TargetDefaultsPlugin: NxPluginV2 = { ], }; +export default TargetDefaultsPlugin; + function getExecutorToTargetMap( packageJsonTargets: Record, projectJsonTargets: Record diff --git a/packages/nx/src/project-graph/affected/locators/project-glob-changes.spec.ts b/packages/nx/src/project-graph/affected/locators/project-glob-changes.spec.ts index 2c8d3078cc9bc..31578fd73cb10 100644 --- a/packages/nx/src/project-graph/affected/locators/project-glob-changes.spec.ts +++ b/packages/nx/src/project-graph/affected/locators/project-glob-changes.spec.ts @@ -1,7 +1,7 @@ import { ProjectGraphProjectNode } from '../../../config/project-graph'; 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 { getTouchedProjectsFromProjectGlobChanges } from './project-glob-changes'; diff --git a/packages/nx/src/project-graph/affected/locators/project-glob-changes.ts b/packages/nx/src/project-graph/affected/locators/project-glob-changes.ts index afd7c4f8d7db0..6c54137366a25 100644 --- a/packages/nx/src/project-graph/affected/locators/project-glob-changes.ts +++ b/packages/nx/src/project-graph/affected/locators/project-glob-changes.ts @@ -1,24 +1,16 @@ import { TouchedProjectLocator } from '../affected-project-graph-models'; import { minimatch } from 'minimatch'; import { workspaceRoot } from '../../../utils/workspace-root'; -import { getNxRequirePaths } from '../../../utils/installation-directory'; import { join } from 'path'; import { existsSync } from 'fs'; import { configurationGlobs } from '../../utils/retrieve-workspace-files'; -import { loadNxPlugins } from '../../../utils/nx-plugin'; +import { loadNxPlugins } from '../../plugins/internal-api'; import { combineGlobPatterns } from '../../../utils/globs'; export const getTouchedProjectsFromProjectGlobChanges: TouchedProjectLocator = async (touchedFiles, projectGraphNodes, nxJson): Promise => { - const globPattern = combineGlobPatterns( - configurationGlobs( - await loadNxPlugins( - nxJson?.plugins, - getNxRequirePaths(workspaceRoot), - workspaceRoot - ) - ) - ); + const [plugins] = await loadNxPlugins(nxJson?.plugins ?? [], workspaceRoot); + const globPattern = combineGlobPatterns(configurationGlobs(plugins)); const touchedProjects = new Set(); for (const touchedFile of touchedFiles) { diff --git a/packages/nx/src/project-graph/build-project-graph.ts b/packages/nx/src/project-graph/build-project-graph.ts index 8e0a6103f5248..841226a26d189 100644 --- a/packages/nx/src/project-graph/build-project-graph.ts +++ b/packages/nx/src/project-graph/build-project-graph.ts @@ -13,12 +13,9 @@ import { } from './nx-deps-cache'; import { applyImplicitDependencies } from './utils/implicit-project-dependencies'; import { normalizeProjectNodes } from './utils/normalize-project-nodes'; -import { - CreateDependenciesContext, - isNxPluginV1, - isNxPluginV2, - loadNxPlugins, -} from '../utils/nx-plugin'; +import { LoadedNxPlugin } from './plugins/internal-api'; +import { isNxPluginV1, isNxPluginV2 } from './plugins/utils'; +import { CreateDependenciesContext } from './plugins'; import { getRootTsConfigPath } from '../plugins/js/utils/typescript'; import { FileMap, @@ -32,9 +29,8 @@ import { ProjectConfiguration } from '../config/workspace-json-project-json'; import { readNxJson } from '../config/configuration'; import { existsSync } from 'fs'; import { PackageJson } from '../utils/package-json'; -import { getNxRequirePaths } from '../utils/installation-directory'; import { output } from '../utils/output'; -import { ExternalObject, NxWorkspaceFilesExternals } from '../native'; +import { NxWorkspaceFilesExternals } from '../native'; let storedFileMap: FileMap | null = null; let storedAllWorkspaceFiles: FileData[] | null = null; @@ -69,7 +65,8 @@ export async function buildProjectGraphUsingProjectFileMap( fileMap: FileMap, allWorkspaceFiles: FileData[], rustReferences: NxWorkspaceFilesExternals, - fileMapCache: FileMapCache | null + fileMapCache: FileMapCache | null, + plugins: LoadedNxPlugin[] ): Promise<{ projectGraph: ProjectGraph; projectFileMapCache: FileMapCache; @@ -118,7 +115,8 @@ export async function buildProjectGraphUsingProjectFileMap( externalNodes, context, cachedFileData, - projectGraphVersion + projectGraphVersion, + plugins ); const projectFileMapCache = createProjectFileMapCache( nxJson, @@ -160,7 +158,8 @@ async function buildProjectGraphUsingContext( knownExternalNodes: Record, ctx: CreateDependenciesContext, cachedFileData: CachedFileData, - projectGraphVersion: string + projectGraphVersion: string, + plugins: LoadedNxPlugin[] ) { performance.mark('build project graph:start'); @@ -176,7 +175,11 @@ async function buildProjectGraphUsingContext( let updatedGraph; let error; try { - updatedGraph = await updateProjectGraphWithPlugins(ctx, initProjectGraph); + updatedGraph = await updateProjectGraphWithPlugins( + ctx, + initProjectGraph, + plugins + ); } catch (e) { if (e instanceof CreateDependenciesError) { updatedGraph = e.partialProjectGraph; @@ -248,17 +251,12 @@ function createContext( async function updateProjectGraphWithPlugins( context: CreateDependenciesContext, - initProjectGraph: ProjectGraph + initProjectGraph: ProjectGraph, + plugins: LoadedNxPlugin[] ) { - const plugins = await loadNxPlugins( - context.nxJsonConfiguration?.plugins, - getNxRequirePaths(), - context.workspaceRoot, - context.projects - ); let graph = initProjectGraph; const errors: Array = []; - for (const { plugin } of plugins) { + for (const plugin of plugins) { try { if ( isNxPluginV1(plugin) && @@ -309,14 +307,14 @@ async function updateProjectGraphWithPlugins( ); const createDependencyPlugins = plugins.filter( - ({ plugin }) => isNxPluginV2(plugin) && plugin.createDependencies + (plugin) => isNxPluginV2(plugin) && plugin.createDependencies ); await Promise.all( - createDependencyPlugins.map(async ({ plugin, options }) => { + createDependencyPlugins.map(async (plugin) => { performance.mark(`${plugin.name}:createDependencies - start`); try { - const dependencies = await plugin.createDependencies(options, { + const dependencies = await plugin.createDependencies({ ...context, }); diff --git a/packages/nx/src/project-graph/error-types.ts b/packages/nx/src/project-graph/error-types.ts new file mode 100644 index 0000000000000..87ca50d23af91 --- /dev/null +++ b/packages/nx/src/project-graph/error-types.ts @@ -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, + 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, + public readonly partialResults: Array + ) { + 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) + ); +} diff --git a/packages/nx/src/project-graph/file-utils.ts b/packages/nx/src/project-graph/file-utils.ts index dc14266170db1..800654067c225 100644 --- a/packages/nx/src/project-graph/file-utils.ts +++ b/packages/nx/src/project-graph/file-utils.ts @@ -27,7 +27,7 @@ import { getDefaultPluginsSync } from '../utils/nx-plugin.deprecated'; import { minimatch } from 'minimatch'; import { CreateNodesResult } from '../devkit-exports'; 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 { type: string; @@ -186,15 +186,15 @@ function getProjectsSyncNoInference(root: string, nxJson: NxJsonConfiguration) { root, getDefaultPluginsSync(root) ); - const plugins: LoadedNxPlugin[] = [ - { plugin: PackageJsonProjectsNextToProjectJsonPlugin }, + const plugins = [ + PackageJsonProjectsNextToProjectJsonPlugin, ...getDefaultPluginsSync(root), ]; const projectRootMap: Map = new Map(); // 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 ?? []; if (!pattern) { continue; @@ -204,11 +204,15 @@ function getProjectsSyncNoInference(root: string, nxJson: NxJsonConfiguration) { ); for (const file of matchingConfigFiles) { if (minimatch(file, pattern, { dot: true })) { - let r = createNodes(file, options, { - nxJsonConfiguration: nxJson, - workspaceRoot: root, - configFiles: matchingConfigFiles, - }) as CreateNodesResult; + let r = createNodes( + file, + {}, + { + nxJsonConfiguration: nxJson, + workspaceRoot: root, + configFiles: matchingConfigFiles, + } + ) as CreateNodesResult; for (const node in r.projects) { const project = { root: node, diff --git a/packages/nx/src/project-graph/plugins/index.ts b/packages/nx/src/project-graph/plugins/index.ts new file mode 100644 index 0000000000000..f48519b8afa77 --- /dev/null +++ b/packages/nx/src/project-graph/plugins/index.ts @@ -0,0 +1,3 @@ +export * from './public-api'; + +export { readPluginPackageJson, registerPluginTSTranspiler } from './loader'; diff --git a/packages/nx/src/project-graph/plugins/internal-api.ts b/packages/nx/src/project-graph/plugins/internal-api.ts new file mode 100644 index 0000000000000..8271c0e2457ed --- /dev/null +++ b/packages/nx/src/project-graph/plugins/internal-api.ts @@ -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 + ]; + readonly createDependencies?: ( + context: CreateDependenciesContext + ) => ReturnType; + 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; + +// 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, () => void] +> = new Map(); + +export async function loadNxPlugins( + plugins: PluginConfiguration[], + root = workspaceRoot +): Promise<[LoadedNxPlugin[], () => void]> { + const result: Promise[] = []; + + 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'), + ]; +} diff --git a/packages/nx/src/project-graph/plugins/isolation/index.ts b/packages/nx/src/project-graph/plugins/isolation/index.ts new file mode 100644 index 0000000000000..f70b2e4001338 --- /dev/null +++ b/packages/nx/src/project-graph/plugins/isolation/index.ts @@ -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, () => void] +>(); + +export function loadNxPluginInIsolation( + plugin: PluginConfiguration, + root = workspaceRoot +): [Promise, () => 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]; +} diff --git a/packages/nx/src/project-graph/plugins/isolation/messaging.ts b/packages/nx/src/project-graph/plugins/isolation/messaging.ts new file mode 100644 index 0000000000000..8ee05a0a1d69d --- /dev/null +++ b/packages/nx/src/project-graph/plugins/isolation/messaging.ts @@ -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>; + 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; + 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 | Promise; + +// The handler can return a message to be sent back to the process from which the message originated +type MessageHandlerReturn = + T extends PluginWorkerResult + ? MaybePromise + : MaybePromise; + +// 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['payload'] + ) => MessageHandlerReturn; + } +) { + 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); +} diff --git a/packages/nx/src/project-graph/plugins/isolation/plugin-pool.ts b/packages/nx/src/project-graph/plugins/isolation/plugin-pool.ts new file mode 100644 index 0000000000000..2371f0ba36403 --- /dev/null +++ b/packages/nx/src/project-graph/plugins/isolation/plugin-pool.ts @@ -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(); + +interface PendingPromise { + promise: Promise; + resolver: (result: any) => void; + rejector: (err: any) => void; +} + +export function loadRemoteNxPlugin( + plugin: PluginConfiguration, + root: string +): [Promise, () => 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(); + + const exitHandler = createWorkerExitHandler(worker, pendingPromises); + + const cleanupFunction = () => { + worker.off('exit', exitHandler); + shutdownPluginWorker(worker, pendingPromises); + }; + + cleanupFunctions.add(cleanupFunction); + + return [ + new Promise((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 +) { + // 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, + 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(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 +) { + 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, + callback: () => void +): Promise { + 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; +} diff --git a/packages/nx/src/project-graph/plugins/isolation/plugin-worker.ts b/packages/nx/src/project-graph/plugins/isolation/plugin-worker.ts new file mode 100644 index 0000000000000..1b759e9649335 --- /dev/null +++ b/packages/nx/src/project-graph/plugins/isolation/plugin-worker.ts @@ -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(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 }, + }; + } + }, + }); +}); diff --git a/packages/nx/src/project-graph/plugins/loader.ts b/packages/nx/src/project-graph/plugins/loader.ts new file mode 100644 index 0000000000000..ff219c700ed0c --- /dev/null +++ b/packages/nx/src/project-graph/plugins/loader.ts @@ -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, + 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, + 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 = 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, + 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, + root = workspaceRoot +): string | null { + const tsConfigPaths: Record = 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; + +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, + 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; + +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 { + 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 { + const m = await import(pluginPath); + if ( + m.default && + ('createNodes' in m.default || 'createDependencies' in m.default) + ) { + return m.default; + } + return m; +} diff --git a/packages/nx/src/project-graph/plugins/public-api.ts b/packages/nx/src/project-graph/plugins/public-api.ts new file mode 100644 index 0000000000000..835ecf77c081d --- /dev/null +++ b/packages/nx/src/project-graph/plugins/public-api.ts @@ -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 = ( + projectConfigurationFile: string, + options: T | undefined, + context: CreateNodesContext +) => CreateNodesResult | Promise; + +export type Optional = Omit & Partial>; + +export interface CreateNodesResult { + /** + * A map of project root -> project configuration + */ + projects?: Record>; + + /** + * A map of external node name -> external node. External nodes do not have a root, so the key is their name. + */ + externalNodes?: Record; +} + +/** + * A pair of file patterns and {@link CreateNodesFunction} + */ +export type CreateNodes = readonly [ + projectFilePattern: string, + createNodesFunction: CreateNodesFunction +]; + +/** + * 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; + + /** + * 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 = ( + options: T | undefined, + context: CreateDependenciesContext +) => RawProjectGraphDependency[] | Promise; + +/** + * A plugin for Nx which creates nodes and dependencies for the {@link ProjectGraph} + */ +export type NxPluginV2 = { + 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; +}; + +/** + * A plugin for Nx + */ +export type NxPlugin = NxPluginV1 | NxPluginV2; diff --git a/packages/nx/src/project-graph/plugins/utils.ts b/packages/nx/src/project-graph/plugins/utils.ts new file mode 100644 index 0000000000000..3285682c214a4 --- /dev/null +++ b/packages/nx/src/project-graph/plugins/utils.ts @@ -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 { + performance.mark(`${plugin.name}:createNodes - start`); + + const promises: Array< + CreateNodesResultWithContext | Promise + > = 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( + arr: Array, + 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]; +} diff --git a/packages/nx/src/project-graph/project-graph-builder.ts b/packages/nx/src/project-graph/project-graph-builder.ts index 39978d05ceeeb..d136c4f1064ba 100644 --- a/packages/nx/src/project-graph/project-graph-builder.ts +++ b/packages/nx/src/project-graph/project-graph-builder.ts @@ -15,7 +15,7 @@ import { ProjectGraphProjectNode, } from '../config/project-graph'; import { ProjectConfiguration } from '../config/workspace-json-project-json'; -import { CreateDependenciesContext } from '../utils/nx-plugin'; +import { CreateDependenciesContext } from './plugins'; import { getFileMap } from './build-project-graph'; /** diff --git a/packages/nx/src/project-graph/project-graph.ts b/packages/nx/src/project-graph/project-graph.ts index c82211e40d583..05b8967a3cb4e 100644 --- a/packages/nx/src/project-graph/project-graph.ts +++ b/packages/nx/src/project-graph/project-graph.ts @@ -26,15 +26,17 @@ import { retrieveWorkspaceFiles, } from './utils/retrieve-workspace-files'; import { readNxJson } from '../config/nx-json'; -import { unregisterPluginTSTranspiler } from '../utils/nx-plugin'; import { ConfigurationResult, ConfigurationSourceMaps, +} from './utils/project-configuration-utils'; +import { CreateNodesError, MergeNodesError, ProjectConfigurationsError, -} from './utils/project-configuration-utils'; +} from './error-types'; 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. @@ -95,15 +97,16 @@ export function readProjectsConfigurationFromProjectGraph( } 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; const nxJson = readNxJson(); performance.mark('retrieve-project-configurations:start'); let configurationResult: ConfigurationResult; let projectConfigurationsError: ProjectConfigurationsError; + const [plugins, cleanup] = await loadNxPlugins(nxJson.plugins); try { configurationResult = await retrieveProjectConfigurations( + plugins, workspaceRoot, nxJson ); @@ -137,7 +140,8 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() { fileMap, allWorkspaceFiles, rustReferences, - cacheEnabled ? readFileMapCache() : null + cacheEnabled ? readFileMapCache() : null, + plugins ); } catch (e) { if (e instanceof CreateDependenciesError) { @@ -149,12 +153,13 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() { } else { throw e; } + } finally { + cleanup(); } const { projectGraph, projectFileMapCache } = projectGraphResult; performance.mark('build-project-graph-using-project-file-map:end'); - unregisterPluginTSTranspiler(); delete global.NX_GRAPH_CREATION; const errors = [ diff --git a/packages/nx/src/project-graph/utils/normalize-project-nodes.ts b/packages/nx/src/project-graph/utils/normalize-project-nodes.ts index c7e12a17e085f..4493df96640d5 100644 --- a/packages/nx/src/project-graph/utils/normalize-project-nodes.ts +++ b/packages/nx/src/project-graph/utils/normalize-project-nodes.ts @@ -5,9 +5,8 @@ import { TargetConfiguration, } from '../../config/workspace-json-project-json'; import { findMatchingProjects } from '../../utils/find-matching-projects'; -import { NX_PREFIX } from '../../utils/logger'; import { resolveNxTokensInOptions } from '../utils/project-configuration-utils'; -import { CreateDependenciesContext } from '../../utils/nx-plugin'; +import { CreateDependenciesContext } from '../plugins'; export async function normalizeProjectNodes( ctx: CreateDependenciesContext, diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts index 39bfc11c10604..b71358040ecf3 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts @@ -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 { ProjectConfiguration, TargetConfiguration, diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.ts index 0e17839b4fdf9..20448f4489936 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.ts @@ -7,17 +7,26 @@ import { TargetMetadata, } from '../../config/workspace-json-project-json'; import { NX_PREFIX } from '../../utils/logger'; -import { CreateNodesResult, LoadedNxPlugin } from '../../utils/nx-plugin'; import { readJsonFile } from '../../utils/fileutils'; import { workspaceRoot } from '../../utils/workspace-root'; import { ONLY_MODIFIES_EXISTING_TARGET, OVERRIDE_SOURCE_FILE, -} from '../../plugins/target-defaults/target-defaults-plugin'; +} from '../../plugins/target-defaults/symbols'; import { minimatch } from 'minimatch'; import { join } from 'path'; 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 ConfigurationSourceMaps = Record< @@ -294,10 +303,6 @@ export type ConfigurationResult = { projectRootMap: Record; sourceMaps: ConfigurationSourceMaps; }; -type CreateNodesResultWithContext = CreateNodesResult & { - file: string; - pluginName: string; -}; /** * 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 plugins The plugins that should be used to infer project configuration */ -export function createProjectConfigurations( +export async function createProjectConfigurations( root: string = workspaceRoot, 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[] ): Promise { performance.mark('build-project-configs:start'); @@ -319,20 +324,21 @@ export function createProjectConfigurations( const errors: Array = []; // We iterate over plugins first - this ensures that plugins specified first take precedence. - for (const { plugin, options, include, exclude } of plugins) { - const [pattern, createNodes] = plugin.createNodes ?? []; - const pluginResults: Array< - CreateNodesResultWithContext | Promise - > = []; + for (const { + name: pluginName, + createNodes: createNodesTuple, + include, + exclude, + } of plugins) { + const [pattern, createNodes] = createNodesTuple ?? []; - performance.mark(`${plugin.name}:createNodes - start`); if (!pattern) { continue; } const matchingConfigFiles: string[] = []; - for (const file of workspaceFiles) { + for (const file of projectFiles) { if (minimatch(file, pattern, { dot: true })) { if (include) { const included = include.some((includedPattern) => @@ -355,76 +361,20 @@ export function createProjectConfigurations( matchingConfigFiles.push(file); } } - for (const file of matchingConfigFiles) { - performance.mark(`${plugin.name}:createNodes:${file} - start`); - try { - let r = createNodes(file, options, { - nxJsonConfiguration: nxJson, - workspaceRoot: root, - configFiles: matchingConfigFiles, - }); - - if (r instanceof Promise) { - pluginResults.push( - 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 { - performance.mark(`${plugin.name}:createNodes:${file} - end`); - 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, - }) - ); + let r = createNodes(matchingConfigFiles, { + nxJsonConfiguration: nxJson, + workspaceRoot: root, + configFiles: matchingConfigFiles, + }).catch((e) => { + if (isAggregateCreateNodesError(e)) { + errors.push(...e.errors); + return e.partialResults; + } else { + throw e; } - } + }); - results.push( - 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; - }) - ); + results.push(r); } return Promise.all(results).then((results) => { @@ -557,62 +507,6 @@ export function readProjectConfigurationsFromRootMap( return projects; } -export class ProjectConfigurationsError extends Error { - constructor( - public readonly errors: Array, - 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. * diff --git a/packages/nx/src/project-graph/utils/retrieve-workspace-files.spec.ts b/packages/nx/src/project-graph/utils/retrieve-workspace-files.spec.ts index 54eaa75de789a..3d23230b11e69 100644 --- a/packages/nx/src/project-graph/utils/retrieve-workspace-files.spec.ts +++ b/packages/nx/src/project-graph/utils/retrieve-workspace-files.spec.ts @@ -1,4 +1,3 @@ -import { getDefaultPlugins } from '../../utils/nx-plugin'; import { TempFs } from '../../internal-testing-utils/temp-fs'; import { retrieveProjectConfigurationPaths } from './retrieve-workspace-files'; @@ -26,10 +25,18 @@ describe('retrieveProjectConfigurationPaths', () => { }) ); - const configPaths = await retrieveProjectConfigurationPaths( - fs.tempDir, - await getDefaultPlugins(fs.tempDir) - ); + const configPaths = retrieveProjectConfigurationPaths(fs.tempDir, [ + { + createNodes: [ + '{project.json,**/project.json}', + () => { + return { + projects: {}, + }; + }, + ], + }, + ]); expect(configPaths).not.toContain('not-projects/project.json'); expect(configPaths).toContain('projects/project.json'); diff --git a/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts b/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts index ba1cdb73a3772..1b54a2154c0b5 100644 --- a/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts +++ b/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts @@ -1,28 +1,22 @@ import { performance } from 'perf_hooks'; -import { getNxRequirePaths } from '../../utils/installation-directory'; import { ProjectConfiguration } from '../../config/workspace-json-project-json'; import { NX_ANGULAR_JSON_PLUGIN_NAME, - NxAngularJsonPlugin, shouldMergeAngularProjects, } from '../../adapter/angular-json'; import { NxJsonConfiguration, readNxJson } from '../../config/nx-json'; -import { getNxPackageJsonWorkspacesPlugin } from '../../plugins/package-json-workspaces'; import { createProjectConfigurations, ConfigurationResult, } from './project-configuration-utils'; -import { - getDefaultPlugins, - LoadedNxPlugin, - loadNxPlugins, -} from '../../utils/nx-plugin'; -import { ProjectJsonProjectsPlugin } from '../../plugins/project-json/build-nodes/project-json'; +import { LoadedNxPlugin, loadNxPlugins } from '../plugins/internal-api'; import { getNxWorkspaceFilesFromContext, globWithWorkspaceContext, } from '../../utils/workspace-context'; 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` @@ -65,41 +59,45 @@ export async function retrieveWorkspaceFiles( /** * Walk through the workspace and return `ProjectConfigurations`. Only use this if the projectFileMap is not needed. - * - * @param workspaceRoot - * @param nxJson */ export async function retrieveProjectConfigurations( + plugins: LoadedNxPlugin[], workspaceRoot: string, nxJson: NxJsonConfiguration ): Promise { - const plugins = await loadNxPlugins( - nxJson?.plugins ?? [], - getNxRequirePaths(workspaceRoot), - workspaceRoot + const projects = await _retrieveProjectConfigurations( + workspaceRoot, + nxJson, + plugins ); - - return _retrieveProjectConfigurations(workspaceRoot, nxJson, plugins); + return projects; } export async function retrieveProjectConfigurationsWithAngularProjects( workspaceRoot: string, nxJson: NxJsonConfiguration ): Promise { - const plugins = await loadNxPlugins( - nxJson?.plugins ?? [], - getNxRequirePaths(workspaceRoot), - workspaceRoot - ); + const pluginsToLoad = nxJson?.plugins ?? []; if ( 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( @@ -120,7 +118,7 @@ function _retrieveProjectConfigurations( export function retrieveProjectConfigurationPaths( root: string, - plugins: LoadedNxPlugin[] + plugins: Array<{ createNodes?: readonly [string, ...unknown[]] } & unknown> ): string[] { const projectGlobPatterns = configurationGlobs(plugins); return globWithWorkspaceContext(root, projectGlobPatterns); @@ -136,7 +134,7 @@ export async function retrieveProjectConfigurationsWithoutPluginInference( root: string ): Promise> { const nxJson = readNxJson(root); - const plugins = await getDefaultPlugins(root); + const [plugins, cleanup] = await loadNxPlugins([]); // only load default plugins const projectGlobPatterns = retrieveProjectConfigurationPaths(root, plugins); const cacheKey = root + ',' + projectGlobPatterns.join(','); @@ -150,21 +148,22 @@ export async function retrieveProjectConfigurationsWithoutPluginInference( root, nxJson, projectFiles, - [ - { plugin: getNxPackageJsonWorkspacesPlugin(root) }, - { plugin: ProjectJsonProjectsPlugin }, - ] + plugins ); projectsWithoutPluginCache.set(cacheKey, projects); + cleanup(); + return projects; } -export function configurationGlobs(plugins: LoadedNxPlugin[]): string[] { +export function configurationGlobs( + plugins: Array<{ createNodes?: readonly [string, ...unknown[]] }> +): string[] { const globPatterns = []; - for (const { plugin } of plugins) { - if (plugin.createNodes) { + for (const plugin of plugins) { + if ('createNodes' in plugin && plugin.createNodes) { globPatterns.push(plugin.createNodes[0]); } } diff --git a/packages/nx/src/utils/logger.ts b/packages/nx/src/utils/logger.ts index 8096d5dc478bf..54b03cd730a23 100644 --- a/packages/nx/src/utils/logger.ts +++ b/packages/nx/src/utils/logger.ts @@ -31,6 +31,11 @@ export const logger = { fatal: (...s) => { console.error(...s); }, + verbose: (...s) => { + if (process.env.NX_VERBOSE_LOGGING) { + console.log(...s); + } + }, }; export function stripIndent(str: string): string { diff --git a/packages/nx/src/utils/nx-plugin.deprecated.ts b/packages/nx/src/utils/nx-plugin.deprecated.ts index c5c24129b964c..6bd67d63ab923 100644 --- a/packages/nx/src/utils/nx-plugin.deprecated.ts +++ b/packages/nx/src/utils/nx-plugin.deprecated.ts @@ -1,10 +1,11 @@ import { shouldMergeAngularProjects } from '../adapter/angular-json'; import { ProjectGraphProcessor } from '../config/project-graph'; import { TargetConfiguration } from '../config/workspace-json-project-json'; -import { ProjectJsonProjectsPlugin } from '../plugins/project-json/build-nodes/project-json'; -import { TargetDefaultsPlugin } from '../plugins/target-defaults/target-defaults-plugin'; -import { getNxPackageJsonWorkspacesPlugin } from '../plugins/package-json-workspaces'; -import { LoadedNxPlugin, NxPluginV2 } from './nx-plugin'; +import ProjectJsonProjectsPlugin from '../plugins/project-json/build-nodes/project-json'; +import TargetDefaultsPlugin from '../plugins/target-defaults/target-defaults-plugin'; +import * as PackageJsonWorkspacesPlugin from '../plugins/package-json-workspaces'; +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 @@ -39,18 +40,16 @@ export type NxPluginV1 = { /** * @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[] = [ require('../plugins/js'), ...(shouldMergeAngularProjects(root, false) ? [require('../adapter/angular-json').NxAngularJsonPlugin] : []), TargetDefaultsPlugin, - getNxPackageJsonWorkspacesPlugin(root), + PackageJsonWorkspacesPlugin, ProjectJsonProjectsPlugin, ]; - return plugins.map((p) => ({ - plugin: p, - })); + return plugins; } diff --git a/packages/nx/src/utils/nx-plugin.ts b/packages/nx/src/utils/nx-plugin.ts deleted file mode 100644 index 1ab2faa9c19be..0000000000000 --- a/packages/nx/src/utils/nx-plugin.ts +++ /dev/null @@ -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 = ( - projectConfigurationFile: string, - options: T | undefined, - context: CreateNodesContext -) => CreateNodesResult | Promise; - -export interface CreateNodesResult { - /** - * A map of project root -> project configuration - */ - projects?: Record>; - - /** - * A map of external node name -> external node. External nodes do not have a root, so the key is their name. - */ - externalNodes?: Record; -} - -/** - * A pair of file patterns and {@link CreateNodesFunction} - */ -export type CreateNodes = readonly [ - configFilePattern: string, - createNodesFunction: CreateNodesFunction -]; - -/** - * 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; - - /** - * 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 = ( - options: T | undefined, - context: CreateDependenciesContext -) => RawProjectGraphDependency[] | Promise; - -/** - * A plugin for Nx which creates nodes and dependencies for the {@link ProjectGraph} - */ -export type NxPluginV2 = { - 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; -}; - -export * from './nx-plugin.deprecated'; - -/** - * A plugin for Nx - */ -export type NxPlugin = NxPluginV1 | NxPluginV2; - -export type LoadedNxPlugin = { - plugin: NxPluginV2 & Pick; - 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 = new Map(); - -export function getPluginPathAndName( - moduleName: string, - paths: string[], - projects: Record, - 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, - root: string -): Promise { - 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 -): Promise { - 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, - 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, - 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 = 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, - 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, - root = workspaceRoot -): string | null { - const tsConfigPaths: Record = 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; - -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 { - 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 = Omit & Partial>; diff --git a/packages/nx/src/utils/plugins/plugin-capabilities.ts b/packages/nx/src/utils/plugins/plugin-capabilities.ts index 156cc111415b8..875a2150d2458 100644 --- a/packages/nx/src/utils/plugins/plugin-capabilities.ts +++ b/packages/nx/src/utils/plugins/plugin-capabilities.ts @@ -1,19 +1,18 @@ -import { workspaceRoot } from '../workspace-root'; import * as chalk from 'chalk'; import { dirname, join } from 'path'; -import { output } from '../output'; -import type { PluginCapabilities } from './models'; -import { hasElements } from './shared'; + +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 { getPackageManagerCommand } from '../package-manager'; -import { - loadNxPluginAsync, - NxPlugin, - readPluginPackageJson, -} from '../nx-plugin'; import { getNxRequirePaths } from '../installation-directory'; +import { output } from '../output'; import { PackageJson } from '../package-json'; -import { ProjectConfiguration } from '../../config/workspace-json-project-json'; +import { getPackageManagerCommand } from '../package-manager'; +import { workspaceRoot } from '../workspace-root'; +import { hasElements } from './shared'; + +import type { PluginCapabilities } from './models'; function tryGetCollection( packageJsonPath: string, @@ -46,7 +45,7 @@ export async function getPluginCapabilities( getNxRequirePaths(workspaceRoot) ); const pluginModule = includeRuntimeCapabilities - ? await tryGetModule(packageJson, workspaceRoot, projects) + ? await tryGetModule(packageJson, workspaceRoot) : ({} as Record); return { name: pluginName, @@ -99,26 +98,24 @@ export async function getPluginCapabilities( async function tryGetModule( packageJson: PackageJson, - workspaceRoot: string, - projects: Record + workspaceRoot: string ): Promise { try { - return packageJson.generators ?? + if ( + packageJson.generators ?? packageJson.executors ?? packageJson['nx-migrations'] ?? packageJson['schematics'] ?? packageJson['builders'] - ? ( - await loadNxPluginAsync( - packageJson.name, - getNxRequirePaths(workspaceRoot), - projects, - workspaceRoot - ) - ).plugin - : ({ - name: packageJson.name, - } as NxPlugin); + ) { + const [pluginPromise] = loadNxPlugin(packageJson.name, workspaceRoot); + const plugin = await pluginPromise; + return plugin; + } else { + return { + name: packageJson.name, + }; + } } catch { return null; } diff --git a/packages/rollup/src/executors/rollup/lib/normalize.spec.ts b/packages/rollup/src/executors/rollup/lib/normalize.spec.ts index 0747214bf6a70..6aa7ff0dd2d06 100644 --- a/packages/rollup/src/executors/rollup/lib/normalize.spec.ts +++ b/packages/rollup/src/executors/rollup/lib/normalize.spec.ts @@ -38,6 +38,7 @@ describe('normalizeRollupExecutorOptions', () => { ); expect(result.rollupConfig).toHaveLength(1); expect(result.rollupConfig[0]).toMatch('react'); + // This fails if the nx repo has been cloned in `/root/...` expect(result.rollupConfig[0]).not.toMatch(root); });