feat(core): add standard way to pass plugin options (#19589)

Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
This commit is contained in:
Craigory Coppola 2023-10-16 18:24:11 -04:00 committed by GitHub
parent 7ea2374caa
commit f918a72307
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 295 additions and 174 deletions

View File

@ -1,10 +1,16 @@
# Type alias: CreateDependencies
# Type alias: CreateDependencies<T\>
Ƭ **CreateDependencies**: (`context`: [`CreateDependenciesContext`](../../devkit/documents/CreateDependenciesContext)) => [`RawProjectGraphDependency`](../../devkit/documents/RawProjectGraphDependency)[] \| `Promise`<[`RawProjectGraphDependency`](../../devkit/documents/RawProjectGraphDependency)[]\>
Ƭ **CreateDependencies**<`T`\>: (`options`: `T` \| `undefined`, `context`: [`CreateDependenciesContext`](../../devkit/documents/CreateDependenciesContext)) => [`RawProjectGraphDependency`](../../devkit/documents/RawProjectGraphDependency)[] \| `Promise`<[`RawProjectGraphDependency`](../../devkit/documents/RawProjectGraphDependency)[]\>
#### Type parameters
| Name | Type |
| :--- | :---------------------------------------------------------------------- |
| `T` | extends `Record`<`string`, `unknown`\> = `Record`<`string`, `unknown`\> |
#### Type declaration
▸ (`context`): [`RawProjectGraphDependency`](../../devkit/documents/RawProjectGraphDependency)[] \| `Promise`<[`RawProjectGraphDependency`](../../devkit/documents/RawProjectGraphDependency)[]\>
▸ (`options`, `context`): [`RawProjectGraphDependency`](../../devkit/documents/RawProjectGraphDependency)[] \| `Promise`<[`RawProjectGraphDependency`](../../devkit/documents/RawProjectGraphDependency)[]\>
A function which parses files in the workspace to create dependencies in the [ProjectGraph](../../devkit/documents/ProjectGraph)
Use [validateDependency](../../devkit/documents/validateDependency) to validate dependencies
@ -13,6 +19,7 @@ Use [validateDependency](../../devkit/documents/validateDependency) to validate
| Name | Type |
| :-------- | :------------------------------------------------------------------------------ |
| `options` | `T` \| `undefined` |
| `context` | [`CreateDependenciesContext`](../../devkit/documents/CreateDependenciesContext) |
##### Returns

View File

@ -1,5 +1,11 @@
# Type alias: CreateNodes
# Type alias: CreateNodes<T\>
Ƭ **CreateNodes**: readonly [projectFilePattern: string, createNodesFunction: CreateNodesFunction]
Ƭ **CreateNodes**<`T`\>: readonly [projectFilePattern: string, createNodesFunction: CreateNodesFunction<T\>]
A pair of file patterns and [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)
#### Type parameters
| Name | Type |
| :--- | :---------------------------------------------------------------------- |
| `T` | extends `Record`<`string`, `unknown`\> = `Record`<`string`, `unknown`\> |

View File

@ -1,10 +1,16 @@
# Type alias: CreateNodesFunction
# Type alias: CreateNodesFunction<T\>
Ƭ **CreateNodesFunction**: (`projectConfigurationFile`: `string`, `context`: [`CreateNodesContext`](../../devkit/documents/CreateNodesContext)) => { `externalNodes?`: `Record`<`string`, [`ProjectGraphExternalNode`](../../devkit/documents/ProjectGraphExternalNode)\> ; `projects?`: `Record`<`string`, [`ProjectConfiguration`](../../devkit/documents/ProjectConfiguration)\> }
Ƭ **CreateNodesFunction**<`T`\>: (`projectConfigurationFile`: `string`, `options`: `T` \| `undefined`, `context`: [`CreateNodesContext`](../../devkit/documents/CreateNodesContext)) => { `externalNodes?`: `Record`<`string`, [`ProjectGraphExternalNode`](../../devkit/documents/ProjectGraphExternalNode)\> ; `projects?`: `Record`<`string`, [`ProjectConfiguration`](../../devkit/documents/ProjectConfiguration)\> }
#### Type parameters
| Name | Type |
| :--- | :---------------------------------------------------------------------- |
| `T` | extends `Record`<`string`, `unknown`\> = `Record`<`string`, `unknown`\> |
#### Type declaration
▸ (`projectConfigurationFile`, `context`): `Object`
▸ (`projectConfigurationFile`, `options`, `context`): `Object`
A function which parses a configuration file into a set of nodes.
Used for creating nodes for the [ProjectGraph](../../devkit/documents/ProjectGraph)
@ -14,6 +20,7 @@ Used for creating nodes for the [ProjectGraph](../../devkit/documents/ProjectGra
| Name | Type |
| :------------------------- | :---------------------------------------------------------------- |
| `projectConfigurationFile` | `string` |
| `options` | `T` \| `undefined` |
| `context` | [`CreateNodesContext`](../../devkit/documents/CreateNodesContext) |
##### Returns

View File

@ -32,8 +32,8 @@ Nx.json configuration
- [nxCloudEncryptionKey](../../devkit/documents/NxJsonConfiguration#nxcloudencryptionkey): string
- [nxCloudUrl](../../devkit/documents/NxJsonConfiguration#nxcloudurl): string
- [parallel](../../devkit/documents/NxJsonConfiguration#parallel): number
- [plugins](../../devkit/documents/NxJsonConfiguration#plugins): string[]
- [pluginsConfig](../../devkit/documents/NxJsonConfiguration#pluginsconfig): Record&lt;string, unknown&gt;
- [plugins](../../devkit/documents/NxJsonConfiguration#plugins): PluginDefinition[]
- [pluginsConfig](../../devkit/documents/NxJsonConfiguration#pluginsconfig): Record&lt;string, Record&lt;string, unknown&gt;&gt;
- [release](../../devkit/documents/NxJsonConfiguration#release): NxReleaseConfiguration
- [targetDefaults](../../devkit/documents/NxJsonConfiguration#targetdefaults): TargetDefaults
- [tasksRunnerOptions](../../devkit/documents/NxJsonConfiguration#tasksrunneroptions): Object
@ -198,7 +198,7 @@ Specifies how many tasks can be run in parallel.
### plugins
`Optional` **plugins**: `string`[]
`Optional` **plugins**: `PluginDefinition`[]
Plugins for extending the project graph
@ -206,7 +206,7 @@ Plugins for extending the project graph
### pluginsConfig
`Optional` **pluginsConfig**: `Record`<`string`, `unknown`\>
`Optional` **pluginsConfig**: `Record`<`string`, `Record`<`string`, `unknown`\>\>
Configuration for Nx Plugins

View File

@ -1,13 +1,19 @@
# Type alias: NxPluginV2
# Type alias: NxPluginV2<T\>
Ƭ **NxPluginV2**: `Object`
Ƭ **NxPluginV2**<`T`\>: `Object`
A plugin for Nx which creates nodes and dependencies for the [ProjectGraph](../../devkit/documents/ProjectGraph)
#### Type parameters
| Name | Type |
| :--- | :---------------------------------------------------------------------- |
| `T` | extends `Record`<`string`, `unknown`\> = `Record`<`string`, `unknown`\> |
#### Type declaration
| Name | Type | Description |
| :-------------------- | :---------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- |
| `createDependencies?` | [`CreateDependencies`](../../devkit/documents/CreateDependencies) | 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 } |
| `name` | `string` | - |
| Name | Type | Description |
| :-------------------- | :---------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- |
| `createDependencies?` | [`CreateDependencies`](../../devkit/documents/CreateDependencies)<`T`\> | Provides a function to analyze files to create dependencies for the [ProjectGraph](../../devkit/documents/ProjectGraph) |
| `createNodes?` | [`CreateNodes`](../../devkit/documents/CreateNodes)<`T`\> | Provides a file pattern and function that retrieves configuration info from those files. e.g. { '\*_/_.csproj': buildProjectsFromCsProjFile } |
| `name` | `string` | - |

View File

@ -30,8 +30,8 @@ use ProjectsConfigurations or NxJsonConfiguration
- [nxCloudEncryptionKey](../../devkit/documents/Workspace#nxcloudencryptionkey): string
- [nxCloudUrl](../../devkit/documents/Workspace#nxcloudurl): string
- [parallel](../../devkit/documents/Workspace#parallel): number
- [plugins](../../devkit/documents/Workspace#plugins): string[]
- [pluginsConfig](../../devkit/documents/Workspace#pluginsconfig): Record&lt;string, unknown&gt;
- [plugins](../../devkit/documents/Workspace#plugins): PluginDefinition[]
- [pluginsConfig](../../devkit/documents/Workspace#pluginsconfig): Record&lt;string, Record&lt;string, unknown&gt;&gt;
- [projects](../../devkit/documents/Workspace#projects): Record&lt;string, ProjectConfiguration&gt;
- [release](../../devkit/documents/Workspace#release): NxReleaseConfiguration
- [targetDefaults](../../devkit/documents/Workspace#targetdefaults): TargetDefaults
@ -254,7 +254,7 @@ Specifies how many tasks can be run in parallel.
### plugins
`Optional` **plugins**: `string`[]
`Optional` **plugins**: `PluginDefinition`[]
Plugins for extending the project graph
@ -266,7 +266,7 @@ Plugins for extending the project graph
### pluginsConfig
`Optional` **pluginsConfig**: `Record`<`string`, `unknown`\>
`Optional` **pluginsConfig**: `Record`<`string`, `Record`<`string`, `unknown`\>\>
Configuration for Nx Plugins

View File

@ -18,3 +18,39 @@ export default async function* execute(
yield* asyncGenerator();
}
`;
export const NX_PLUGIN_V2_CONTENTS = `import { basename, dirname } from "path";
import { CreateNodes } from "@nx/devkit";
type PluginOptions = {
inferredTags: string[]
}
export const createNodes: CreateNodes<PluginOptions> = [
"**/my-project-file",
(f, options, ctx) => {
// f = path/to/my/file/my-project-file
const root = dirname(f);
// root = path/to/my/file
const name = basename(root);
// name = file
return {
projects: {
[name]: {
root,
targets: {
build: {
executor: "nx:run-commands",
options: {
command: "echo 'custom registered target'",
},
},
},
tags: options.inferredTags
},
},
};
},
];
`;

View File

@ -16,7 +16,10 @@ import {
} from '@nx/e2e/utils';
import type { PackageJson } from 'nx/src/utils/package-json';
import { ASYNC_GENERATOR_EXECUTOR_CONTENTS } from './nx-plugin.fixtures';
import {
ASYNC_GENERATOR_EXECUTOR_CONTENTS,
NX_PLUGIN_V2_CONTENTS,
} from './nx-plugin.fixtures';
import { join } from 'path';
describe('Nx Plugin', () => {
@ -263,7 +266,7 @@ describe('Nx Plugin', () => {
runCLI(`generate @nx/plugin:plugin ${plugin} --linter=eslint`);
});
it('should be able to infer projects and targets', async () => {
it('should be able to infer projects and targets (v1)', async () => {
// Setup project inference + target inference
updateFile(
`libs/${plugin}/src/index.ts`,
@ -303,6 +306,36 @@ describe('Nx Plugin', () => {
);
});
it('should be able to infer projects and targets (v2)', async () => {
// Setup project inference + target inference
updateFile(`libs/${plugin}/src/index.ts`, NX_PLUGIN_V2_CONTENTS);
// Register plugin in nx.json (required for inference)
updateFile(`nx.json`, (nxJson) => {
const nx = JSON.parse(nxJson);
nx.plugins = [
{
plugin: `@${npmScope}/${plugin}`,
options: { inferredTags: ['my-tag'] },
},
];
return JSON.stringify(nx, null, 2);
});
// Create project that should be inferred by Nx
const inferredProject = uniq('inferred');
createFile(`libs/${inferredProject}/my-project-file`);
// Attempt to use inferred project w/ Nx
expect(runCLI(`build ${inferredProject}`)).toContain(
'custom registered target'
);
const configuration = JSON.parse(
runCLI(`show project ${inferredProject} --json`)
);
expect(configuration.tags).toEqual(['my-tag']);
});
it('should be able to use local generators and executors', async () => {
const generator = uniq('generator');
const executor = uniq('executor');

View File

@ -70,7 +70,7 @@
"type": "array",
"description": "Plugins for extending the project graph.",
"items": {
"type": "string"
"$ref": "#/definitions/plugins"
}
},
"defaultProject": {
@ -377,6 +377,27 @@
}
},
"additionalProperties": false
},
"plugins": {
"oneOf": [
{
"type": "string",
"description": "A plugin module to load with default options"
},
{
"type": "object",
"properties": {
"plugin": {
"type": "string",
"description": "The plugin module to load"
},
"options": {
"type": "object",
"description": "The options passed to the plugin when creating nodes and dependencies"
}
}
}
]
}
}
}

View File

@ -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 '../devkit-exports';
import { NxPluginV2 } from '../utils/nx-plugin';
export const NX_ANGULAR_JSON_PLUGIN_NAME = 'nx-angular-json-plugin';
@ -10,7 +10,7 @@ export const NxAngularJsonPlugin: NxPluginV2 = {
name: NX_ANGULAR_JSON_PLUGIN_NAME,
createNodes: [
'angular.json',
(f, ctx) => ({
(f, _, ctx) => ({
projects: readAngularJson(ctx.workspaceRoot),
}),
],

View File

@ -367,7 +367,7 @@ function addNrwlJsPluginsConfig(repoRoot: string) {
json.pluginsConfig = {
'@nx/js': {
analyzeSourceFiles: true,
} as NrwlJsPluginConfig,
},
};
}

View File

@ -176,12 +176,12 @@ export interface NxJsonConfiguration<T = '*' | string[]> {
/**
* Plugins for extending the project graph
*/
plugins?: string[];
plugins?: PluginDefinition[];
/**
* Configuration for Nx Plugins
*/
pluginsConfig?: Record<string, unknown>;
pluginsConfig?: Record<string, Record<string, unknown>>;
/**
* Default project. When project isn't provided, the default project
@ -234,6 +234,10 @@ export interface NxJsonConfiguration<T = '*' | string[]> {
useDaemonProcess?: boolean;
}
export type PluginDefinition =
| string
| { plugin: string; options?: Record<string, unknown> };
export function readNxJson(root: string = workspaceRoot): NxJsonConfiguration {
const nxJson = join(root, 'nx.json');
if (existsSync(nxJson)) {

View File

@ -213,7 +213,7 @@ function readAndCombineAllProjectConfigurations(tree: Tree): {
if (basename(projectFile) === 'project.json') {
const json = readJson(tree, projectFile);
const config = buildProjectFromProjectJson(json, projectFile);
mergeProjectConfigurationIntoRootMap(rootMap, config, projectFile);
mergeProjectConfigurationIntoRootMap(rootMap, config);
} else {
const packageJson = readJson<PackageJson>(tree, projectFile);
const config = buildProjectConfigurationFromPackageJson(
@ -228,8 +228,7 @@ function readAndCombineAllProjectConfigurations(tree: Tree): {
{
name: config.name,
root: config.root,
},
projectFile
}
);
}
}

View File

@ -36,7 +36,7 @@ let parsedLockFile: ParsedLockFile = {};
export const createNodes: CreateNodes = [
// Look for all lockfiles
combineGlobPatterns(LOCKFILES),
(lockFile, context) => {
(lockFile, _, context) => {
const pluginConfig = jsPluginConfig(context.nxJsonConfiguration);
if (!pluginConfig.analyzeLockfile) {
return {};
@ -74,6 +74,7 @@ export const createNodes: CreateNodes = [
];
export const createDependencies: CreateDependencies = (
_,
ctx: CreateDependenciesContext
) => {
const pluginConfig = jsPluginConfig(ctx.nxJsonConfiguration);

View File

@ -55,7 +55,8 @@ describe('nx project.json plugin', () => {
'/root'
);
expect(createNodes[1]('project.json', context)).toMatchInlineSnapshot(`
expect(createNodes[1]('project.json', undefined, context))
.toMatchInlineSnapshot(`
{
"projects": {
"root": {
@ -68,7 +69,7 @@ describe('nx project.json plugin', () => {
},
}
`);
expect(createNodes[1]('packages/lib-a/project.json', context))
expect(createNodes[1]('packages/lib-a/project.json', undefined, context))
.toMatchInlineSnapshot(`
{
"projects": {

View File

@ -17,7 +17,7 @@ export const CreateProjectJsonProjectsPlugin: NxPluginV2 = {
name: 'nx-core-build-project-json-nodes',
createNodes: [
'{project.json,**/project.json}',
(file, context) => {
(file, _, context) => {
const root = context.workspaceRoot;
const json = readJsonFile<ProjectConfiguration>(join(root, file));
const project = buildProjectFromProjectJson(json, file);

View File

@ -15,7 +15,6 @@ import { applyImplicitDependencies } from './utils/implicit-project-dependencies
import { normalizeProjectNodes } from './utils/normalize-project-nodes';
import {
CreateDependenciesContext,
CreateNodesContext,
isNxPluginV1,
isNxPluginV2,
loadNxPlugins,
@ -234,7 +233,7 @@ async function updateProjectGraphWithPlugins(
) {
const plugins = await loadNxPlugins(context.nxJsonConfiguration?.plugins);
let graph = initProjectGraph;
for (const plugin of plugins) {
for (const { plugin } of plugins) {
try {
if (
isNxPluginV1(plugin) &&
@ -280,12 +279,12 @@ async function updateProjectGraphWithPlugins(
);
const createDependencyPlugins = plugins.filter(
(plugin) => isNxPluginV2(plugin) && plugin.createDependencies
({ plugin }) => isNxPluginV2(plugin) && plugin.createDependencies
);
await Promise.all(
createDependencyPlugins.map(async (plugin) => {
createDependencyPlugins.map(async ({ plugin, options }) => {
try {
const dependencies = await plugin.createDependencies({
const dependencies = await plugin.createDependencies(options, {
...context,
});

View File

@ -111,7 +111,7 @@ describe('nx deps utils', () => {
createCache({}),
createPackageJsonDeps({ plugin: '2.0.0' }),
createProjectsConfiguration({}),
createNxJson({ pluginsConfig: { one: 1 } }),
createNxJson({ pluginsConfig: { somePlugin: { one: 1 } } }),
createTsConfigJson()
)
).toEqual(true);

View File

@ -2,7 +2,7 @@ import { existsSync } from 'fs';
import { ensureDirSync, renameSync } from 'fs-extra';
import { join } from 'path';
import { performance } from 'perf_hooks';
import { NxJsonConfiguration } from '../config/nx-json';
import { NxJsonConfiguration, PluginDefinition } from '../config/nx-json';
import {
FileData,
FileMap,
@ -17,6 +17,7 @@ import {
readJsonFile,
writeJsonFile,
} from '../utils/fileutils';
import { PackageJson } from '../utils/package-json';
import { nxVersion } from '../utils/versions';
export interface FileMapCache {
@ -24,7 +25,7 @@ export interface FileMapCache {
nxVersion: string;
deps: Record<string, string>;
pathMappings: Record<string, any>;
nxJsonPlugins: { name: string; version: string }[];
nxJsonPlugins: PluginData[];
pluginsConfig?: any;
fileMap: FileMap;
}
@ -111,10 +112,7 @@ export function createProjectFileMapCache(
fileMap: FileMap,
tsConfig: { compilerOptions?: { paths?: { [p: string]: any } } }
) {
const nxJsonPlugins = (nxJson?.plugins || []).map((p) => ({
name: p,
version: packageJsonDeps[p],
}));
const nxJsonPlugins = getNxJsonPluginsData(nxJson, packageJsonDeps);
const newValue: FileMapCache = {
version: '6.0',
nxVersion: nxVersion,
@ -207,16 +205,9 @@ export function shouldRecomputeWholeGraph(
}
// a new plugin has been added
if ((nxJson?.plugins || []).length !== cache.nxJsonPlugins.length)
return true;
// a plugin has changed
if (
(nxJson?.plugins || []).some((t) => {
const matchingPlugin = cache.nxJsonPlugins.find((p) => p.name === t);
if (!matchingPlugin) return true;
return matchingPlugin.version !== packageJsonDeps[t];
})
JSON.stringify(getNxJsonPluginsData(nxJson, packageJsonDeps)) !==
JSON.stringify(cache.nxJsonPlugins)
) {
return true;
}
@ -333,3 +324,24 @@ function processProjectNode(
}
}
}
type PluginData = {
name: string;
version: string;
options?: Record<string, unknown>;
};
function getNxJsonPluginsData(
nxJson: NxJsonConfiguration,
packageJsonDeps: Record<string, string>
): PluginData[] {
return (nxJson?.plugins || []).map((p) => {
const [plugin, options] =
typeof p === 'string' ? [p] : [p.plugin, p.options];
return {
name: plugin,
version: packageJsonDeps[plugin],
options,
};
});
}

View File

@ -371,19 +371,15 @@ describe('mergeProjectConfigurationIntoRootMap', () => {
},
})
.getRootMap();
mergeProjectConfigurationIntoRootMap(
rootMap,
{
root: 'libs/lib-a',
name: 'lib-a',
targets: {
build: {
command: 'tsc',
},
mergeProjectConfigurationIntoRootMap(rootMap, {
root: 'libs/lib-a',
name: 'lib-a',
targets: {
build: {
command: 'tsc',
},
},
'inferred-project-config-file.ts'
);
});
expect(rootMap.get('libs/lib-a')).toMatchInlineSnapshot(`
{
"name": "lib-a",
@ -399,33 +395,6 @@ describe('mergeProjectConfigurationIntoRootMap', () => {
}
`);
});
it("shouldn't overwrite project name, unless merging project from project.json", () => {
const rootMap = new RootMapBuilder()
.addProject({
name: 'bad-name',
root: 'libs/lib-a',
})
.getRootMap();
mergeProjectConfigurationIntoRootMap(
rootMap,
{
name: 'other-bad-name',
root: 'libs/lib-a',
},
'libs/lib-a/package.json'
);
expect(rootMap.get('libs/lib-a').name).toEqual('bad-name');
mergeProjectConfigurationIntoRootMap(
rootMap,
{
name: 'lib-a',
root: 'libs/lib-a',
},
'libs/lib-a/project.json'
);
expect(rootMap.get('libs/lib-a').name).toEqual('lib-a');
});
});
describe('readProjectsConfigurationsFromRootMap', () => {

View File

@ -1,5 +1,3 @@
import { basename } from 'node:path';
import { NxJsonConfiguration, TargetDefaults } from '../../config/nx-json';
import { ProjectGraphExternalNode } from '../../config/project-graph';
import {
@ -7,30 +5,20 @@ import {
TargetConfiguration,
} from '../../config/workspace-json-project-json';
import { NX_PREFIX } from '../../utils/logger';
import { NxPluginV2 } from '../../utils/nx-plugin';
import { LoadedNxPlugin } from '../../utils/nx-plugin';
import { workspaceRoot } from '../../utils/workspace-root';
import minimatch = require('minimatch');
export function mergeProjectConfigurationIntoRootMap(
projectRootMap: Map<string, ProjectConfiguration>,
project: ProjectConfiguration,
// project.json is a special case, so we need to detect it.
file: string
project: ProjectConfiguration
): void {
const matchingProject = projectRootMap.get(project.root);
if (!matchingProject) {
projectRootMap.set(project.root, project);
return;
} else if (
project.name &&
project.name !== matchingProject.name &&
basename(file) === 'project.json'
) {
// `name` inside project.json overrides any names from
// inference plugins
matchingProject.name = project.name;
}
// This handles top level properties that are overwritten.
@ -41,7 +29,6 @@ export function mergeProjectConfigurationIntoRootMap(
const updatedProjectConfiguration = {
...matchingProject,
...project,
name: matchingProject.name,
};
// The next blocks handle properties that should be themselves merged (e.g. targets, tags, and implicit dependencies)
@ -79,7 +66,7 @@ export function mergeProjectConfigurationIntoRootMap(
export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
nxJson: NxJsonConfiguration,
projectFiles: string[], // making this parameter allows devkit to pick up newly created projects
plugins: NxPluginV2[],
plugins: LoadedNxPlugin[],
root: string = workspaceRoot
): {
projects: Record<string, ProjectConfiguration>;
@ -89,7 +76,7 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
const externalNodes: Record<string, ProjectGraphExternalNode> = {};
// We iterate over plugins first - this ensures that plugins specified first take precedence.
for (const plugin of plugins) {
for (const { plugin, options } of plugins) {
const [pattern, createNodes] = plugin.createNodes ?? [];
if (!pattern) {
continue;
@ -97,7 +84,7 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
for (const file of projectFiles) {
if (minimatch(file, pattern, { dot: true })) {
const { projects: projectNodes, externalNodes: pluginExternalNodes } =
createNodes(file, {
createNodes(file, options, {
nxJsonConfiguration: nxJson,
workspaceRoot: root,
});
@ -105,8 +92,7 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
projectNodes[node].name ??= node;
mergeProjectConfigurationIntoRootMap(
projectRootMap,
projectNodes[node],
file
projectNodes[node]
);
}
Object.assign(externalNodes, pluginExternalNodes);

View File

@ -22,6 +22,7 @@ import {
} from '../../../plugins/package-json-workspaces';
import { buildProjectsConfigurationsFromProjectPathsAndPlugins } from './project-configuration-utils';
import {
LoadedNxPlugin,
loadNxPlugins,
loadNxPluginsSync,
NxPluginV2,
@ -137,9 +138,9 @@ export async function retrieveProjectConfigurationsWithAngularProjects(
if (
shouldMergeAngularProjects(workspaceRoot, true) &&
!plugins.some((p) => p.name === NX_ANGULAR_JSON_PLUGIN_NAME)
!plugins.some((p) => p.plugin.name === NX_ANGULAR_JSON_PLUGIN_NAME)
) {
plugins.push(NxAngularJsonPlugin);
plugins.push({ plugin: NxAngularJsonPlugin });
}
const globs = configurationGlobs(workspaceRoot, plugins);
@ -169,7 +170,7 @@ export function retrieveProjectConfigurationsSync(
function _retrieveProjectConfigurations(
workspaceRoot: string,
nxJson: NxJsonConfiguration,
plugins: NxPluginV2[],
plugins: LoadedNxPlugin[],
globs: string[]
): {
externalNodes: Record<string, ProjectGraphExternalNode>;
@ -236,8 +237,8 @@ export function retrieveProjectConfigurationsWithoutPluginInference(
projectGlobPatterns,
(configs: string[]) => {
const { projects } = createProjectConfigurations(root, nxJson, configs, [
getNxPackageJsonWorkspacesPlugin(root),
CreateProjectJsonProjectsPlugin,
{ plugin: getNxPackageJsonWorkspacesPlugin(root) },
{ plugin: CreateProjectJsonProjectsPlugin },
]);
return {
projectNodes: projects,
@ -273,7 +274,7 @@ export function createProjectConfigurations(
workspaceRoot: string,
nxJson: NxJsonConfiguration,
configFiles: string[],
plugins: NxPluginV2[]
plugins: LoadedNxPlugin[]
): {
projects: Record<string, ProjectConfiguration>;
externalNodes: Record<string, ProjectGraphExternalNode>;
@ -305,11 +306,11 @@ export function createProjectConfigurations(
export function configurationGlobs(
workspaceRoot: string,
plugins: NxPluginV2[]
plugins: LoadedNxPlugin[]
): string[] {
const globPatterns: string[] =
configurationGlobsWithoutPlugins(workspaceRoot);
for (const plugin of plugins) {
for (const { plugin } of plugins) {
if (plugin.createNodes) {
globPatterns.push(plugin.createNodes[0]);
}

View File

@ -1,9 +1,7 @@
import { existsSync } from 'fs';
import * as path from 'path';
import {
FileData,
FileMap,
ProjectFileMap,
ProjectGraph,
ProjectGraphExternalNode,
} from '../config/project-graph';
@ -19,10 +17,7 @@ import {
registerTranspiler,
registerTsConfigPaths,
} from '../plugins/js/utils/register';
import {
ProjectConfiguration,
ProjectsConfigurations,
} from '../config/workspace-json-project-json';
import { ProjectConfiguration } from '../config/workspace-json-project-json';
import { logger } from './logger';
import {
createProjectRootMappingsFromProjectConfigurations,
@ -32,7 +27,7 @@ import { normalizePath } from './path';
import { dirname, join } from 'path';
import { getNxRequirePaths } from './installation-directory';
import { readTsConfig } from '../plugins/js/utils/typescript';
import { NxJsonConfiguration } from '../config/nx-json';
import { NxJsonConfiguration, PluginDefinition } from '../config/nx-json';
import type * as ts from 'typescript';
import { retrieveProjectConfigurationsWithoutPluginInference } from '../project-graph/utils/retrieve-workspace-files';
@ -45,6 +40,7 @@ import {
} from '../adapter/angular-json';
import { getNxPackageJsonWorkspacesPlugin } from '../../plugins/package-json-workspaces';
import { CreateProjectJsonProjectsPlugin } from '../plugins/project-json/build-nodes/project-json';
import { FileMapCache } from '../project-graph/nx-deps-cache';
/**
* Context for {@link CreateNodesFunction}
@ -58,8 +54,11 @@ export interface CreateNodesContext {
* A function which parses a configuration file into a set of nodes.
* Used for creating nodes for the {@link ProjectGraph}
*/
export type CreateNodesFunction = (
export type CreateNodesFunction<
T extends Record<string, unknown> = Record<string, unknown>
> = (
projectConfigurationFile: string,
options: T | undefined,
context: CreateNodesContext
) => {
projects?: Record<string, ProjectConfiguration>;
@ -69,9 +68,11 @@ export type CreateNodesFunction = (
/**
* A pair of file patterns and {@link CreateNodesFunction}
*/
export type CreateNodes = readonly [
export type CreateNodes<
T extends Record<string, unknown> = Record<string, unknown>
> = readonly [
projectFilePattern: string,
createNodesFunction: CreateNodesFunction
createNodesFunction: CreateNodesFunction<T>
];
/**
@ -110,27 +111,32 @@ export interface CreateDependenciesContext {
* A function which parses files in the workspace to create dependencies in the {@link ProjectGraph}
* Use {@link validateDependency} to validate dependencies
*/
export type CreateDependencies = (
export type CreateDependencies<
T extends Record<string, unknown> = Record<string, unknown>
> = (
options: T | undefined,
context: CreateDependenciesContext
) => RawProjectGraphDependency[] | Promise<RawProjectGraphDependency[]>;
/**
* A plugin for Nx which creates nodes and dependencies for the {@link ProjectGraph}
*/
export type NxPluginV2 = {
export type NxPluginV2<
T extends Record<string, unknown> = Record<string, unknown>
> = {
name: string;
/**
* Provides a file pattern and function that retrieves configuration info from
* those files. e.g. { '**\/*.csproj': buildProjectsFromCsProjFile }
*/
createNodes?: CreateNodes;
createNodes?: CreateNodes<T>;
// 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;
createDependencies?: CreateDependencies<T>;
};
export * from './nx-plugin.deprecated';
@ -140,11 +146,16 @@ export * from './nx-plugin.deprecated';
*/
export type NxPlugin = NxPluginV1 | NxPluginV2;
export type LoadedNxPlugin = {
plugin: NxPluginV2 & Pick<NxPluginV1, 'processProjectGraph'>;
options?: Record<string, unknown>;
};
// 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.
let nxPluginCache: Map<string, NxPlugin> = new Map();
let nxPluginCache: Map<string, LoadedNxPlugin['plugin']> = new Map();
function getPluginPathAndName(
moduleName: string,
@ -191,50 +202,66 @@ function getPluginPathAndName(
}
export async function loadNxPluginAsync(
moduleName: string,
pluginDefinition: PluginDefinition,
paths: string[],
root: string
) {
): Promise<LoadedNxPlugin> {
const { plugin: moduleName, options } =
typeof pluginDefinition === 'object'
? pluginDefinition
: { plugin: pluginDefinition, options: undefined };
let pluginModule = nxPluginCache.get(moduleName);
if (pluginModule) {
return pluginModule;
return { plugin: pluginModule, options };
}
let { pluginPath, name } = getPluginPathAndName(moduleName, paths, root);
const plugin = (await import(pluginPath)) as NxPlugin;
const plugin = ensurePluginIsV2(
(await import(pluginPath)) as LoadedNxPlugin['plugin']
);
plugin.name ??= name;
nxPluginCache.set(moduleName, plugin);
return plugin;
return { plugin, options };
}
function loadNxPluginSync(moduleName: string, paths: string[], root: string) {
function loadNxPluginSync(
pluginDefinition: PluginDefinition,
paths: string[],
root: string
): LoadedNxPlugin {
const { plugin: moduleName, options } =
typeof pluginDefinition === 'object'
? pluginDefinition
: { plugin: pluginDefinition, options: undefined };
let pluginModule = nxPluginCache.get(moduleName);
if (pluginModule) {
return pluginModule;
return { plugin: pluginModule, options };
}
let { pluginPath, name } = getPluginPathAndName(moduleName, paths, root);
const plugin = require(pluginPath) as NxPlugin;
const plugin = ensurePluginIsV2(
require(pluginPath)
) as LoadedNxPlugin['plugin'];
plugin.name ??= name;
nxPluginCache.set(moduleName, plugin);
return plugin;
return { plugin, options };
}
/**
* @deprecated Use loadNxPlugins instead.
*/
export function loadNxPluginsSync(
plugins: string[],
plugins: NxJsonConfiguration['plugins'],
paths = getNxRequirePaths(),
root = workspaceRoot
): (NxPluginV2 & Pick<NxPluginV1, 'processProjectGraph'>)[] {
): LoadedNxPlugin[] {
// TODO: This should be specified in nx.json
// Temporarily load js as if it were a plugin which is built into nx
// In the future, this will be optional and need to be specified in nx.json
const result: NxPlugin[] = [...getDefaultPluginsSync(root)];
const result: LoadedNxPlugin[] = [...getDefaultPluginsSync(root)];
if (shouldMergeAngularProjects(root, false)) {
result.push(NxAngularJsonPlugin);
result.push({ plugin: NxAngularJsonPlugin, options: undefined });
}
plugins ??= [];
@ -253,19 +280,19 @@ export function loadNxPluginsSync(
// We push the nx core node plugins onto the end, s.t. it overwrites any other plugins
result.push(
getNxPackageJsonWorkspacesPlugin(root),
CreateProjectJsonProjectsPlugin
{ plugin: getNxPackageJsonWorkspacesPlugin(root) },
{ plugin: CreateProjectJsonProjectsPlugin }
);
return result.map(ensurePluginIsV2);
return result;
}
export async function loadNxPlugins(
plugins: string[],
plugins: PluginDefinition[],
paths = getNxRequirePaths(),
root = workspaceRoot
): Promise<(NxPluginV2 & Pick<NxPluginV1, 'processProjectGraph'>)[]> {
const result: NxPlugin[] = [...(await getDefaultPlugins(root))];
): Promise<LoadedNxPlugin[]> {
const result: LoadedNxPlugin[] = [...(await getDefaultPlugins(root))];
// TODO: These should be specified in nx.json
// Temporarily load js as if it were a plugin which is built into nx
@ -279,11 +306,11 @@ export async function loadNxPlugins(
// We push the nx core node plugins onto the end, s.t. it overwrites any other plugins
result.push(
getNxPackageJsonWorkspacesPlugin(root),
CreateProjectJsonProjectsPlugin
{ plugin: getNxPackageJsonWorkspacesPlugin(root) },
{ plugin: CreateProjectJsonProjectsPlugin }
);
return result.map(ensurePluginIsV2);
return result;
}
function ensurePluginIsV2(plugin: NxPlugin): NxPluginV2 {
@ -494,22 +521,26 @@ function readPluginMainFromProjectConfiguration(
return main;
}
async function getDefaultPlugins(root: string) {
const plugins: NxPlugin[] = [await import('../plugins/js')];
async function getDefaultPlugins(root: string): Promise<LoadedNxPlugin[]> {
const plugins: NxPluginV2[] = [await import('../plugins/js')];
if (shouldMergeAngularProjects(root, false)) {
plugins.push(
await import('../adapter/angular-json').then((m) => m.NxAngularJsonPlugin)
);
}
return plugins;
return plugins.map((p) => ({
plugin: p,
}));
}
function getDefaultPluginsSync(root: string) {
const plugins: NxPlugin[] = [require('../plugins/js')];
function getDefaultPluginsSync(root: string): LoadedNxPlugin[] {
const plugins: NxPluginV2[] = [require('../plugins/js')];
if (shouldMergeAngularProjects(root, false)) {
plugins.push(require('../adapter/angular-json').NxAngularJsonPlugin);
}
return plugins;
return plugins.map((p) => ({
plugin: p,
}));
}

View File

@ -21,7 +21,7 @@ export async function getLocalWorkspacePlugins(
if (existsSync(packageJsonPath)) {
const packageJson: PackageJson = readJsonFile(packageJsonPath);
const includeRuntimeCapabilities = nxJson?.plugins?.some((p) =>
p.startsWith(packageJson.name)
(typeof p === 'string' ? p : p.plugin).startsWith(packageJson.name)
);
const capabilities = await getPluginCapabilities(
workspaceRoot,

View File

@ -103,11 +103,13 @@ async function tryGetModule(
packageJson['nx-migrations'] ??
packageJson['schematics'] ??
packageJson['builders']
? await loadNxPluginAsync(
packageJson.name,
getNxRequirePaths(workspaceRoot),
workspaceRoot
)
? (
await loadNxPluginAsync(
packageJson.name,
getNxRequirePaths(workspaceRoot),
workspaceRoot
)
).plugin
: ({
name: packageJson.name,
} as NxPlugin);