Skip to content

Commit

Permalink
feat(core): use custom resolution to resolve from source local plugin…
Browse files Browse the repository at this point in the history
…s with artifacts pointing to the outputs (#29222)

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

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

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

## Current Behavior
<!-- This is the behavior we have today -->

Local Nx plugins in the new TS setup can't be resolved properly if they
aren't built first. Graph plugins can't be built either because the
graph is needed to run a task, but the plugin must be built to construct
the graph.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

Local Nx plugins should work in the new TS setup. A custom resolution is
added to resolve the local plugin artifacts from the source.

It will try to use a `development` condition from the `exports` entry in
`package.json` if it exists. If it doesn't, it will fall back to guess
the source based on the artifact path and some commonly known/used
source dirs: `.`, `./src`, `./src/lib`.

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

Fixes #
  • Loading branch information
leosvelperez authored Dec 13, 2024
1 parent 5bdda1d commit 48cd50a
Show file tree
Hide file tree
Showing 18 changed files with 542 additions and 45 deletions.
4 changes: 2 additions & 2 deletions docs/generated/devkit/ProjectGraphProjectNode.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A node describing a project in a workspace

- [data](../../devkit/documents/ProjectGraphProjectNode#data): ProjectConfiguration & Object
- [name](../../devkit/documents/ProjectGraphProjectNode#name): string
- [type](../../devkit/documents/ProjectGraphProjectNode#type): "app" | "e2e" | "lib"
- [type](../../devkit/documents/ProjectGraphProjectNode#type): "lib" | "app" | "e2e"

## Properties

Expand All @@ -28,4 +28,4 @@ Additional metadata about a project

### type

**type**: `"app"` \| `"e2e"` \| `"lib"`
**type**: `"lib"` \| `"app"` \| `"e2e"`
193 changes: 193 additions & 0 deletions e2e/plugin/src/nx-plugin-ts-solution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import {
checkFilesExist,
cleanupProject,
createFile,
newProject,
renameFile,
runCLI,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import {
ASYNC_GENERATOR_EXECUTOR_CONTENTS,
NX_PLUGIN_V2_CONTENTS,
} from './nx-plugin.fixtures';

describe('Nx Plugin (TS solution)', () => {
let workspaceName: string;

beforeAll(() => {
workspaceName = newProject({ preset: 'ts', packages: ['@nx/plugin'] });
});

afterAll(() => cleanupProject());

it('should be able to infer projects and targets', async () => {
const plugin = uniq('plugin');
runCLI(`generate @nx/plugin:plugin packages/${plugin}`);

// Setup project inference + target inference
updateFile(`packages/${plugin}/src/index.ts`, NX_PLUGIN_V2_CONTENTS);

// Register plugin in nx.json (required for inference)
updateJson(`nx.json`, (nxJson) => {
nxJson.plugins = [
{
plugin: `@${workspaceName}/${plugin}`,
options: { inferredTags: ['my-tag'] },
},
];
return nxJson;
});

// Create project that should be inferred by Nx
const inferredProject = uniq('inferred');
createFile(
`packages/${inferredProject}/package.json`,
JSON.stringify({
name: inferredProject,
version: '0.0.1',
})
);
createFile(`packages/${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).toContain('my-tag');
expect(configuration.metadata.technologies).toEqual(['my-plugin']);
});

it('should be able to use local generators and executors', async () => {
const plugin = uniq('plugin');
const generator = uniq('generator');
const executor = uniq('executor');
const generatedProject = uniq('project');

runCLI(`generate @nx/plugin:plugin packages/${plugin}`);

runCLI(
`generate @nx/plugin:generator --name ${generator} --path packages/${plugin}/src/generators/${generator}/generator`
);

runCLI(
`generate @nx/plugin:executor --name ${executor} --path packages/${plugin}/src/executors/${executor}/executor`
);

updateFile(
`packages/${plugin}/src/executors/${executor}/executor.ts`,
ASYNC_GENERATOR_EXECUTOR_CONTENTS
);

runCLI(
`generate @${workspaceName}/${plugin}:${generator} --name ${generatedProject}`
);

updateJson(`libs/${generatedProject}/project.json`, (project) => {
project.targets['execute'] = {
executor: `@${workspaceName}/${plugin}:${executor}`,
};
return project;
});

expect(() => checkFilesExist(`libs/${generatedProject}`)).not.toThrow();
expect(() => runCLI(`execute ${generatedProject}`)).not.toThrow();
});

it('should be able to resolve local generators and executors using package.json development condition export', async () => {
const plugin = uniq('plugin');
const generator = uniq('generator');
const executor = uniq('executor');
const generatedProject = uniq('project');

runCLI(`generate @nx/plugin:plugin packages/${plugin}`);

// move/generate everything in the "code" folder, which is not a standard location and wouldn't
// be considered by the fall back resolution logic, so the only way it could be resolved is if
// the development condition export is used
renameFile(
`packages/${plugin}/src/index.ts`,
`packages/${plugin}/code/index.ts`
);

runCLI(
`generate @nx/plugin:generator --name ${generator} --path packages/${plugin}/code/generators/${generator}/generator`
);
runCLI(
`generate @nx/plugin:executor --name ${executor} --path packages/${plugin}/code/executors/${executor}/executor`
);

updateJson(`packages/${plugin}/package.json`, (pkg) => {
pkg.nx.sourceRoot = `packages/${plugin}/code`;
pkg.nx.targets.build.options.main = `packages/${plugin}/code/index.ts`;
pkg.nx.targets.build.options.rootDir = `packages/${plugin}/code`;
pkg.nx.targets.build.options.assets.forEach(
(asset: { input: string }) => {
asset.input = `./packages/${plugin}/code`;
}
);
pkg.exports = {
'.': {
types: './dist/index.d.ts',
development: './code/index.ts',
default: './dist/index.js',
},
'./package.json': './package.json',
'./generators.json': {
development: './generators.json',
default: './generators.json',
},
'./executors.json': './executors.json',
'./dist/generators/*/schema.json': {
development: './code/generators/*/schema.json',
default: './dist/generators/*/schema.json',
},
'./dist/generators/*/generator': {
types: './dist/generators/*/generator.d.ts',
development: './code/generators/*/generator.ts',
default: './dist/generators/*/generator.js',
},
'./dist/executors/*/schema.json': {
development: './code/executors/*/schema.json',
default: './dist/executors/*/schema.json',
},
'./dist/executors/*/executor': {
types: './dist/executors/*/executor.d.ts',
development: './code/executors/*/executor.ts',
default: './dist/executors/*/executor.js',
},
};
return pkg;
});

updateJson(`packages/${plugin}/tsconfig.lib.json`, (tsconfig) => {
tsconfig.compilerOptions.rootDir = 'code';
tsconfig.include = ['code/**/*.ts'];
return tsconfig;
});

updateFile(
`packages/${plugin}/code/executors/${executor}/executor.ts`,
ASYNC_GENERATOR_EXECUTOR_CONTENTS
);

runCLI(
`generate @${workspaceName}/${plugin}:${generator} --name ${generatedProject}`
);

updateJson(`libs/${generatedProject}/project.json`, (project) => {
project.targets['execute'] = {
executor: `@${workspaceName}/${plugin}:${executor}`,
};
return project;
});

expect(() => checkFilesExist(`libs/${generatedProject}`)).not.toThrow();
expect(() => runCLI(`execute ${generatedProject}`)).not.toThrow();
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@
"react-router-dom": "^6.23.1",
"react-textarea-autosize": "^8.5.3",
"regenerator-runtime": "0.13.7",
"resolve.exports": "1.1.0",
"resolve.exports": "2.0.3",
"rollup": "^4.14.0",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-postcss": "^4.0.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/jest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"jest-resolve": "^29.4.1",
"jest-util": "^29.4.1",
"minimatch": "9.0.3",
"resolve.exports": "1.1.0",
"resolve.exports": "2.0.3",
"semver": "^7.5.3",
"tslib": "^2.3.0",
"yargs-parser": "21.1.1"
Expand Down
2 changes: 1 addition & 1 deletion packages/jest/plugins/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ module.exports = function (path: string, options: ResolverOptions) {
return path;
}

return resolveExports(pkg, path) || path;
return resolveExports(pkg, path)?.[0] || path;
},
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/nx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"npm-run-path": "^4.0.1",
"open": "^8.4.0",
"ora": "5.3.0",
"resolve.exports": "2.0.3",
"semver": "^7.5.3",
"string-width": "^4.2.3",
"tar-stream": "~2.2.0",
Expand Down
30 changes: 23 additions & 7 deletions packages/nx/src/adapter/ngcli-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,9 @@ async function getWrappedWorkspaceNodeModulesArchitectHost(
optionSchema: builderInfo.schema,
import: resolveImplementation(
executorConfig.implementation,
dirname(executorsFilePath)
dirname(executorsFilePath),
packageName,
this.projects
),
};
}
Expand Down Expand Up @@ -1240,25 +1242,33 @@ async function getWrappedWorkspaceNodeModulesArchitectHost(
const { executorsFilePath, executorConfig, isNgCompat } =
this.readExecutorsJson(nodeModule, executor);
const executorsDir = dirname(executorsFilePath);
const schemaPath = resolveSchema(executorConfig.schema, executorsDir);
const schemaPath = resolveSchema(
executorConfig.schema,
executorsDir,
nodeModule,
this.projects
);
const schema = normalizeExecutorSchema(readJsonFile(schemaPath));

const implementationFactory = this.getImplementationFactory<Executor>(
executorConfig.implementation,
executorsDir
executorsDir,
nodeModule
);

const batchImplementationFactory = executorConfig.batchImplementation
? this.getImplementationFactory<TaskGraphExecutor>(
executorConfig.batchImplementation,
executorsDir
executorsDir,
nodeModule
)
: null;

const hasherFactory = executorConfig.hasher
? this.getImplementationFactory<CustomHasher>(
executorConfig.hasher,
executorsDir
executorsDir,
nodeModule
)
: null;

Expand All @@ -1278,9 +1288,15 @@ async function getWrappedWorkspaceNodeModulesArchitectHost(

private getImplementationFactory<T>(
implementation: string,
executorsDir: string
executorsDir: string,
packageName: string
): () => T {
return getImplementationFactory(implementation, executorsDir);
return getImplementationFactory(
implementation,
executorsDir,
packageName,
this.projects
);
}
}

Expand Down
11 changes: 9 additions & 2 deletions packages/nx/src/command-line/generate/generator-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ export function getGeneratorInformation(
generatorsJson.generators?.[normalizedGeneratorName] ||
generatorsJson.schematics?.[normalizedGeneratorName];
const isNgCompat = !generatorsJson.generators?.[normalizedGeneratorName];
const schemaPath = resolveSchema(generatorConfig.schema, generatorsDir);
const schemaPath = resolveSchema(
generatorConfig.schema,
generatorsDir,
collectionName,
projects
);
const schema = readJsonFile(schemaPath);
if (!schema.properties || typeof schema.properties !== 'object') {
schema.properties = {};
Expand All @@ -49,7 +54,9 @@ export function getGeneratorInformation(
generatorConfig.implementation || generatorConfig.factory;
const implementationFactory = getImplementationFactory<Generator>(
generatorConfig.implementation,
generatorsDir
generatorsDir,
collectionName,
projects
);
const normalizedGeneratorConfiguration: GeneratorsJsonEntry = {
...generatorConfig,
Expand Down
19 changes: 15 additions & 4 deletions packages/nx/src/command-line/run/executor-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,36 @@ export function getExecutorInformation(
projects
);
const executorsDir = dirname(executorsFilePath);
const schemaPath = resolveSchema(executorConfig.schema, executorsDir);
const schemaPath = resolveSchema(
executorConfig.schema,
executorsDir,
nodeModule,
projects
);
const schema = normalizeExecutorSchema(readJsonFile(schemaPath));

const implementationFactory = getImplementationFactory<Executor>(
executorConfig.implementation,
executorsDir
executorsDir,
nodeModule,
projects
);

const batchImplementationFactory = executorConfig.batchImplementation
? getImplementationFactory<TaskGraphExecutor>(
executorConfig.batchImplementation,
executorsDir
executorsDir,
nodeModule,
projects
)
: null;

const hasherFactory = executorConfig.hasher
? getImplementationFactory<CustomHasher>(
executorConfig.hasher,
executorsDir
executorsDir,
nodeModule,
projects
)
: null;

Expand Down
Loading

0 comments on commit 48cd50a

Please sign in to comment.