Skip to content

Commit

Permalink
feat(core): support for project inference and graph extension local p…
Browse files Browse the repository at this point in the history
…lugins
  • Loading branch information
AgentEnder committed Feb 22, 2022
1 parent 7641644 commit fdf3de5
Show file tree
Hide file tree
Showing 4 changed files with 447 additions and 376 deletions.
34 changes: 34 additions & 0 deletions e2e/nx-plugin/src/nx-plugin.test.ts
Expand Up @@ -9,6 +9,8 @@ import {
runCLI,
runCLIAsync,
uniq,
updateFile,
createFile,
} from '@nrwl/e2e/utils';

describe('Nx Plugin', () => {
Expand Down Expand Up @@ -183,11 +185,43 @@ describe('Nx Plugin', () => {
`generate @nrwl/nx-plugin:generator ${generator} --project=${plugin}`
);

updateFile(
`libs/${plugin}/src/index.ts`,
`
export function registerProjectTargets(f) {
if (f === 'my-project-file') {
return {
build: {
executor: "@nrwl/workspace:run-commands",
options: {
command: "echo 'custom registered target'"
}
}
}
}
}
export const projectFilePatterns = ['my-project-file'];
`
);

updateFile(`nx.json`, (nxJson) => {
const nx = JSON.parse(nxJson);
nx.plugins = [`@${npmScope}/${plugin}`];
return JSON.stringify(nx, null, 2);
});

const inferredProject = uniq('inferred');
createFile(`libs/${inferredProject}/my-project-file`);

runCLI(
`generate @${npmScope}/${plugin}:${generator} --name ${generatedProject}`
);
expect(() => checkFilesExist(`libs/${generatedProject}`)).not.toThrow();
expect(() => runCLI(`build ${generatedProject}`)).not.toThrow();
expect(runCLI(`build ${inferredProject}`)).toContain(
'custom registered target'
);
}, 90000);

describe('--directory', () => {
Expand Down
174 changes: 149 additions & 25 deletions packages/tao/src/shared/nx-plugin.ts
@@ -1,10 +1,18 @@
import { execSync } from 'child_process';
import { sync } from 'fast-glob';
import { existsSync } from 'fs';
import { dirname, join } from 'path';
import * as path from 'path';

import { appRootPath } from '../utils/app-root';
import { readJsonFile } from '../utils/fileutils';
import { PackageJson } from './package-json';
import { getPackageManagerCommand } from './package-manager';
import { ProjectGraphProcessor } from './project-graph';
import { TargetConfiguration } from './workspace';
import { Workspaces } from './workspace';
import {
TargetConfiguration,
WorkspaceJsonConfiguration,
} from './workspace.model';

export type ProjectTargetConfigurator = (
file: string
Expand All @@ -25,30 +33,36 @@ export interface NxPlugin {
projectFilePatterns?: string[];
}

function findPluginPackageJson(path: string, plugin: string) {
while (true) {
if (!path.startsWith(appRootPath)) {
throw new Error("Couldn't find a package.json for Nx plugin:" + plugin);
}
if (existsSync(join(path, 'package.json'))) {
return join(path, 'package.json');
}
path = dirname(path);
}
}

// 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: NxPlugin[] = null;
export function loadNxPlugins(plugins?: string[]): NxPlugin[] {
export function loadNxPlugins(
plugins?: string[],
paths = [appRootPath]
): NxPlugin[] {
return plugins?.length
? nxPluginCache ||
(nxPluginCache = plugins.map((path) => {
const pluginPath = require.resolve(path, {
paths: [appRootPath],
});

const { name } = readJsonFile(
findPluginPackageJson(pluginPath, path)
);
(nxPluginCache = plugins.map((moduleName) => {
let pluginPath: string;
try {
pluginPath = require.resolve(moduleName, {
paths,
});
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
pluginPath = resolveLocalNxPlugin(moduleName);
} else {
throw e;
}
}
const packageJsonPath = path.join(pluginPath, 'package.json');
const { name } =
!['.ts', '.js'].some((x) => x === path.extname(pluginPath)) && // 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: path.basename(pluginPath) }; // use the name of the file we point to
const plugin = require(pluginPath) as NxPlugin;
plugin.name = name;

Expand All @@ -69,14 +83,124 @@ export function mergePluginTargetsWithNxTargets(
}

const projectFiles = sync(`+(${plugin.projectFilePatterns.join('|')})`, {
cwd: join(appRootPath, projectRoot),
cwd: path.join(appRootPath, projectRoot),
});
for (const projectFile of projectFiles) {
newTargets = {
...newTargets,
...plugin.registerProjectTargets(join(projectRoot, projectFile)),
...plugin.registerProjectTargets(path.join(projectRoot, projectFile)),
};
}
}
return { ...newTargets, ...targets };
}

export function readPluginPackageJson(
pluginName: string,
paths = [appRootPath]
): {
path: string;
json: PackageJson;
} {
let packageJsonPath: string;
try {
packageJsonPath = require.resolve(`${pluginName}/package.json`, {
paths,
});
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
const localPluginPath = resolveLocalNxPlugin(pluginName);
if (localPluginPath) {
const localPluginPackageJson = path.join(
localPluginPath,
'package.json'
);
return {
path: localPluginPackageJson,
json: readJsonFile(localPluginPackageJson),
};
}
}
throw e;
}
return { json: readJsonFile(packageJsonPath), path: packageJsonPath };
}

/**
* 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 = {};
export function resolveLocalNxPlugin(
importPath: string,
root = appRootPath
): string | null {
localPluginCache[importPath] ??= buildLocalPlugin(importPath, root);
return localPluginCache[importPath];
}

function buildLocalPlugin(importPath: string, root = appRootPath): string {
const workspace = new Workspaces(root).readWorkspaceConfiguration();
const plugin = findNxProjectForImportPath(importPath, workspace, root);
if (!plugin) {
return null;
}

const projectConfig = workspace.projects[plugin];

/**
* todo(v14-v15) make this stuff async + use task scheduler
*
* Ideally, we wouldn't invoke nx via execSync here. We should be
* able to run an executor given a project and target programmatically.
* Currently, runExecutor is async and doesn't hit the task orchestrator.
* So to use it, we would have to make a bunch of this stuff async (a breaking change),
* and we would also not benefit from remote or local caches which would be much slower.
* Therefore, currently we use execSync here. We should work towards simplifying
* the task orchestrator API, while consolidating @nrwl/workspace and @nrwl/tao
* to make this something like `await TaskScheduler.runTaskNow({project, target: 'build'})`,
* but that API doesn't exist.
*/
execSync(`${getPackageManagerCommand().exec} nx build ${plugin}`, {
cwd: root,
});
return path.join(root, projectConfig.targets?.build?.options?.outputPath);
}

function findNxProjectForImportPath(
importPath: string,
workspace: WorkspaceJsonConfiguration,
root = appRootPath
): string | null {
const tsConfigPaths: Record<string, string[]> = readJsonFile(
path.join(root, 'tsconfig.base.json')
)?.compilerOptions?.paths;
const possiblePaths = tsConfigPaths[importPath]?.map((p) =>
path.resolve(root, p)
);
if (tsConfigPaths[importPath]) {
const projectRootMappings = Object.entries(workspace.projects).reduce(
(m, [project, config]) => {
m[path.resolve(root, config.root)] = project;
return m;
},
{}
);
for (const root of Object.keys(projectRootMappings)) {
if (possiblePaths.some((p) => p.startsWith(root))) {
return projectRootMappings[root];
}
}
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
);
}
}

0 comments on commit fdf3de5

Please sign in to comment.